diff --git a/db-updates/0014-add-project-settings.sql b/db-updates/0014-add-project-settings.sql index b5b2a0d0..7c2bc37e 100644 --- a/db-updates/0014-add-project-settings.sql +++ b/db-updates/0014-add-project-settings.sql @@ -1,15 +1,15 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ /** * Author: Jim Ewald * Created: Aug 27, 2018 * * Add a field to the project table to store a JSON encoded group of project * settings. + * + * Add a field to the user profile to store JSON encoded settings related to + * specific user. */ ALTER TABLE blocklyprop.project ADD settings TEXT NULL; +ALTER TABLE cloudsession.user ADD settings TEXT NULL; + diff --git a/src/main/java/com/parallax/server/blocklyprop/config/RestModule.java b/src/main/java/com/parallax/server/blocklyprop/config/RestModule.java index 04e11b84..999523c3 100644 --- a/src/main/java/com/parallax/server/blocklyprop/config/RestModule.java +++ b/src/main/java/com/parallax/server/blocklyprop/config/RestModule.java @@ -25,10 +25,13 @@ import com.parallax.server.blocklyprop.rest.RestMotd; import com.parallax.server.blocklyprop.rest.RestProfile; import com.parallax.server.blocklyprop.rest.RestProject; +import com.parallax.server.blocklyprop.rest.RestV2Project; import com.parallax.server.blocklyprop.rest.RestSharedProject; import com.parallax.server.blocklyprop.rest.RestUser; + import com.sun.jersey.guice.JerseyServletModule; import com.sun.jersey.guice.spi.container.servlet.GuiceContainer; + import javax.ws.rs.ext.MessageBodyReader; import javax.ws.rs.ext.MessageBodyWriter; import org.codehaus.jackson.jaxrs.JacksonJsonProvider; @@ -45,6 +48,8 @@ protected void configureServlets() { bind(RestCompile.class); bind(RestUser.class); bind(RestProject.class); + bind(RestV2Project.class); + bind(RestSharedProject.class); bind(RestProfile.class); bind(RestMotd.class); diff --git a/src/main/java/com/parallax/server/blocklyprop/config/SetupConfig.java b/src/main/java/com/parallax/server/blocklyprop/config/SetupConfig.java index 49aae174..fd830cdb 100644 --- a/src/main/java/com/parallax/server/blocklyprop/config/SetupConfig.java +++ b/src/main/java/com/parallax/server/blocklyprop/config/SetupConfig.java @@ -26,11 +26,12 @@ import com.google.inject.Injector; import com.google.inject.servlet.GuiceServletContextListener; -import com.parallax.server.blocklyprop.SessionData; import com.parallax.server.blocklyprop.jsp.Properties; import com.parallax.server.blocklyprop.monitoring.Monitor; + import java.sql.Driver; import java.sql.DriverManager; +import java.sql.SQLException; import java.util.Enumeration; import javax.servlet.ServletContextEvent; import org.apache.commons.configuration.Configuration; @@ -40,22 +41,25 @@ import org.slf4j.LoggerFactory; + /** * * @author Michel */ public class SetupConfig extends GuiceServletContextListener { - /** - * Application-specific configuration options - */ + // Application logging connector + private final Logger LOG = LoggerFactory.getLogger(SetupConfig.class); + + // Application-specific configuration options private Configuration configuration; - + + /** - * Application logging connector + * Create a Guice injector object + * + * @return a Guice injector object */ - private final Logger LOG = LoggerFactory.getLogger(SetupConfig.class); - @Override protected Injector getInjector() { readConfiguration(); @@ -65,9 +69,9 @@ protected Injector getInjector() { @Override protected void configure() { + LOG.info("Binding Configuration class"); bind(Configuration.class).toInstance(configuration); - bind(SessionData.class); bind(Properties.class).asEagerSingleton(); bind(Monitor.class).asEagerSingleton(); @@ -81,7 +85,6 @@ protected void configure() { install(new ServletsModule()); install(new RestModule()); } - }); } @@ -119,26 +122,16 @@ public void contextDestroyed(ServletContextEvent servletContextEvent) { // This manually deregisters JDBC driver, which prevents Tomcat 7 from // complaining about memory leaks into this class -/* while (drivers.hasMoreElements()) { Driver driver = drivers.nextElement(); try { DriverManager.deregisterDriver(driver); - LOG.info("deregistering jdbc driver: {}",driver); + LOG.info("Deregister the jdbc driver: {}",driver); } catch (SQLException sqlE) { - LOG.error("Error deregistering driver %s", driver); + LOG.error("Unable to deregister the jdbc driver {}", driver.toString()); LOG.error("{}", sqlE.getSQLState()); } - - } - - // Shut down the loggers. Assume SLF4J is bound to logback-classic - // in the current environment - LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); - if (loggerContext != null) { - loggerContext.stop(); } -*/ } } diff --git a/src/main/java/com/parallax/server/blocklyprop/converter/ProjectConverter.java b/src/main/java/com/parallax/server/blocklyprop/converter/ProjectConverter.java index ce6fa7d9..727db630 100644 --- a/src/main/java/com/parallax/server/blocklyprop/converter/ProjectConverter.java +++ b/src/main/java/com/parallax/server/blocklyprop/converter/ProjectConverter.java @@ -29,9 +29,7 @@ public class ProjectConverter { - /** - * Application logging facility - */ + // Application logging facility private static final Logger LOG = LoggerFactory.getLogger(ProjectConverter.class); private ProjectDao projectDao; @@ -41,7 +39,6 @@ public class ProjectConverter { // Internal flag to enable/disable parent project details private Boolean includeParentProjectDetails = false; - @Inject public void setProjectDao(ProjectDao projectDao) { this.projectDao = projectDao; @@ -61,48 +58,45 @@ public void setProjectSharingService(ProjectSharingService projectSharingService /** * Convert a ProjectRecord to a JSON object * - * @param project - * @return + * @param project is the ProjectRecord object to convert + * + * @return a JsonObject. The JsonObject may be empty if the source project was null */ public JsonObject toListJson(ProjectRecord project) { + LOG.debug("Converting a ProjectRecord to a Json object"); - - JsonObject result = new JsonObject(); - if (project != null) { - result.addProperty("id", project.getId()); - result.addProperty("name", project.getName()); - result.addProperty("description", project.getDescription()); - result.addProperty("type", project.getType().name()); - result.addProperty("board", project.getBoard()); - result.addProperty("private", project.getPrivate()); - result.addProperty("shared", project.getShared()); - result.addProperty("created", DateConversion.toDateTimeString(project.getCreated().getTime())); - result.addProperty("modified", DateConversion.toDateTimeString(project.getModified().getTime())); - - boolean isYours = project.getIdUser().equals(BlocklyPropSecurityUtils.getCurrentUserId()); - result.addProperty("yours", isYours); + if (project == null) { + return new JsonObject(); + } - // Get user screen name only if it's a registered user - if (project.getId() > 0) { - // Get the project owner's screen name - String screenName = userService.getUserScreenName(project.getIdUser()); - result.addProperty("user",(screenName == "" ? "unknown" : screenName)); + // Load the core project details + JsonObject result = addProjectRecordProperties(project); - // Add the project user's BP user id - result.addProperty("id-user", project.getIdUser()); - } - else { - result.addProperty("user", "anonymous"); - result.addProperty("id-user", 0); - } + boolean isYours = project.getIdUser().equals(BlocklyPropSecurityUtils.getCurrentUserId()); + result.addProperty("yours", isYours); + + // Get user screen name only if it's a registered user + if (project.getId() > 0) { + // Get the project owner's screen name + String screenName = userService.getUserScreenName(project.getIdUser()); + result.addProperty("user",(screenName == "" ? "unknown" : screenName)); + + // Add the project user's BP user id + result.addProperty("id-user", project.getIdUser()); + } + else { + result.addProperty("user", "anonymous"); + result.addProperty("id-user", 0); } + return result; } + + // TODO: Refactor code to eliminate the parent project details. We don't use it - - + // Overrride method to provide option to turn off parent project details public JsonObject toJson(ProjectRecord project, Boolean includeParentDetails) { includeParentProjectDetails = includeParentDetails; @@ -119,38 +113,29 @@ public JsonObject toJson(Project project, Boolean includeParentDetails) { /** * Convert a project record to a JSON payload * - * @param project - * @return + * @param project is te source ProjectRecord object to convert + * + * @return A Json object that contains a set of properties that represent + * the fields contained in the ProjectRecord object */ public JsonObject toJson(ProjectRecord project) { LOG.debug("Converting a ProjectRecord object to JSON"); - JsonObject result = null; - if (project == null) { - LOG.error("Recieved a null project. Unable to convert it to JSON"); - return null; + LOG.error("Received a null project. Unable to convert it to JSON"); + return new JsonObject(); } - + + JsonObject result = null; + try { - result = new JsonObject(); - - result.addProperty("id", project.getId()); - result.addProperty("name", project.getName()); - result.addProperty("description", project.getDescription()); - result.addProperty("description-html", project.getDescriptionHtml()); - result.addProperty("type", project.getType().name()); - result.addProperty("board", project.getBoard()); - result.addProperty("private", project.getPrivate()); - result.addProperty("shared", project.getShared()); - result.addProperty("created", DateConversion.toDateTimeString(project.getCreated().getTime())); - result.addProperty("modified", DateConversion.toDateTimeString(project.getModified().getTime())); - - LOG.debug("Evaluating project owner"); - + // Load the project fields into the Json object + result = addProjectRecordProperties(project); + + // Does current user own this project? + LOG.debug("Evaluating project owner"); boolean isYours = project.getIdUser().equals(BlocklyPropSecurityUtils.getCurrentUserId()); - LOG.debug("isYours = {}", isYours); result.addProperty("yours", isYours); @@ -224,17 +209,10 @@ public JsonObject toJson(ProjectRecord project) { private JsonObject toJson(Project project) { - JsonObject result = new JsonObject(); - result.addProperty("id", project.getId()); - result.addProperty("name", project.getName()); - result.addProperty("description", project.getDescription()); - result.addProperty("description-html", project.getDescriptionHtml()); - result.addProperty("type", project.getType().name()); - result.addProperty("board", project.getBoard()); - result.addProperty("private", project.getPrivate()); - result.addProperty("shared", project.getShared()); - result.addProperty("modified", DateConversion.toDateTimeString(project.getModified().getTime())); + boolean isYours = project.getIdUser().equals(BlocklyPropSecurityUtils.getCurrentUserId()); + JsonObject result = addProperties(project); + result.addProperty("yours", isYours); result.addProperty("user", userService.getUserScreenName(project.getIdUser())); @@ -258,4 +236,67 @@ private JsonObject toJson(Project project) { return result; } + + /** + * Create a Json object representing a Project object. + * + * @param project is the Project object to translate + * + * @return a Json object that contains a collection of properties representing + * the fields in the source Project object + */ + private JsonObject addProperties(Project project) { + + JsonObject result = new JsonObject(); + + try { + result.addProperty("id", project.getId()); + result.addProperty("name", project.getName()); + result.addProperty("description", project.getDescription()); + result.addProperty("description-html", project.getDescriptionHtml()); + result.addProperty("type", project.getType().name()); + result.addProperty("board", project.getBoard()); + result.addProperty("private", project.getPrivate()); + result.addProperty("shared", project.getShared()); + result.addProperty("modified", DateConversion.toDateTimeString(project.getModified().getTime())); + result.addProperty("settings", project.getSettings()); + } + catch (Exception ex) { + LOG.error("Exception {} detected.", ex.toString()); + } + + return result; + } + + + /** + * Create a Json string representing a ProjectRecord object + * + * @param projectRecord is the ProjectRecord object to translate + * + * @return a Json object that contains a collection of properties representing + * the fields in the ProjectRecord object + */ + private JsonObject addProjectRecordProperties( ProjectRecord projectRecord) { + + JsonObject result = new JsonObject(); + + try { + result.addProperty("id", projectRecord.getId()); + result.addProperty("name", projectRecord.getName()); + result.addProperty("description", projectRecord.getDescription()); + result.addProperty("type", projectRecord.getType().name()); + result.addProperty("board", projectRecord.getBoard()); + result.addProperty("private", projectRecord.getPrivate()); + result.addProperty("shared", projectRecord.getShared()); + result.addProperty("created", DateConversion.toDateTimeString(projectRecord.getCreated().getTime())); + result.addProperty("modified", DateConversion.toDateTimeString(projectRecord.getModified().getTime())); + result.addProperty("settings", projectRecord.getSettings()); + } + catch (Exception ex) { + LOG.error("Exception {} detected.", ex.toString()); + } + + return result; + } } diff --git a/src/main/java/com/parallax/server/blocklyprop/db/dao/ProjectDao.java b/src/main/java/com/parallax/server/blocklyprop/db/dao/ProjectDao.java index ee4be6e2..383c9f2b 100644 --- a/src/main/java/com/parallax/server/blocklyprop/db/dao/ProjectDao.java +++ b/src/main/java/com/parallax/server/blocklyprop/db/dao/ProjectDao.java @@ -13,108 +13,133 @@ /** * Interface for the blocklyprop.project table - * + * * @author Michel - * + * * Fields: * id: Unique record number within the table. - * + * * id_user: Link to primary key in the blocklyprop.user table. - * + * * id_clouduser: Link to the primary key in the cloudsession.user table. - * + * * name: Project name - * + * * description: Project description formatted in plain text - * + * * description_html:Project description formatted in HTML - * + * * code: XML content that hold the project block structure. - * - * type: Project source language (SPIN or PROPC) - * + * + * type: Project source language (SPIN or PROPC) + * * board: Descriptor for the target device the project will use. - * + * * private: Flag to indicate if the project is visible to anyone but * the project owner.This flag is mutually exclusive with the * 'shared' flag. - * + * * shared: Flag to indicate if the project is available for viewing by * anyone.This flag is mutually exclusive with the 'private' * flag. - * + * * created: Timestamp indicating when the project record was created. - * + * * modified: Timestamp indicating when the project record was last changed. - * + * * based_on: The id from the project that is the parent of the current * project record. - * + * */ public interface ProjectDao { ProjectRecord getProject(Long idProject); + // V2 implementation ProjectRecord createProject( - String name, - String description, - String descriptionHtml, - String code, - ProjectType type, - String board, - boolean privateProject, + String name, + String description, + String descriptionHtml, + String code, + ProjectType type, + String board, + boolean privateProject, + boolean sharedProject, + Long idProjectBasedOn, + String settings); + + @Deprecated + ProjectRecord createProject( + String name, + String description, + String descriptionHtml, + String code, + ProjectType type, + String board, + boolean privateProject, boolean sharedProject, Long idProjectBasedOn); ProjectRecord createProject( - String name, - String description, - String descriptionHtml, - ProjectType type, - String board, - boolean privateProject, + String name, + String description, + String descriptionHtml, + ProjectType type, + String board, + boolean privateProject, boolean sharedProject); ProjectRecord updateProject( - Long idProject, - String name, - String description, - String descriptionHtml, - boolean privateProject, + Long idProject, + String name, + String description, + String descriptionHtml, + boolean privateProject, boolean sharedProject); ProjectRecord updateProject( - Long idProject, - String name, - String description, - String descriptionHtml, - String code, - boolean privateProject, + Long idProject, + String name, + String description, + String descriptionHtml, + String code, + boolean privateProject, boolean sharedProject); + ProjectRecord updateProject( + Long idProject, + String name, + String description, + String descriptionHtml, + String code, + boolean privateProject, + boolean sharedProject, + String settings); + + ProjectRecord saveCode( - Long idProject, + Long idProject, String code); List getUserProjects( - Long idUser, - TableSort sort, - TableOrder order, - Integer limit, + Long idUser, + TableSort sort, + TableOrder order, + Integer limit, Integer offset); // Return a list of community projects List getSharedProjects( - TableSort sort, - TableOrder order, - Integer limit, + TableSort sort, + TableOrder order, + Integer limit, Integer offset); List getSharedProjectsByUser( - TableSort sort, - TableOrder order, - Integer limit, - Integer offset, + TableSort sort, + TableOrder order, + Integer limit, + Integer offset, Long idUser); int countUserProjects(Long idUser); @@ -128,13 +153,13 @@ List getSharedProjectsByUser( boolean deleteProject(Long idProject); ProjectRecord updateProjectCode( - Long idProject, + Long idProject, String code); ProjectRecord saveProjectCodeAs( - Long idProject, - String code, + Long idProject, + String code, String newName, String newBoard); -} +} \ No newline at end of file diff --git a/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/ProjectDaoImpl.java b/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/ProjectDaoImpl.java index 93c7308a..ce20502b 100644 --- a/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/ProjectDaoImpl.java +++ b/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/ProjectDaoImpl.java @@ -1,7 +1,22 @@ /* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. + * Copyright (c) 2019 Parallax Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the “Software”), to deal in the Software without + * restriction, including without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. */ package com.parallax.server.blocklyprop.db.dao.impl; @@ -32,7 +47,7 @@ /** * DAO interface to the blocklyprop.project table. - * + * * @author Michel * */ @@ -45,32 +60,32 @@ public class ProjectDaoImpl implements ProjectDao { */ private static final int Min_BlocklyCodeSize = 48; - + /** * Application logging facility */ private static final Logger LOG = LoggerFactory.getLogger(ProjectDao.class); - + /** * Database connection */ private DSLContext create; - + // Used by the randomString function static final String AB = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#()*-+./:;=@[]^_`{|}~"; static Random rnd = new Random(); - + // Constants to clarify the edit flag in method calls static final boolean EDIT_MODE_OFF = false; - static final boolean EDIT_MODE_ON = true; - + private static final boolean EDIT_MODE_ON = true; + // Constant to identify the current version of the blockly block library; public static final short BLOCKLY_LIBRARY_VERSION = 1; - - + + @Inject public void setDSLContext(DSLContext dsl) { this.create = dsl; @@ -98,12 +113,12 @@ public void setProjectSharingContext(ProjectSharingService projectSharingService * * @param idProject The id for the project that will be the source for the * new project record - * + * * @return ProjectRecord object containing a copy of the original project */ @Override public ProjectRecord getProject(Long idProject) { - LOG.info("Retreiving data for project #{}", idProject); + LOG.info("Retrieving data for project #{}", idProject); ProjectRecord record = null; @@ -117,21 +132,114 @@ record = create LOG.warn("Unable to retreive project {}", idProject); return null; } - + } catch (org.jooq.exception.DataAccessException sqex) { LOG.error("Database error encountered {}", sqex.getMessage()); return null; } catch (Exception ex) { - LOG.error("Unexpected exception retreiving a project record"); + LOG.error("Unexpected exception retrieving a project record"); LOG.error("Error Message: {}", ex.getMessage()); return null; } - // Return the project after checking if for depricated blocks - return alterReadRecord(record); + // Return the project after checking if for deprecated blocks + LOG.info("Cleaning project {} blocks", record.getId()); + + ProjectRecord prjRecord = alterReadRecord(record); + + if (prjRecord == null) { + LOG.info("Hmm, Something broke the project record"); + return null; + } + + LOG.info("Returning project {}.", prjRecord.getId()); + + return prjRecord; + } + + + + // V2 implementations + @Override + public ProjectRecord createProject( + String name, + String description, + String descriptionHtml, + String code, + ProjectType type, + String board, + boolean privateProject, + boolean sharedProject, + Long idProjectBasedOn, + String settings) { + + LOG.info("Creating a new project with existing code."); + + ProjectRecord record = null; + + // Get the logged in user's userID + Long idUser = BlocklyPropSecurityUtils.getCurrentUserId(); + if (idUser == null) { + LOG.error("Null BP UserID"); + return null; + } + + // Get the cloud session user id from the current authenticated session + Long idCloudUser = BlocklyPropSecurityUtils.getCurrentSessionUserId(); + if (idCloudUser == null) { + LOG.error("Null cloud user ID"); + return null; + } + + try { + record = create + .insertInto(Tables.PROJECT, + Tables.PROJECT.ID_USER, + Tables.PROJECT.ID_CLOUDUSER, + Tables.PROJECT.NAME, + Tables.PROJECT.DESCRIPTION, + Tables.PROJECT.DESCRIPTION_HTML, + Tables.PROJECT.CODE, + Tables.PROJECT.CODE_BLOCK_VERSION, + Tables.PROJECT.TYPE, + Tables.PROJECT.BOARD, + Tables.PROJECT.PRIVATE, + Tables.PROJECT.SHARED, + Tables.PROJECT.BASED_ON, + Tables.PROJECT.SETTINGS) + .values(idUser, + idCloudUser, + name, + description, + descriptionHtml, + code, + BLOCKLY_LIBRARY_VERSION, + type, + board, + privateProject, + sharedProject, + idProjectBasedOn, + settings) + .returning() + .fetchOne(); + } + catch (org.jooq.exception.DataAccessException sqex) { + LOG.error("Database error encountered {}", sqex.getMessage()); + return null; + } catch (Exception ex) { + LOG.error("Unexpected exception creating a project record"); + LOG.error("Error Message: {}", ex.getMessage()); + return null; + } + + return record; + } - + + + + /** * * Create a new project with supplied code. @@ -146,23 +254,24 @@ record = create * @param sharedProject Flag to indicate if the project is a community project * @param idProjectBasedOn Parent project id if the new project is cloned * from another project - * + * * @return a fully formed ProjectRecord or a null if an error is detected. */ + @Deprecated @Override - public ProjectRecord createProject( + public ProjectRecord createProject( String name, String description, String descriptionHtml, String code, - ProjectType type, - String board, + ProjectType type, + String board, boolean privateProject, boolean sharedProject, Long idProjectBasedOn) { LOG.info("Creating a new project with existing code."); - + ProjectRecord record = null; // Get the logged in user's userID @@ -171,43 +280,43 @@ public ProjectRecord createProject( LOG.error("Null BP UserID"); return null; } - + // Get the cloud session user id from the current authenticated session Long idCloudUser = BlocklyPropSecurityUtils.getCurrentSessionUserId(); if (idCloudUser == null) { LOG.error("Null cloud user ID"); return null; } - + try { record = create - .insertInto(Tables.PROJECT, - Tables.PROJECT.ID_USER, - Tables.PROJECT.ID_CLOUDUSER, - Tables.PROJECT.NAME, - Tables.PROJECT.DESCRIPTION, - Tables.PROJECT.DESCRIPTION_HTML, - Tables.PROJECT.CODE, - Tables.PROJECT.CODE_BLOCK_VERSION, - Tables.PROJECT.TYPE, - Tables.PROJECT.BOARD, - Tables.PROJECT.PRIVATE, - Tables.PROJECT.SHARED, - Tables.PROJECT.BASED_ON) - .values(idUser, - idCloudUser, - name, - description, - descriptionHtml, - code, - BLOCKLY_LIBRARY_VERSION, - type, - board, - privateProject, - sharedProject, - idProjectBasedOn) - .returning() - .fetchOne(); + .insertInto(Tables.PROJECT, + Tables.PROJECT.ID_USER, + Tables.PROJECT.ID_CLOUDUSER, + Tables.PROJECT.NAME, + Tables.PROJECT.DESCRIPTION, + Tables.PROJECT.DESCRIPTION_HTML, + Tables.PROJECT.CODE, + Tables.PROJECT.CODE_BLOCK_VERSION, + Tables.PROJECT.TYPE, + Tables.PROJECT.BOARD, + Tables.PROJECT.PRIVATE, + Tables.PROJECT.SHARED, + Tables.PROJECT.BASED_ON) + .values(idUser, + idCloudUser, + name, + description, + descriptionHtml, + code, + BLOCKLY_LIBRARY_VERSION, + type, + board, + privateProject, + sharedProject, + idProjectBasedOn) + .returning() + .fetchOne(); } catch (org.jooq.exception.DataAccessException sqex) { LOG.error("Database error encountered {}", sqex.getMessage()); @@ -217,7 +326,7 @@ record = create LOG.error("Error Message: {}", ex.getMessage()); return null; } - + return record; } @@ -248,16 +357,16 @@ public ProjectRecord createProject( boolean sharedProject) { LOG.info("Creating a new, empty project from existing project."); - + // TODO: Add based_on field to end of argument list - return createProject( - name, - description, - descriptionHtml, - "", - type, - board, - privateProject, + return createProject( + name, + description, + descriptionHtml, + "", + type, + board, + privateProject, sharedProject, null); } @@ -353,6 +462,45 @@ public ProjectRecord updateProject( return null; } + + + + @Override + public ProjectRecord updateProject( + Long idProject, + String name, + String description, + String descriptionHtml, + String code, + boolean privateProject, + boolean sharedProject, + String settings) { + + LOG.info("Update project {}.", idProject); + + ProjectRecord record = getProject(idProject, EDIT_MODE_ON); + if (record != null) { + record.setName(name); + record.setDescription(description); + record.setDescriptionHtml(descriptionHtml); + record.setCode(code); + record.setPrivate(privateProject); + record.setShared(sharedProject); + record.setSettings(settings); + record.setModified(getCurrentTimestamp()); + record.setCodeBlockVersion(BLOCKLY_LIBRARY_VERSION); + record.update(); + + return record; + } + + LOG.warn("Unable to update project {}", idProject); + return null; + } + + + + /** * Update the code blocks for a project * @@ -370,18 +518,18 @@ public ProjectRecord saveCode(Long idProject, String code) { record.setCode(code); record.setCodeBlockVersion(BLOCKLY_LIBRARY_VERSION); record.setModified(getCurrentTimestamp()); - + // Update the record ProjectRecord returningRecord = create .update(Tables.PROJECT) .set(record) .returning() .fetchOne(); - + // Return a copy of the updated project record return returningRecord; } - + LOG.error("Unable to save code for project {}", idProject); return null; } @@ -398,22 +546,29 @@ public ProjectRecord saveCode(Long idProject, String code) { */ @Override public List getUserProjects( - Long idUser, - TableSort sort, - TableOrder order, - Integer limit, + Long idUser, + TableSort sort, + TableOrder order, + Integer limit, Integer offset) { - - LOG.info("Retreive projects for user {}.", idUser); - SortField orderField = Tables.PROJECT.NAME.asc(); + LOG.info("Retrieve projects for user {}.", idUser); + + SortField orderField; + + // Set sort order of the result if (TableOrder.desc == order) { orderField = Tables.PROJECT.NAME.desc(); + } else { + orderField = Tables.PROJECT.NAME.asc(); } - return create.selectFrom(Tables.PROJECT) + return create + .selectFrom(Tables.PROJECT) .where(Tables.PROJECT.ID_USER.equal(idUser)) - .orderBy(orderField).limit(limit).offset(offset) + .orderBy(orderField) + .limit(limit) + .offset(offset) .fetch(); } @@ -424,19 +579,18 @@ public List getUserProjects( * @param order * @param limit * @param offset - * @return * Returns a list of ProjectRecord objects corresponding to the projects * matching the selection creiteria */ @Override public List getSharedProjects( - TableSort sort, - TableOrder order, - Integer limit, + TableSort sort, + TableOrder order, + Integer limit, Integer offset) { - - LOG.info("Retreive shared projects."); + + LOG.info("List shared projects."); SortField orderField = sort == null ? Tables.PROJECT.NAME.asc() : sort.getField().asc(); if (TableOrder.desc == order) { @@ -445,7 +599,7 @@ public List getSharedProjects( // Search for community projects Condition conditions = Tables.PROJECT.SHARED.eq(Boolean.TRUE); - + return create.selectFrom(Tables.PROJECT) .where(conditions) .orderBy(orderField).limit(limit).offset(offset) @@ -464,12 +618,12 @@ public List getSharedProjects( */ @Override public List getSharedProjectsByUser( - TableSort sort, - TableOrder order, - Integer limit, - Integer offset, + TableSort sort, + TableOrder order, + Integer limit, + Integer offset, Long idUser) { - + LOG.info("Retreive shared projects."); SortField orderField = sort == null ? Tables.PROJECT.NAME.asc() : sort.getField().asc(); @@ -494,9 +648,11 @@ public List getSharedProjectsByUser( */ @Override public int countUserProjects(Long idUser) { - LOG.info("Count project for user {}.", idUser); - return create.fetchCount(Tables.PROJECT, Tables.PROJECT.ID_USER.equal(idUser)); + int rows = create.fetchCount(Tables.PROJECT, Tables.PROJECT.ID_USER.equal(idUser)); + LOG.info("Found a total of {} projects for user: {}.",rows, idUser); + + return rows; } /** @@ -511,7 +667,7 @@ public int countSharedProjects(Long idUser) { LOG.info("Count shared projects for user {}.", idUser); Condition conditions = Tables.PROJECT.SHARED.equal(Boolean.TRUE); - + // Shared projects are really community projects. We should not include // the logged in user's projects in the community listing. There is a // separate listing available for the logged in user's private projects. @@ -552,10 +708,13 @@ public ProjectRecord cloneProject(Long idProject) { LOG.info("Clone existing project {} to a new project.", idProject); ProjectRecord original = getProject(idProject); + if (original == null) { throw new NullPointerException("Project doesn't exist"); } + Long idUser = BlocklyPropSecurityUtils.getCurrentUserId(); + if (original.getIdUser().equals(idUser) || original.getShared()) { // TODO check if friends return doProjectClone(original); } @@ -566,7 +725,7 @@ public ProjectRecord cloneProject(Long idProject) { * TODO: add details. * * @param idProject - * @return + * @return boolean true if record was deleted, otherwise false. */ @Override public boolean deleteProject(Long idProject) { @@ -661,7 +820,7 @@ public ProjectRecord updateProjectCode(Long idProject, String code) { */ @Override public ProjectRecord saveProjectCodeAs(Long idProject, String code, String newName, String newBoard) { - + LOG.info("Saving project code from project {} as '{}'", idProject, newName); // Retreive the source project @@ -677,10 +836,10 @@ public ProjectRecord saveProjectCodeAs(Long idProject, String code, String newNa newBoard = original.getBoard(); } - - // Obtain the current bp user record. + + // Obtain the current bp user record. Long idUser = BlocklyPropSecurityUtils.getCurrentUserId(); - + if (idUser != null) { // Create a copy of the source project is the source project is owned // by the current user OR if the source project is designated as a @@ -689,12 +848,12 @@ public ProjectRecord saveProjectCodeAs(Long idProject, String code, String newNa boolean sharedStatus = projectSharingService.isProjectShared(idProject); LOG.info("Project shared status: {}", sharedStatus); - if (original.getIdUser().equals(idUser) || // Project is owned by currently logged in user - sharedStatus || // Project is shared - (!original.getPrivate())) { // Project is public + if (original.getIdUser().equals(idUser) || // Project is owned by currently logged in user + sharedStatus || // Project is shared + (!original.getPrivate())) { // Project is public ProjectRecord cloned = createProject( - newName, + newName, original.getDescription(), original.getDescriptionHtml(), code, @@ -720,7 +879,7 @@ public ProjectRecord saveProjectCodeAs(Long idProject, String code, String newNa // Private over-ride of the public getProject() // - // + // private ProjectRecord getProject(Long idProject, boolean toEdit) { LOG.info("Retreiving project {}.", idProject); ProjectRecord record = create @@ -751,7 +910,7 @@ private ProjectRecord getProject(Long idProject, boolean toEdit) { } private ProjectRecord doProjectClone(ProjectRecord original) { - + // TODO: Add based_on parameter as last argument ProjectRecord cloned = createProject( original.getName(), @@ -762,34 +921,35 @@ private ProjectRecord doProjectClone(ProjectRecord original) { original.getBoard(), original.getPrivate(), original.getShared(), - original.getId() // set the parent project id + original.getId(), // set the parent project id + original.getSettings() ); // cloned.setBasedOn(original.getId()); // cloned.update(); - // WHAT IS THIS DOING? + // TODO: URGENT - Evaluate query to verify that it is updating only one record create.update(Tables.PROJECT) .set(Tables.PROJECT.BASED_ON, original.getId()) .where(Tables.PROJECT.ID.equal(cloned.getId())); - + return cloned; } - + // Produce a current timestamp private GregorianCalendar getCurrentTimestamp() { GregorianCalendar cal = new GregorianCalendar(); cal.setTime(new java.util.Date()); - + return cal; } - - - - + + + + // Evaluate project code and replace any deprecated or updated blocks // // Return a ProjectRecord object. The code field may be altered to correct @@ -798,7 +958,7 @@ private GregorianCalendar getCurrentTimestamp() { // horribly wrong with the string conversions. // private ProjectRecord alterReadRecord(ProjectRecord record) { - + String currentCode, newCode; @@ -812,11 +972,11 @@ private ProjectRecord alterReadRecord(ProjectRecord record) { LOG.info("Bypassing project block evaluation"); return record; } - + LOG.info("Verify project {} block version {} characteristics", record.getId(), record.getCodeBlockVersion()); - + currentCode = record.getCode(); // Return immediately if there is no code to adjust @@ -824,7 +984,7 @@ private ProjectRecord alterReadRecord(ProjectRecord record) { LOG.warn("Project () code block is empty.", record.getId()); return record; } - + if (currentCode.length() < Min_BlocklyCodeSize ) { LOG.warn("Project code appears to be empty. Code size:{}",currentCode.length()); return record; @@ -849,7 +1009,7 @@ private ProjectRecord alterReadRecord(ProjectRecord record) { return record; } - /** + /** * * Create a random string to use as a blockID. * @@ -891,20 +1051,20 @@ private String fixSpinProjectBlocks(String newCode) { newCode = newCode.replaceAll( "block type=\"math_number\"", "block type=\"spin_integer\""); - + return newCode; } /** * Find and replace deprecated project code blocks - * + * * @param originalCode is the project code that will be evaluated. * @param projType - * @return + * @return */ private String fixPropcProjectBlocks(String originalCode, ProjectType projType) { LOG.info("Looking for depricated PropC blocks."); - + // Copy the original project code into a working variable String newCode = originalCode; @@ -991,32 +1151,32 @@ private String fixPropcProjectBlocks(String originalCode, ProjectType projType) newCode = newCode.replaceAll( "block type=\"logic_boolean_negate\"", "block type=\"logic_negate\""); - + newCode = newCode.replaceAll( "_000 / ", "000 / "); // Fix a small issue with calling the wrong project type. newCode = newCode.replaceAll( "block type=\"spin_integer\"", "block type=\"math_number\""); - + if (!newCode.contains("block type=\"math_number\"") && projType == ProjectType.SPIN) { // Change all math number blocks to the same kind newCode = newCode.replaceAll( "block type=\"math_int_angle\"", "block type=\"math_number\""); - + newCode = newCode.replaceAll( "block type=\"math_integer\"", "block type=\"math_number\""); - + newCode = newCode.replaceAll( "block type=\"scribbler_random_number\"", "block type=\"math_random\""); - + newCode = newCode.replaceAll( "field name=\"INT_VALUE\"", "field name=\"NUM\""); - + newCode = newCode.replaceAll( "field name=\"ANGLE_VALUE\"", "field name=\"NUM\""); @@ -1024,70 +1184,70 @@ private String fixPropcProjectBlocks(String originalCode, ProjectType projType) newCode = newCode.replaceAll( "block type=\"digital_input\"", "block type=\"check_pin\""); - + newCode = newCode.replaceAll( "block type=\"digital_output\"", "block type=\"make_pin\""); - + newCode = newCode.replaceAll( "block type=\"scribbler_servo\"", "block type=\"servo_move\""); - + newCode = newCode.replaceAll( "field name=\"SERVO_PIN\"", "field name=\"PIN\""); - + newCode = newCode.replaceAll( "field name=\"SERVO_ANGLE\"", "field name=\"ANGLE\""); - + newCode = newCode.replaceAll( "1000<", "field name=\"TIMESCALE\">Z1<"); - + newCode = newCode.replaceAll( "field name=\"TIMESCALE\">1<", "field name=\"TIMESCALE\">Z1000<"); - + newCode = newCode.replaceAll( "field name=\"TIMESCALE\">10<", "field name=\"TIMESCALE\">100<"); - + newCode = newCode.replaceAll( "field name=\"TIMESCALE\">Z", "field name=\"TIMESCALE\">"); - + newCode = newCode.replaceAll("Scribbler#CS","256"); newCode = newCode.replaceAll("Scribbler#NL","10"); newCode = newCode.replaceAll("Scribbler#LF","13"); newCode = newCode.replaceAll("Scribbler#BS","127"); - + newCode = newCode.replaceAll( "block type=\"scribbler_loop\"", "block type=\"controls_repeat\""); - + newCode = newCode.replaceAll( "statement name=\"LOOP\"", "statement name=\"DO\""); - + newCode = newCode.replaceAll( "(.*)", - "TIMES$2"); + "TIMES$2"); } - + // Replace the Robot init block with two blocks, need to generate unique 20-digit blockID: if (!newCode.contains("block type=\"ab_drive_ramping\"")) { newCode = newCode.replaceAll( - "", + "", ""); } - + return newCode; } -} +} \ No newline at end of file diff --git a/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/SessionDaoImpl.java b/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/SessionDaoImpl.java index edab7453..968a2bf9 100644 --- a/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/SessionDaoImpl.java +++ b/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/SessionDaoImpl.java @@ -40,6 +40,7 @@ /** + * This is the session database layer. * * @author Michel */ @@ -48,37 +49,42 @@ public class SessionDaoImpl implements SessionDao { // Get a logger instance private static final Logger LOG = LoggerFactory.getLogger(SessionDaoImpl.class); - /** - * - */ + // Database connection private DSLContext create; - /** - * An instance of the application configuration settings - */ + //An instance of the application configuration settings private Configuration configuration; + + /** + * Get a copy of the database context + * + * @param dsl is the context provided by JooQ + */ @Inject public void setDSLContext(DSLContext dsl) { this.create = dsl; } + /** - * - * @param configuration + * Get a copy of the application configuration + * + * @param configuration is the context provided by the configuration manager */ @Inject public void setConfiguration(Configuration configuration) { this.configuration = configuration; } + /** - * - * @param session + * Create a new session + * + * @param session is a initialized SessrionRecord object */ @Override public void create(SessionRecord session) { - LOG.debug("Create a session. Timeout set to: {}", session.getTimeout()); // Log session details if the configuration file permits it @@ -108,49 +114,53 @@ public void create(SessionRecord session) { } /** - * - * @param idSession - * @return - * @throws NullPointerException + * Retrieve a session + * + * @param idSession is the unique identifier for the requested session + * + * @return a SessionRecord, otherwise throws a NullPointerException + * + * @throws NullPointerException raise an NPE if the session is not found, + * otherwise the caller expects to receive a valid session record */ @Override public SessionRecord readSession(String idSession) throws NullPointerException { - LOG.debug("Getting session {} details", idSession); - SessionRecord sessionRecord = null; - try { - sessionRecord = create.selectFrom(Tables.SESSION) + SessionRecord sessionRecord = create + .selectFrom(Tables.SESSION) .where(Tables.SESSION.IDSESSION.eq(idSession)) .fetchOne(); // Log session details if the configuration file permits it printSessionInfo("read", sessionRecord); + return sessionRecord; } catch (org.jooq.exception.DataAccessException sqex) { LOG.error("Database exception {}", sqex.getMessage()); + throw new NullPointerException("Session not found"); } - - return sessionRecord; } /** - * - * @param session - * @throws NullPointerException + * Update an existing session + * + * @param session is the unique identifier for the requested session + * + * @throws NullPointerException raise an NPE if the session yo be updated + * is not found */ @Override public void updateSession(SessionRecord session) throws NullPointerException { - - LOG.debug("Update a session"); + LOG.debug("Update session {}", session.getIdsession()); try { // Get the current session record SessionRecord dbRecord = readSession(session.getIdsession()); if (dbRecord == null) { - throw new NullPointerException("Session not found"); + throw new NullPointerException("Invalid session"); } dbRecord.setStarttimestamp(session.getStarttimestamp()); @@ -167,17 +177,18 @@ public void updateSession(SessionRecord session) throws NullPointerException { } catch (org.jooq.exception.DataAccessException sqex) { LOG.error("Database exception {}", sqex.getMessage()); - throw new NullPointerException("Database error"); + throw new NullPointerException("Session not found"); } } + /** - * - * @param idSession + * Remove a session record from the backing store + * + * @param idSession is the unique identifier for the requested session */ @Override public void deleteSession(String idSession) { - LOG.info("Deleting session {}", idSession); create.deleteFrom(Tables.SESSION) @@ -185,16 +196,25 @@ public void deleteSession(String idSession) { .execute(); } + /** - * - * @return + * Get a list of all active (unexpired) sessions + * + * @return a collection of session records */ @Override public Collection getActiveSessions() { return Arrays.asList(create.selectFrom(Tables.SESSION).fetchArray()); } - + + /** + * Provide detailed logging for the specified session + * + * @param action is a message that is embedded in the log record + * + * @param session is the session record to to log. + */ private void printSessionInfo(String action, SessionRecord session) { if (configuration.getBoolean("debug.session", false)) { try { @@ -206,9 +226,11 @@ private void printSessionInfo(String action, SessionRecord session) { HashMap attributes = (HashMap) in.readObject(); LOG.info("Session info: {}:{}", action, attributes); } - } catch (IOException ex) { + } + catch (IOException ex) { LOG.error("I/O error. {}",ex.getMessage()); - } catch (ClassNotFoundException cnfe) { + } + catch (ClassNotFoundException cnfe) { LOG.error("Class not found. {}", cnfe.getMessage()); } } diff --git a/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/UserDaoImpl.java b/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/UserDaoImpl.java index be056238..f3e7702d 100644 --- a/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/UserDaoImpl.java +++ b/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/UserDaoImpl.java @@ -1,8 +1,24 @@ /* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. + * Copyright (c) 2019 Parallax Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the “Software”), to deal in the Software without + * restriction, including without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. */ + package com.parallax.server.blocklyprop.db.dao.impl; import com.google.inject.Inject; @@ -24,6 +40,7 @@ import org.slf4j.LoggerFactory; /** + * Blockly user * * @author Michel */ diff --git a/src/main/java/com/parallax/server/blocklyprop/db/generated/tables/Motd.java b/src/main/java/com/parallax/server/blocklyprop/db/generated/tables/Motd.java index b7b64129..d64066da 100644 --- a/src/main/java/com/parallax/server/blocklyprop/db/generated/tables/Motd.java +++ b/src/main/java/com/parallax/server/blocklyprop/db/generated/tables/Motd.java @@ -160,4 +160,4 @@ public Motd as(String alias) { public Motd rename(String name) { return new Motd(name, null); } -} \ No newline at end of file +} diff --git a/src/main/java/com/parallax/server/blocklyprop/db/generated/tables/Project.java b/src/main/java/com/parallax/server/blocklyprop/db/generated/tables/Project.java index 8c201861..da28e09f 100644 --- a/src/main/java/com/parallax/server/blocklyprop/db/generated/tables/Project.java +++ b/src/main/java/com/parallax/server/blocklyprop/db/generated/tables/Project.java @@ -38,7 +38,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes" }) public class Project extends TableImpl { - private static final long serialVersionUID = -1048994704; + private static final long serialVersionUID = -1976592299; /** * The reference instance of blocklyprop.project @@ -128,6 +128,11 @@ public Class getRecordType() { */ public final TableField BASED_ON = createField("based_on", org.jooq.impl.SQLDataType.BIGINT, this, ""); + /** + * The column blocklyprop.project.settings. + */ + public final TableField SETTINGS = createField("settings", org.jooq.impl.SQLDataType.CLOB, this, ""); + /** * Create a blocklyprop.project table reference */ diff --git a/src/main/java/com/parallax/server/blocklyprop/db/generated/tables/pojos/Motd.java b/src/main/java/com/parallax/server/blocklyprop/db/generated/tables/pojos/Motd.java index 1ef5b542..65d052bc 100644 --- a/src/main/java/com/parallax/server/blocklyprop/db/generated/tables/pojos/Motd.java +++ b/src/main/java/com/parallax/server/blocklyprop/db/generated/tables/pojos/Motd.java @@ -173,4 +173,4 @@ public String toString() { sb.append(")"); return sb.toString(); } -} \ No newline at end of file +} diff --git a/src/main/java/com/parallax/server/blocklyprop/db/generated/tables/pojos/Project.java b/src/main/java/com/parallax/server/blocklyprop/db/generated/tables/pojos/Project.java index 4cded530..b84c7b66 100644 --- a/src/main/java/com/parallax/server/blocklyprop/db/generated/tables/pojos/Project.java +++ b/src/main/java/com/parallax/server/blocklyprop/db/generated/tables/pojos/Project.java @@ -25,7 +25,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes" }) public class Project implements Serializable { - private static final long serialVersionUID = -1074694014; + private static final long serialVersionUID = 1473532278; private Long id; private Long idUser; @@ -42,6 +42,7 @@ public class Project implements Serializable { private GregorianCalendar created; private GregorianCalendar modified; private Long basedOn; + private String settings; public Project() {} @@ -61,6 +62,7 @@ public Project(Project value) { this.created = value.created; this.modified = value.modified; this.basedOn = value.basedOn; + this.settings = value.settings; } public Project( @@ -78,7 +80,8 @@ public Project( Boolean shared, GregorianCalendar created, GregorianCalendar modified, - Long basedOn + Long basedOn, + String settings ) { this.id = id; this.idUser = idUser; @@ -95,6 +98,7 @@ public Project( this.created = created; this.modified = modified; this.basedOn = basedOn; + this.settings = settings; } public Long getId() { @@ -217,6 +221,14 @@ public void setBasedOn(Long basedOn) { this.basedOn = basedOn; } + public String getSettings() { + return this.settings; + } + + public void setSettings(String settings) { + this.settings = settings; + } + @Override public String toString() { StringBuilder sb = new StringBuilder("Project ("); @@ -236,6 +248,7 @@ public String toString() { sb.append(", ").append(created); sb.append(", ").append(modified); sb.append(", ").append(basedOn); + sb.append(", ").append(settings); sb.append(")"); return sb.toString(); diff --git a/src/main/java/com/parallax/server/blocklyprop/db/generated/tables/records/MotdRecord.java b/src/main/java/com/parallax/server/blocklyprop/db/generated/tables/records/MotdRecord.java index b6e83213..00d16202 100644 --- a/src/main/java/com/parallax/server/blocklyprop/db/generated/tables/records/MotdRecord.java +++ b/src/main/java/com/parallax/server/blocklyprop/db/generated/tables/records/MotdRecord.java @@ -500,4 +500,4 @@ public MotdRecord(Long id, String messageText, String messageHtml, String notes, setValue(8, messageEnableTime); setValue(9, messageDisableTime); } -} \ No newline at end of file +} diff --git a/src/main/java/com/parallax/server/blocklyprop/db/generated/tables/records/ProjectRecord.java b/src/main/java/com/parallax/server/blocklyprop/db/generated/tables/records/ProjectRecord.java index ce0e1357..1fc51c93 100644 --- a/src/main/java/com/parallax/server/blocklyprop/db/generated/tables/records/ProjectRecord.java +++ b/src/main/java/com/parallax/server/blocklyprop/db/generated/tables/records/ProjectRecord.java @@ -13,8 +13,8 @@ import org.jooq.Field; import org.jooq.Record1; -import org.jooq.Record15; -import org.jooq.Row15; +import org.jooq.Record16; +import org.jooq.Row16; import org.jooq.impl.UpdatableRecordImpl; @@ -29,9 +29,9 @@ comments = "This class is generated by jOOQ" ) @SuppressWarnings({ "all", "unchecked", "rawtypes" }) -public class ProjectRecord extends UpdatableRecordImpl implements Record15 { +public class ProjectRecord extends UpdatableRecordImpl implements Record16 { - private static final long serialVersionUID = 902114783; + private static final long serialVersionUID = 83268425; /** * Setter for blocklyprop.project.id. @@ -243,6 +243,20 @@ public Long getBasedOn() { return (Long) getValue(14); } + /** + * Setter for blocklyprop.project.settings. + */ + public void setSettings(String value) { + setValue(15, value); + } + + /** + * Getter for blocklyprop.project.settings. + */ + public String getSettings() { + return (String) getValue(15); + } + // ------------------------------------------------------------------------- // Primary key information // ------------------------------------------------------------------------- @@ -256,23 +270,23 @@ public Record1 key() { } // ------------------------------------------------------------------------- - // Record15 type implementation + // Record16 type implementation // ------------------------------------------------------------------------- /** * {@inheritDoc} */ @Override - public Row15 fieldsRow() { - return (Row15) super.fieldsRow(); + public Row16 fieldsRow() { + return (Row16) super.fieldsRow(); } /** * {@inheritDoc} */ @Override - public Row15 valuesRow() { - return (Row15) super.valuesRow(); + public Row16 valuesRow() { + return (Row16) super.valuesRow(); } /** @@ -395,6 +409,14 @@ public Field field15() { return Project.PROJECT.BASED_ON; } + /** + * {@inheritDoc} + */ + @Override + public Field field16() { + return Project.PROJECT.SETTINGS; + } + /** * {@inheritDoc} */ @@ -515,6 +537,14 @@ public Long value15() { return getBasedOn(); } + /** + * {@inheritDoc} + */ + @Override + public String value16() { + return getSettings(); + } + /** * {@inheritDoc} */ @@ -654,7 +684,16 @@ public ProjectRecord value15(Long value) { * {@inheritDoc} */ @Override - public ProjectRecord values(Long value1, Long value2, Long value3, String value4, String value5, String value6, String value7, Short value8, ProjectType value9, String value10, Boolean value11, Boolean value12, GregorianCalendar value13, GregorianCalendar value14, Long value15) { + public ProjectRecord value16(String value) { + setSettings(value); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public ProjectRecord values(Long value1, Long value2, Long value3, String value4, String value5, String value6, String value7, Short value8, ProjectType value9, String value10, Boolean value11, Boolean value12, GregorianCalendar value13, GregorianCalendar value14, Long value15, String value16) { value1(value1); value2(value2); value3(value3); @@ -670,6 +709,7 @@ public ProjectRecord values(Long value1, Long value2, Long value3, String value4 value13(value13); value14(value14); value15(value15); + value16(value16); return this; } @@ -687,7 +727,7 @@ public ProjectRecord() { /** * Create a detached, initialised ProjectRecord */ - public ProjectRecord(Long id, Long idUser, Long idClouduser, String name, String description, String descriptionHtml, String code, Short codeBlockVersion, ProjectType type, String board, Boolean private_, Boolean shared, GregorianCalendar created, GregorianCalendar modified, Long basedOn) { + public ProjectRecord(Long id, Long idUser, Long idClouduser, String name, String description, String descriptionHtml, String code, Short codeBlockVersion, ProjectType type, String board, Boolean private_, Boolean shared, GregorianCalendar created, GregorianCalendar modified, Long basedOn, String settings) { super(Project.PROJECT); setValue(0, id); @@ -705,5 +745,6 @@ public ProjectRecord(Long id, Long idUser, Long idClouduser, String name, String setValue(12, created); setValue(13, modified); setValue(14, basedOn); + setValue(15, settings); } } diff --git a/src/main/java/com/parallax/server/blocklyprop/rest/RestProject.java b/src/main/java/com/parallax/server/blocklyprop/rest/RestProject.java index 9d490bd1..2cb71f1d 100644 --- a/src/main/java/com/parallax/server/blocklyprop/rest/RestProject.java +++ b/src/main/java/com/parallax/server/blocklyprop/rest/RestProject.java @@ -27,9 +27,11 @@ import com.cuubez.visualizer.annotation.Name; import com.cuubez.visualizer.annotation.M; import com.cuubez.visualizer.annotation.ParameterDetail; + import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.inject.Inject; + import com.parallax.server.blocklyprop.TableOrder; import com.parallax.server.blocklyprop.TableSort; import com.parallax.server.blocklyprop.utils.RestProjectUtils; @@ -51,6 +53,7 @@ import javax.ws.rs.core.Response; import org.apache.shiro.authz.AuthorizationException; +import org.apache.shiro.authz.UnauthorizedException; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -60,6 +63,20 @@ * REST endpoints for project persistence * * @author Michel + * + * @implNote + * + * Supported endpoints: + * [POST] /project/ Save current project + * [GET] /project/list List projects + * [GET] /project/get/{id} Retrieve a project + * [POST] /project/code Update project code + * [POST] /project/code-as Create new project from existing project + * + * The code block is not returned with most of these calls because it is only used when + * the user is viewing or editing a project. This eliminates the unnecessary overhead of + * moving a code block that could easily dwarf the size of the other elements in the project + * record. */ @Path("/project") @@ -153,7 +170,7 @@ public Response get( if (idUser == null || idUser == 0) { // Current session is not logged in. - return Response.status(Response.Status.FORBIDDEN).build(); + return Response.status(Response.Status.UNAUTHORIZED).build(); } //Sanity checks - is the request reasonable @@ -176,7 +193,7 @@ public Response get( limit = REQUEST_LIMIT; } - // Check ofset from the beginning of the record set + // Check offset from the beginning of the record set if ((offset == null) || (offset < 0)) { offset = 0; } @@ -189,13 +206,19 @@ public Response get( return Response.status(Response.Status.NOT_FOUND).build(); } + LOG.info("Returning {} projects for user {}", userProjects.size(), idUser); + return Response.ok( returnProjectsJson( userProjects, projectService.countUserProjects(idUser))) .build(); } - + catch(UnauthorizedException ex) { + // User is logged in but attempting to access a secured resource + LOG.warn("Unauthorized access attempted"); + return Response.status(Response.Status.FORBIDDEN).build(); + } catch(Exception ex) { LOG.warn("Unable to process REST request."); LOG.warn("Error is {}", ex.getMessage()); @@ -276,18 +299,14 @@ public Response saveProjectCode( /* WARNING: * ================================================================================= - * This call can create a new project record under specific circumstances and does - * not appear to provide any notification that this has occurred. + * The call to the saveProjectCode() method can create a new project record under + * specific circumstances and does not appear to provide any notification that this + * has occurred. * ================================================================================= */ ProjectRecord savedProject = projectService.saveProjectCode(idProject, code); LOG.debug("Code for project {} has been saved", idProject); -/* - JsonObject result = projectConverter.toJson(savedProject,false); - result.addProperty("success", true); - return Response.ok(result.toString()).build(); -*/ return Response.ok(buildConvertedResponse(savedProject)).build(); } catch (AuthorizationException ae) { LOG.warn("Project code not saved. Not Authorized"); @@ -371,7 +390,6 @@ public Response saveProjectCodeAs( * or an error message upon failure */ @POST - @Path("/") @Detail("Save project") @Name("Save project") @Produces("application/json") @@ -439,15 +457,19 @@ public Response saveProject( * A String containing the array of the converted Json objects */ private String returnProjectsJson(@NotNull List projects, int projectCount) { + JsonObject result = new JsonObject(); - JsonArray jsonProjects = new JsonArray(); - for (ProjectRecord project : projects) { - jsonProjects.add(projectConverter.toListJson(project)); - } + if (projects.size() > 0) { + JsonArray jsonProjects = new JsonArray(); + + for (ProjectRecord project : projects) { + jsonProjects.add(projectConverter.toListJson(project)); + } - result.add("rows", jsonProjects); - result.addProperty("total", projectCount); + result.add("rows", jsonProjects); + result.addProperty("total", projectCount); + } return result.toString(); } diff --git a/src/main/java/com/parallax/server/blocklyprop/rest/RestUser.java b/src/main/java/com/parallax/server/blocklyprop/rest/RestUser.java index ad881022..ed35eb13 100644 --- a/src/main/java/com/parallax/server/blocklyprop/rest/RestUser.java +++ b/src/main/java/com/parallax/server/blocklyprop/rest/RestUser.java @@ -72,7 +72,7 @@ public void setUserService(UserService userService) { * Returns a list of all user objects. */ @GET - @Path("/") +// @Path("/") @Detail("Get all users") @Name("Get all users") @Produces("application/json") diff --git a/src/main/java/com/parallax/server/blocklyprop/rest/RestV2Project.java b/src/main/java/com/parallax/server/blocklyprop/rest/RestV2Project.java new file mode 100644 index 00000000..e85aca9f --- /dev/null +++ b/src/main/java/com/parallax/server/blocklyprop/rest/RestV2Project.java @@ -0,0 +1,945 @@ +/* + * Copyright (c) 2019 Parallax Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the “Software”), to deal in the Software without + * restriction, including without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.parallax.server.blocklyprop.rest; + +import com.cuubez.visualizer.annotation.*; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.inject.Inject; + +import com.parallax.server.blocklyprop.TableOrder; +import com.parallax.server.blocklyprop.TableSort; +import com.parallax.server.blocklyprop.converter.ProjectConverter; +import com.parallax.server.blocklyprop.db.enums.ProjectType; +import com.parallax.server.blocklyprop.db.generated.tables.records.ProjectRecord; +import com.parallax.server.blocklyprop.security.BlocklyPropSecurityUtils; +import com.parallax.server.blocklyprop.services.ProjectService; +import com.parallax.server.blocklyprop.utils.RestProjectUtils; + +import org.apache.shiro.authz.AuthorizationException; +import org.apache.shiro.authz.UnauthorizedException; +import org.apache.commons.lang.Validate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.List; + +import org.jetbrains.annotations.NotNull; + + +/** + * Version 2 REST endpoints for private project persistence. Access is limited to + * authenticated users. Refer to the /shared/project/* endpoints for + * public access to projects. + * + * @author Michel, J. Ewald + * + * @implNote + * + * Version 1 Supported endpoints: + * [POST] /project/ Save current project + * [GET] /project/list List projects + * [GET] /project/get/{id} Retrieve a project + * [POST] /project/code Update project code + * [POST] /project/code-as Create new project from existing project + * + * Version 2 supported endpoints + * ----------------------------------------------------------------------------------------------------- + * CREATE + * [POST] /v2/project/ Creates a new project and returns it in the response body + * [POST] /v2/project/{id} Creates a new project using the contents of the provided + * project id + * + * RETRIEVE + * [GET] /v2/project/ Returns a list of projects; parameters in request body + * [GET] /v2/project/{id} Returns the specific project if authorized + * + * UPDATE + * [PUT] /v2/project/{id} Updates s specific project. Project details are in the request body + * + * DELETE + * [DELETE] /v2/project/{id} Destroys the project specified by {id} + * ----------------------------------------------------------------------------------------------------- + */ + +@Path("/v2/project") +@Group(name = "/project", title = "Project management") +@HttpCode("500>Internal Server Error,200>Success Response") +public class RestV2Project { + + // Get a logger instance + private static final Logger LOG = LoggerFactory.getLogger(RestProject.class); + + + // Connector to project services object + private ProjectService projectService; + + + //Connector to project converter object + private ProjectConverter projectConverter; + + + //Limit the number of records that can be returned in list functions + private static final int REQUEST_LIMIT = 100; + + + + /** + * Connect to the project service object + * + * @param projectService + * An instance of the ProjectService object + */ + @Inject + public void setProjectService(ProjectService projectService) { + this.projectService = projectService; + } + + + + /** + * Connect to the project converter object + * + * @param projectConverter + * An instance of the ProjectConverter object + */ + @Inject + public void setProjectConverter(ProjectConverter projectConverter) { + this.projectConverter = projectConverter; + } + + + + // GET /v2/project/ping + /** + * Test endpoint to verify that the class is reachable + * + * @return a Json reply, 'Pong' + */ + @GET + @Path("/ping") + @Detail("Testing for life on Mars") + @Name("PingProjects") + @Produces("application/json") + public Response get() { + return Response.ok("{\"result\": \"Pong\"}").build(); + } + + + + // POST /v2/project/ + /** + * Create a new project. Access is limited to authenticated users. + * + * @param projectName + * is a required string parameter containing the project name + * + * @param description + * is a required string parameter containing the project description + * + * @param descriptionHtml + * is an optional parameter containing the HTML representation of the + * project description + * + * @param code + * is a required parameter containing the XML representation of the + * project's code blocks + * + * @param projectSharing + * is an optional parameter containing one of two possible strings; + * 'private' or 'shared. + * + * The project will be marked as a public or community project only + * if the 'shared' keyword value is supplied. Otherwise, if the + * 'private' keyword is supplied or no keyword is supplied, the + * project will be configured as a private project. + * + * @param type + * is a required parameter indicating the project's source language, either + * SPIN or PROPC. + * + * @param board + * is a required parameter indicating the type of board used for the project. + * + * @param settings + * is an optional parameter containing a Json encoded string of various + * custom project settings. + * + * @return + * Returns a Json string containing the project details, including the new + * project ID if successful or an error message upon failure + * + * @implNote + * + * VERB URI Notes: + * ------- ---------------------- ---------------------------------------------- + * [POST] /v2/project/ Create a new project from the data provided. + * The service returns a Json string containing + * the new project details. + * + * Return value in response body: + * { + * "id": 66, + * "name": "Chocolate Factory IV", + * "description": "Willie Wonka and the factory", + * "type": "PROPC", + * "board": "heb", + * "private": true, + * "shared": false, + * "created": "2019/02/28 06:36", + * "modified": "2019/02/28 06:36", + * "settings": null, + * "yours": true, + * "user": "demo-998", + * "success": true + * } + * + * @apiNote + * + * This endpoint could also handle the use case where the client wishes to + * create a new copy of an existing project. There are two possible branches + * in this expanded specification; + * + * 1) The existing project belongs to the current user. + * + * 2) The existing project is a) a community project or b) a shared project. + * + * In both cases, a new project is created from the contents of the existing + * project. The new project is designated as 'private' and it's parent field + * is set to the project id of the source project. + * + * TODO: Monitor hits/second from a single user to prevent malicious spamming + */ + @POST + @Detail("Create a new project") + @Name("Create project") + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Produces(MediaType.APPLICATION_JSON) + public Response createProject( + @FormParam("name") String projectName, + @FormParam("description") String description, + @FormParam("description-html") String descriptionHtml, + @FormParam("code") String code, + @FormParam("sharing") String projectSharing, + @FormParam("type") ProjectType type, + @FormParam("board") String board, + @FormParam("settings") String settings) { + + LOG.info("REST:/rest/v2/project/ POST request received to create a new project '{}'", projectName); + + try { + // Required fields + Validate.notEmpty(projectName, "A project name is required."); + Validate.notEmpty(description, "A project description is required."); + Validate.notEmpty(code, "A project code block is required."); + Validate.notNull(type, "The project type is required."); + Validate.notEmpty(board, "The project board type is required."); + + boolean privateProject = false; + boolean sharedProject = false; + + if ("private".equalsIgnoreCase(projectSharing)) { + privateProject = true; + } else if ("shared".equalsIgnoreCase(projectSharing)) { + sharedProject = true; + } + + // Validate the project type + if (! RestProjectUtils.ValidateProjectType(type)) { + return Response.status(Response.Status.NOT_ACCEPTABLE).build(); + } + + LOG.info("Parameters are valid. Saving the project"); + ProjectRecord savedProject = projectService.saveProject( + null, projectName, description, descriptionHtml, code, + privateProject, sharedProject, type, board, settings); + + if (savedProject == null) { + LOG.warn("Unable to create a new project record"); + } + + JsonObject result = projectConverter.toJson(savedProject,false); + result.addProperty("success", true); + + return Response.ok(result.toString()).build(); + } + catch (NullPointerException ex) { + LOG.warn("Null pointer exception detected. {}", ex.getMessage()); + return Response.status(Response.Status.NOT_ACCEPTABLE).build(); + } + catch (IllegalArgumentException aex) { + LOG.warn("Illegal argument exception detected. {}", aex.getMessage()); + return Response.status(Response.Status.NOT_ACCEPTABLE).build(); + } + catch (AuthorizationException ae) { + LOG.warn("Project not saved. Not Authorized"); + return Response.status(Response.Status.UNAUTHORIZED).build(); + } + catch (Exception ex) { + LOG.error("General exception encountered. Message is: ", ex.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + } + + + + // POST /v2/project/{id} + /** + * Create a new project based on an existing project. The resulting new + * project will be placed in the logged in user's library as a private + * project. + * + * @param idProject is the identifier of the project to use as a + * source for the new project + * @param projectName + * is a required string parameter containing the project name + * + * @param description + * is a required string parameter containing the project description + * + * @param descriptionHtml + * is an optional parameter containing the HTML representation of the + * project description + * + * @param code + * is a required parameter containing the XML representation of the + * project's code blocks + * + * @param type + * is a required parameter indicating the project's source language, either + * SPIN or PROPC. + * + * @param board + * is a required parameter indicating the type of board used for the project. + * + * @param settings + * is an optional parameter containing a Json encoded string of various + * custom project settings. + * + * @return + * Returns a Json string containing the project details, including the new + * project ID if successful or an error message upon failure + * + * @implNote + * + * VERB URI Notes: + * ------- ---------------------- ---------------------------------------------- + * [POST] /v2/project/{id} Create a new project from the data provided. + * The service returns a Json string containing + * the new project details. + * + * Return value in response body: + * { + * "id": 66, + * "name": "Chocolate Factory IV", + * "description": "Willie Wonka and the factory", + * "type": "PROPC", + * "board": "heb", + * "private": true, + * "shared": false, + * "created": "2019/02/28 06:36", + * "modified": "2019/02/28 06:36", + * "settings": null, + * "yours": true, + * "user": "demo-998", + * "success": true + * } + * + * @apiNote + * + * This endpoint could also handle the use case where the client wishes to + * create a new copy of an existing project. There are two possible branches + * in this expanded specification; + * + * 1) The existing project belongs to the current user. + * + * 2) The existing project is a) a community project or b) a shared project. + * + * In both cases, a new project is created from the contents of the existing + * project. The new project is designated as 'private' and it's parent field + * is set to the project id of the source project. + * + * TODO: Monitor hits/second from a single user to prevent malicious spamming + */ + @POST + @Path("/{id}") + @Detail("Create a new copy of an existing project") + @Name("Create project copy") + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Produces(MediaType.APPLICATION_JSON) + public Response createProjectCopy( + @PathParam("id") @ParameterDetail("Project identifier") Long idProject, + @FormParam("name") String projectName, + @FormParam("description") String description, + @FormParam("description-html") String descriptionHtml, + @FormParam("code") String code, + @FormParam("type") ProjectType type, + @FormParam("board") String board, + @FormParam("settings") String settings) { + + LOG.info("REST:/rest/v2/project/id POST request received to create a new project '{}'", projectName); + + /** + * This method will retrieve the specified project, verify that it is either a + * public (community) project or if it is a private project, is the project available + * as a shared project. + * + * TODO: If we are cloning a shared project, should we require the shared project key? + * It look like we're creating a separate endpoint to accept a shared project token as + * the source project identifier. + * + */ + try { + // Required fields + Validate.notNull(idProject, "A parent project id is required."); + + /* + * All of these parameters are optional. IF they are not provided, + * the copy operation will take the settings from the original project + * + */ + + LOG.info("Parameters are valid. Saving the project"); + + ProjectRecord project = projectService.createProjectCopy( + idProject, projectName, description, descriptionHtml, + code, type, board, settings); + + LOG.info("Project created?"); + + if (project == null) { + LOG.warn("Unable to create a new project record"); + } + + JsonObject result = projectConverter.toJson(project,false); + result.addProperty("success", true); + + return Response.ok(result.toString()).build(); + } + catch (NullPointerException ex) { + LOG.warn("Null pointer exception detected. {}", ex.getMessage()); + return Response.status(Response.Status.NOT_ACCEPTABLE).build(); + } + catch (IllegalArgumentException aex) { + LOG.warn("Illegal argument exception detected. {}", aex.getMessage()); + return Response.status(Response.Status.NOT_ACCEPTABLE).build(); + } + catch (AuthorizationException ae) { + LOG.warn("Project not saved. Not Authorized"); + return Response.status(Response.Status.UNAUTHORIZED).build(); + } + catch (Exception ex) { + LOG.error("General exception encountered. Message is: ", ex.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + } + + + + // GET /v2/project/ + /** + * Return a list of projects owned by the currently authenticated user. + * + * @param sort + * The project field used to evaluate the sort + * + * @param order + * Specify the sort order - ascending or descending + * + * @param limit + * Specify the maximum number of rows to return + * + * @param offset + * Specify the beginning row to return + * + * @return + * Return a response object that contains either the data requested + * or a JSON string containing the error details + * + * @implNote + * + * VERB URI Notes: + * ------- ---------------------- -------------------------------------- + * [GET] /v2/project/ Returns a list of projects; parameters + * + * Sample output: + * { + * "rows": [ + * { + * "id": 16, + * "name": "XBee Sandbox", + * "description": "Testing XBee blocks", + * "type": "PROPC", + * "board": "activity-board", + * "private": false, + * "shared": true, + * "created": "2017/04/12 06:01", + * "modified": "2018/10/18 04:06", + * "settings": null, + * "yours": true, + * "user": "demo-998", + * "id-user": 1 + * }, + * { + * "id": 13, + * "name": "TestIssue#886", + * "description": "Testing pin dropdown.", + * "type": "PROPC", + * "board": "activity-board", + * "private": true, + * "shared": false, + * "created": "2017/01/17 23:25", + * "modified": "2017/01/17 23:25", + * "settings": null, + * "yours": true, + * "user": "demo-998", + * "id-user": 1 + * } + * ], + * "total": 41 + * } + * --------------------------------------------------------------------------------- + */ + @GET + @Detail("Retrieve a list of projects for the authenticated user") + @Name("ListProjects") + @Produces("application/json") + public Response get( + @QueryParam("sort") @ParameterDetail("Sort detail") @M() TableSort sort, + @QueryParam("order") @ParameterDetail("Sort order") @M() TableOrder order, + @QueryParam("limit") @ParameterDetail("Number of rows to return") @M() Integer limit, + @QueryParam("offset") @ParameterDetail("Offset to next row returned") @M() Integer offset) { + + String endPoint = "REST:/rest/v2/project/list/"; + LOG.info("{} Get request received", endPoint); + + try { + LOG.debug("Requesting blockly user id"); + + // Get the logged in user id for the current session + Long idUser = BlocklyPropSecurityUtils.getCurrentUserId(); + + // Return UNAUTHORIZED if we cannot identify the current user. This could + // mean that the user is not logged in or that some underlying issue + // is causing the authentication system to fail. + LOG.info("Received blockly user id: {}", idUser); + + if (idUser == null || idUser == 0) { + // Current session is not logged in. + return Response.status(Response.Status.UNAUTHORIZED).build(); + } + + //Sanity checks - is the request reasonable + + // Sort flag evaluation + if (!RestProjectUtils.ValidateSortType(sort)) { + LOG.warn("{} Sort parameter failed. Defaulting to sort by project name", endPoint); + sort = TableSort.name; + } + + // Sort order evaluation + if (!RestProjectUtils.ValidateSortOrder(order)) { + LOG.warn("{} Sort order parameter failed. Defaulting to ascending order", endPoint); + order = TableOrder.asc; + } + + // Limit result set value + if ( (limit == null) || (limit > REQUEST_LIMIT)) { + LOG.info("{} Limit throttle to {} entries", endPoint, REQUEST_LIMIT); + limit = REQUEST_LIMIT; + } + + // Check offset from the beginning of the record set + if ((offset == null) || (offset < 0)) { + offset = 0; + } + + // Obtain a list of the user's projects + List userProjects = projectService.getUserProjects(idUser, sort, order, limit, offset); + + // Tell the caller that there is nothing to see here + if (userProjects == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + + return Response.ok( + returnProjectsJson( + userProjects, + projectService.countUserProjects(idUser))) + .build(); + } + catch(UnauthorizedException ex) { + LOG.warn("Unauthorized access attempted"); + return Response.status(Response.Status.FORBIDDEN).build(); + } + catch(Exception ex) { + LOG.warn("Unable to process REST request."); + LOG.warn("Error is {}", ex.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + } + + + // GET /v2/project/{id} + /** + * Retreive a project based on the supplied project ID + * + * @param idProject the project key ID + * + * @return + * Return a string representation of the project in Json format if successful, otherwise + * return a Json string containing an error status message + * + * @implNote + * + * VERB URI Notes: + * ------- ---------------------- -------------------------------------- + * [GET] /v2/project/{id} Returns a single projects based on the + * project ID received and the user is + * owner of the project. + * + * Sample output: + * + * { + * "id": 20, + * "name": "AB-Drive 360 Error", + * "description": "Testing a linking error that appears when the AB360 library is invoked.\nTesting search engine bits a little more.", + * "type": "PROPC", + * "board": "activity-board", + * "private": true, + * "shared": false, + * "created": "2017/10/27 16:50", + * "modified": "2018/10/17 19:22", + * "settings": "{\"settings\": {\"setting_1\": \"Always-ON\", \"setting-2\": \"false\", \"setting-3\": \"/cdn/bootloader\"}}", + * "yours": true, + * "user": "demo-998", + * "id-user": 1 + * } + * + * ---------------------------------------------------------------------------------- + * + */ + @GET + @Path("/{id}") + @Detail("Get project by id") + @Name("Get project by id") + @Produces("application/json") + public Response get(@PathParam("id") @ParameterDetail("Project identifier") Long idProject) { + + LOG.info("REST:/rest/v2/project/get/ Get request received for project '{}'", idProject); + + try { + ProjectRecord project = projectService.getProject(idProject); + + if (project != null) { + // Verify that the current user owns the requested project + if (!project.getIdUser().equals(BlocklyPropSecurityUtils.getCurrentUserId())) { + LOG.info("User not authorized to get project {}", idProject); + return Response.status(Response.Status.UNAUTHORIZED).build(); + } + } else { + LOG.info("Project {} was not found", idProject); + return Response.status(Response.Status.NOT_FOUND).build(); + } + + // The current user owns this project + JsonObject result = projectConverter.toJson(project,false); + result.addProperty("id-user", project.getIdUser()); + + return Response.ok(result.toString()).build(); + } + + catch(Exception ex) { + LOG.warn("Unable to process REST request."); + LOG.warn("Error is {}", ex.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + } + + + + + // PUT /v2/project/{id} + /** + * Update an existing project + * + * @param idProject is the identifier of the project to use as a + * source for the new project + * @param projectName + * is a required string parameter containing the project name + * + * @param description + * is a required string parameter containing the project description + * + * @param descriptionHtml + * is an optional parameter containing the HTML representation of the + * project description + * + * @param code + * is a required parameter containing the XML representation of the + * project's code blocks + * + * @param type + * is a required parameter indicating the project's source language, either + * SPIN or PROPC. + * + * @param board + * is a required parameter indicating the type of board used for the project. + * + * @param settings + * is an optional parameter containing a Json encoded string of various + * custom project settings. + * + * @return + * Returns a Json string containing the project details, including the new + * project ID if successful or an error message upon failure + */ + @PUT + @Path("/{id}") + @Detail("Update voluble elements of an existing project") + @Name("Update Project by id") + @Produces("application/json") + public Response update( + @PathParam("id") @ParameterDetail("Project identifier") Long idProject, + @FormParam("name") String projectName, + @FormParam("description") String description, + @FormParam("description-html") String descriptionHtml, + @FormParam("code") String code, + @FormParam("type") ProjectType type, + @FormParam("board") String board, + @FormParam("settings") String settings) { + + LOG.info("REST:/rest/v2/project/{} PUT request received for project", idProject); + + ProjectRecord project; + + // Get the specified project + try { + project = projectService.getProject(idProject); + + if (project != null) { + // Verify that the current user owns the requested project + if (!project.getIdUser().equals(BlocklyPropSecurityUtils.getCurrentUserId())) { + LOG.info("User not authorized to get project {}", idProject); + return Response.status(Response.Status.UNAUTHORIZED).build(); + } + } else { + LOG.info("Project {} was not found", idProject); + return Response.status(Response.Status.NOT_FOUND).build(); + } + } + catch (Exception ex) { + LOG.warn("An unexpected exception has occurred. Message: {}", ex.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + + // ----------------------------------------------------------------------- + // Update fields with passed in parameters. Parameters that are null will + // not update the record. However, parameters that are empty strings will + // update the corresponding field in the project record + // ----------------------------------------------------------------------- + project = updateProjectRecordFields( + project, projectName, description, descriptionHtml, + code, type, board, settings); + + // Save the record + ProjectRecord revisedProject = projectService.saveProject(project); + if (revisedProject == null) { + LOG.warn("Unable to update project {}", idProject); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + + // Return the saved record + JsonObject result = projectConverter.toJson(project,false); + + result.addProperty("result", "success"); + return Response.ok(result.toString()).build(); + } + + + // DELETE /v2/project/{id} + /** + * Delete a project only if the project is owned by the currently logged in user. + * + * @param idProject is the identifier of the project to use as a + * source for the new project + + * @return a Json formatted string containing "success" otherwise returns an + * error code indicating the type of failure encountered. + */ + @DELETE + @Path("/{id}") + @Detail("Delete a project by the project id") + @Name("Delete Project by id") + @Produces("application/json") + public Response delete( + @PathParam("id") @ParameterDetail("Project identifier") Long idProject) { + + LOG.info("REST:/rest/v2/project/{} DELETE request received for project", idProject); + + ProjectRecord project; + + // Get the specified project + try { + project = projectService.getProject(idProject); + + if (project == null) { + LOG.info("Project {} was not found", idProject); + return Response.status(Response.Status.NOT_FOUND).build(); + } + + // Verify that the current user owns the requested project + if (!project.getIdUser().equals(BlocklyPropSecurityUtils.getCurrentUserId())) { + LOG.info("User not authorized to get project {}", idProject); + return Response.status(Response.Status.UNAUTHORIZED).build(); + } + + LOG.info("Project {} is ready to be deleted."); + + // Delete the record + if (!projectService.deleteProject(idProject)) { + LOG.warn("Unable to delete project {}", idProject); + return Response.status(Response.Status.NOT_MODIFIED).build(); + } + + JsonObject result = new JsonObject(); + result.addProperty("result", "success"); + return Response.ok(result.toString()).build(); + + } + catch (Exception ex) { + LOG.warn("An unexpected exception has occurred. Message: {}", ex.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + } + + + + + + /** + * Iterate a list of projects into an array of Json objects + * + * @param projects + * A List of ProjectRecord objects + * + * @param projectCount + * The number of projects available. This may not be the same value as + * the number of records contained in the passed list of ProjectRecords. + * + * @return + * A String containing the array of the converted Json objects + */ + private String returnProjectsJson(@NotNull List projects, int projectCount) { + JsonObject result = new JsonObject(); + JsonArray jsonProjects = new JsonArray(); + + for (ProjectRecord project : projects) { + jsonProjects.add(projectConverter.toListJson(project)); + } + + result.add("rows", jsonProjects); + result.addProperty("total", projectCount); + + return result.toString(); + } + + + + /** + * Convert a ProjectRecord to a Json string + * + * @param project is the project record to convert + * + * @return a Json string representing the project contents and the operation results message + */ + private String buildConvertedResponse(@NotNull ProjectRecord project) { + + /* Convert the project record to a Json object */ + JsonObject result = projectConverter.toJson(project,false); + + /* Add in a results message */ + result.addProperty("success", true); + + return result.toString(); + } + + + /** + * Update the fields in a ProjectRecord object + * + * @param project is a ProjectRecord object that will be updated with the date + * passed in the other parameters of this call. + * @param projectName is a string parameter containing the new project name + * @param description is a string parameter containing the project description + * @param descriptionHtml is a string containing the HTML representation of the + * project description + * @param code is a string containing the XML representation of the project's code blocks + * @param type is a ProjectType enumberation indicating the project's source language, + * currently either SPIN or PROPC + * @param board is a string indicating the type of board used for the project. + * @param settings is an optional parameter containing a Json encoded string of various + * custom project settings. + * + * @return an updated ProjectRecord object. + */ + private ProjectRecord updateProjectRecordFields( + @NotNull ProjectRecord project, + String projectName, String description, String descriptionHtml, + String code, ProjectType type, String board, String settings) { + + if (projectName != null) { + project.setName(projectName); + } + + if (description != null) { + project.setDescription(description); + } + + if (descriptionHtml != null) { + project.setDescriptionHtml(descriptionHtml); + } + + if (code != null) { + project.setCode(code); + } + + if (type != null) { + project.setType(type); + } + + if (board != null) { + project.setBoard(board); + } + + if (settings != null) { + project.setSettings(settings); + } + + return project; + } +} + + diff --git a/src/main/java/com/parallax/server/blocklyprop/services/ProjectService.java b/src/main/java/com/parallax/server/blocklyprop/services/ProjectService.java index 5f2b9369..e7695a20 100644 --- a/src/main/java/com/parallax/server/blocklyprop/services/ProjectService.java +++ b/src/main/java/com/parallax/server/blocklyprop/services/ProjectService.java @@ -1,8 +1,24 @@ /* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. + * Copyright (c) 2019 Parallax Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the “Software”), to deal in the Software without + * restriction, including without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. */ + package com.parallax.server.blocklyprop.services; import com.parallax.server.blocklyprop.TableOrder; @@ -12,38 +28,266 @@ import java.util.List; /** + * Interface definition for the project services layer * * @author Michel */ public interface ProjectService { - ProjectRecord getProjectOwnedByThisUser(Long idProject); + //------------- + // Getters + //------------- - // Return a project + /** + * Retrieve a project record keyed on the unique project id + * + * @param idProject Primary key id for the requested project + * @return a ProjectRecord object if successful, otherwise null + */ ProjectRecord getProject(Long idProject); - List getUserProjects(Long idUser, TableSort tablesSort, TableOrder order, Integer limit, Integer offset); - List getSharedProjects(TableSort tablesSort, TableOrder order, Integer limit, Integer offset); + /** + * Retrieve a project record keyed on the unique project id. + * The requested record must be owned by the current user + * + * @param idProject Primary key id for the requested project + * @return a ProjectRecord object if successful, otherwise null + * + * @implNote This method appears to be used only within the context of deleting + * a project. In the current implementation, there is no concept of + * an admin or superuser account or role that can delete projects on + * a global scale. + */ + ProjectRecord getProjectOwnedByThisUser(Long idProject); + + + /** + * Retrieve a list of projects owned by a specific user + * + * @param idUser + * @param tablesSort + * @param order + * @param limit + * @param offset + * + * @return + */ + List getUserProjects( + Long idUser, + TableSort tablesSort, + TableOrder order, + Integer limit, + Integer offset); + + + /** + * Retrieve a list of public projects + * + * @param tablesSort + * @param order + * @param limit + * @param offset + * + * @return + */ + List getSharedProjects( + TableSort tablesSort, + TableOrder order, + Integer limit, + Integer offset); + + + /** + * Retrieve a list of public projects owned by a specific user + * + * @param tablesSort + * @param order + * @param limit + * @param offset + * @param idUser + * @return + */ + List getSharedProjectsByUser( + TableSort tablesSort, + TableOrder order, + Integer limit, + Integer offset, + Long idUser); + + + //------------- + // Create + //------------- + + /** + * Create a new project + * + * @param name + * @param description + * @param descriptionHtml + * @param privateProject + * @param sharedProject + * @param type + * @param board + * @return + */ + ProjectRecord createProject( + String name, + String description, + String descriptionHtml, + boolean privateProject, + boolean sharedProject, + ProjectType type, + String board); + + + + ProjectRecord createProject( + String name, + String description, + String descriptionHtml, + String projectCode, + boolean privateProject, + boolean sharedProject, + ProjectType type, + String board, + String settings); - List getSharedProjectsByUser(TableSort tablesSort, TableOrder order, Integer limit, Integer offset, Long idUser); + + // Create a copy of an existing project overriding project elements + // with those provided in the parameters supplied + // ----------------------------------------------------------------- + ProjectRecord createProjectCopy( + Long idSourceProject, + String name, + String description, + String descriptionHtml, + String projectCode, + ProjectType type, + String board, + String settings); + + //------------- + // Count + //------------- + + /** + * Count the number of projects owned by a specific user + * + * @param idUser + * @return + */ int countUserProjects(Long idUser); + // TODO: Merge this method with the countSharedProjectsByUser method + + /** + * Count the number of public projects + * + * @return + */ int countSharedProjects(); - + + + /** + * Count the number of public projects oned by a specific user + * + * @param idUser + * @return + */ int countSharedProjectsByUser(Long idUser); - ProjectRecord saveProject(Long idProject, String name, String description, String descriptionHtml, boolean privateProject, boolean sharedProject, ProjectType type, String board); - ProjectRecord cloneProject(Long idProject); + //------------- + // Update + //------------- - ProjectRecord createProject(String name, String description, String descriptionHtml, boolean privateProject, boolean sharedProject, ProjectType type, String board); + /** + * Update an existing project + * + * @param idProject + * @param name + * @param description + * @param descriptionHtml + * @param privateProject + * @param sharedProject + * @param type + * @param board + * + * @return + */ + @Deprecated + ProjectRecord saveProject( + Long idProject, + String name, + String description, + String descriptionHtml, + boolean privateProject, + boolean sharedProject, + ProjectType type, + String board); + + ProjectRecord saveProject( + Long idProject, + String name, + String description, + String descriptionHtml, + String projectCode, + boolean privateProject, + boolean sharedProject, + ProjectType type, + String board, + String settings); - boolean deleteProject(Long idProject); + // Update an existing project + ProjectRecord saveProject(ProjectRecord project); + + /** + * Save the code blocks for a specified project + * + * @param idProject + * @param code + * + * @return + */ ProjectRecord saveProjectCode(Long idProject, String code); - public ProjectRecord saveProjectCodeAs(Long idProject, String code, String newName, String newBoard); + /** + * Create a new project owned by the current user that is a copy of the original project + * + * @param idProject + * @param code + * @param newName + * @param newBoard + * + * @return + */ + ProjectRecord saveProjectCodeAs(Long idProject, String code, String newName, String newBoard); + + + /** + * Create a copy of the existing project into the current user's library + * + * @param idProject + * + * @return + */ + ProjectRecord cloneProject(Long idProject); + + + //------------- + // Delete + //------------- + + /** + * Destroy the specified project only if the project is owned by the current user + * + * @param idProject + * @return + */ + boolean deleteProject(Long idProject); } diff --git a/src/main/java/com/parallax/server/blocklyprop/services/impl/ProjectServiceImpl.java b/src/main/java/com/parallax/server/blocklyprop/services/impl/ProjectServiceImpl.java index ad3ada24..1b5830cd 100644 --- a/src/main/java/com/parallax/server/blocklyprop/services/impl/ProjectServiceImpl.java +++ b/src/main/java/com/parallax/server/blocklyprop/services/impl/ProjectServiceImpl.java @@ -20,29 +20,61 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.shiro.authz.UnauthorizedException; +// import org.apache.commons.lang.StringUtils; /** + * Implementation of project services layer * * @author Michel + * + * @implNote + * Any method or class marked with the @Transactional decorator will be considered for + * transactionality. Consult the documentation on https://github.com/google/guice/wiki/GuicePersist + * for detailed semantics. Marking a method @Transactional will start a new transaction before + * the method executes and commit it after the method returns. + * + * If the method throws an exception, the transaction will be rolled back unless you have specifically + * requested not to in the ignore() clause. + * + * Similarly, the set of exceptions that will trigger a rollback can be defined in the rollbackOn() + * clause. By default, only unchecked exceptions trigger a rollback. + * + * Public Methods: + * createProject() + * createProjectCopy() + * saveProject() + * */ @Singleton @Transactional public class ProjectServiceImpl implements ProjectService { + //Application logging facility + private static final Logger LOG = LoggerFactory.getLogger(ProjectServiceImpl.class); + + // Object to store database layer access private ProjectDao projectDao; + + // Object to store project sharing service access private ProjectSharingService projectSharingService; - + /** - * Application logging facility + * Inject support for DAO layer access + * + * @param projectDao object to store injected database layer access */ - private static final Logger LOG = LoggerFactory.getLogger(ProjectServiceImpl.class); - @Inject public void setProjectDao(ProjectDao projectDao) { this.projectDao = projectDao; } + + /** + * Inject support for access to the project sharing services + * + * @param projectSharingService object to store injected project sharing service access + */ @Inject public void setProjectSharingService(ProjectSharingService projectSharingService) { this.projectSharingService = projectSharingService; @@ -51,7 +83,9 @@ public void setProjectSharingService(ProjectSharingService projectSharingService /** * Create a new project record - * + *

+ * This method signature is deprecated. + * * @param name * @param description * @param descriptionHtml @@ -61,12 +95,17 @@ public void setProjectSharingService(ProjectSharingService projectSharingService * @param board * @return */ + @Deprecated @Override public ProjectRecord createProject( - String name, String description, String descriptionHtml, - boolean privateProject, boolean sharedProject, ProjectType type, + String name, + String description, + String descriptionHtml, + boolean privateProject, + boolean sharedProject, + ProjectType type, String board) { - + // Calling saveProject with a null project id will force underlying code // to create a new project return saveProject( @@ -74,6 +113,133 @@ public ProjectRecord createProject( sharedProject, type, board); } + + /** + * + * @param name + * @param description + * @param descriptionHtml + * @param projectCode + * @param privateProject + * @param sharedProject + * @param type + * @param board + * @param settings + * @return + */ + @Override + public ProjectRecord createProject( + String name, + String description, + String descriptionHtml, + String projectCode, + boolean privateProject, + boolean sharedProject, + ProjectType type, + String board, + String settings) { + + return saveProject( + null, + name, + description, + descriptionHtml, + projectCode, + privateProject, + sharedProject, + type, + board, + settings); + // FIXME: Add project code blocks string + + } + + + + @Override + public ProjectRecord createProjectCopy( + Long idSourceProject, + String name, + String description, + String descriptionHtml, + String code, + ProjectType type, + String board, + String settings) { + + + // local project details + String projectName = null; + String projectDescription = null; + String projectDescriptionHtml = null; + String projectCode = null; + ProjectType projectType = null; + String projectBoard = null; + String projectSettings = null; + + + // Obtain the source project + LOG.info("Service retrieving project {} from database", idSourceProject); + ProjectRecord sourceProject = projectDao.getProject(idSourceProject); + + if (sourceProject == null) { + LOG.info("Project retrieval failed."); + return null; + } + + LOG.info("Got project #{}", sourceProject.getId()); + + // Prepare date for new project + try { + projectName = (name != null && !name.trim().isEmpty()) ? + name : sourceProject.getName(); + + projectDescription = (description != null && !description.trim().isEmpty()) ? + description : sourceProject.getDescription(); + + projectDescriptionHtml = (descriptionHtml != null && !descriptionHtml.trim().isEmpty()) ? + descriptionHtml : + sourceProject.getDescriptionHtml(); + + projectCode = (code != null && !code.trim().isEmpty()) ? + code : sourceProject.getCode(); + + projectType = (type != null) ? type : sourceProject.getType(); + + projectBoard = (board != null && !board.trim().isEmpty()) ? + board : sourceProject.getBoard(); + + projectSettings = (settings != null && !settings.trim().isEmpty()) ? + settings : sourceProject.getSettings(); + + } + catch (NullPointerException ex) { + LOG.info("Something is null and not a good thing"); + } + + LOG.info("Creating the new project copy"); + + + // Create a new project record + ProjectRecord newProject = projectDao.createProject( + projectName, + projectDescription, + projectDescriptionHtml, + projectCode, + projectType, + projectBoard, + true, // Make it a private project + false, // which means it is not shared + idSourceProject, // parent project is the source project + projectSettings); + + + LOG.info("Returning the new project"); + return newProject; + } + + + /** * Update an existing project or create a new project. * @@ -93,6 +259,7 @@ public ProjectRecord createProject( * @param board * @return */ + @Deprecated @Override public ProjectRecord saveProject( Long idProject, String name, String description, @@ -112,10 +279,48 @@ public ProjectRecord saveProject( } - + @Override + public ProjectRecord saveProject( + Long idProject, String name, String description, String descriptionHtml, + String code, + boolean privateProject, + boolean sharedProject, ProjectType type, String board, String settings) { + + // Check if project is from the current user, if not, unset idProject and create new + if (idProject != null) { + return projectDao.updateProject( + idProject, name, description, descriptionHtml, + privateProject, sharedProject); + } else { + return projectDao.createProject( + name, description, descriptionHtml, + code, + type, board, privateProject, sharedProject, + null, + settings); + } + } + + + // Update a project, using the existing project record. + @Override + public ProjectRecord saveProject( ProjectRecord project) { + return projectDao.updateProject( + project.getId(), + project.getName(), + project.getDescription(), + project.getDescriptionHtml(), + project.getCode(), + project.getPrivate(), + project.getShared(), + project.getSettings()); + } + @Override public ProjectRecord getProjectOwnedByThisUser(Long idProject) { + ProjectRecord projectRecord = projectDao.getProject(idProject); + if (projectRecord != null) { if (projectRecord.getIdUser().equals(BlocklyPropSecurityUtils.getCurrentUserId())) { return projectRecord; @@ -208,7 +413,13 @@ public List getSharedProjects( } @Override - public List getSharedProjectsByUser(TableSort sort, TableOrder order, Integer limit, Integer offset, Long idUser) { + public List getSharedProjectsByUser( + TableSort sort, + TableOrder order, + Integer limit, + Integer offset, + Long idUser) { + return projectDao.getSharedProjectsByUser(sort, order, limit, offset, idUser); } @@ -244,11 +455,11 @@ public ProjectRecord cloneProject(Long idProject) { @Override public boolean deleteProject(Long idProject) { - LOG.info("Deleting project {}", idProject); - // Remove the project shared key if it exists. + LOG.info("Deleting project {} shared link", idProject); projectSharingService.deleteSharedProject(idProject); + LOG.info("Deleting project {}", idProject); return projectDao.deleteProject(idProject); } @@ -256,9 +467,10 @@ public boolean deleteProject(Long idProject) { /** * Update the code block in the specified project * - * @param idProject - * @param code - * @return + * @param idProject is the primary key ID of the source project + * @param code is the code blocks to add to the new project + * + * @return a ProjectRecord representing the updated project */ @Override public ProjectRecord saveProjectCode(Long idProject, String code) { @@ -267,15 +479,25 @@ public ProjectRecord saveProjectCode(Long idProject, String code) { /** + * Create a new project, specifying a new project name and board type, based on an existing project * - * @param idProject - * @param code - * @param newName - * @param newBoard - * @return + * @param idProject the primary key ID of the source project + * + * @param code is the code blocks to add to the new project + * + * @param newName is the nae that will be assigned to the new project + * + * @param newBoard is the new board type that will be assigned to the new project + * + * @return a ProjectRecord representing the newly created project */ @Override - public ProjectRecord saveProjectCodeAs(Long idProject, String code, String newName, String newBoard) { + public ProjectRecord saveProjectCodeAs( + Long idProject, + String code, + String newName, + String newBoard) { + return projectDao.saveProjectCodeAs(idProject, code, newName, newBoard); } diff --git a/src/main/java/com/parallax/server/blocklyprop/utils/RestProjectUtils.java b/src/main/java/com/parallax/server/blocklyprop/utils/RestProjectUtils.java index 014dbe67..69e7f113 100644 --- a/src/main/java/com/parallax/server/blocklyprop/utils/RestProjectUtils.java +++ b/src/main/java/com/parallax/server/blocklyprop/utils/RestProjectUtils.java @@ -23,15 +23,23 @@ import com.parallax.server.blocklyprop.TableOrder; import com.parallax.server.blocklyprop.TableSort; +import com.parallax.server.blocklyprop.db.enums.ProjectType; -public class RestProjectUtils { +/** + * Project REST endpoint static utility methods + */ +public class RestProjectUtils { /** + * Validate that the provided value represents a valid column name on which to sort + * a list of projects + * + * @param sort is the column name to sort on. Note that not all project columns are + * sortable. * - * @param sort - * @return - * Return true if the provided sort is a valid item, otherwise return false + * @return a boolean true if the supplied value matches a sortable column, otherwise + * return a boolean false value */ public static boolean ValidateSortType(TableSort sort) { @@ -46,21 +54,45 @@ public static boolean ValidateSortType(TableSort sort) { return false; } + /** + * Validate the provided sort order. + * + * @param order Project sort order + * + * @return true if the sort order is valid, otherwise false + * + */ public static boolean ValidateSortOrder(TableOrder order) { - boolean parametersValid = false; - if (order != null) { for (TableOrder t : TableOrder.values()) { if (order == t) { - parametersValid = true; - break; + return true; } } } - return parametersValid; + return false; } -} + /** + * Validate the project language + * + * @param type is a ProjectType that indicates the underlying language used for the project + * + * @return true if the project type is a known type, otherwise return false + */ + public static boolean ValidateProjectType(ProjectType type) { + + if (type != null) { + for (ProjectType p : ProjectType.values()) { + if (type == p) { + return true; + } + } + } + + return false; + } +} diff --git a/src/main/resources/shiro.ini b/src/main/resources/shiro.ini index 23a16291..05c06763 100644 --- a/src/main/resources/shiro.ini +++ b/src/main/resources/shiro.ini @@ -157,7 +157,17 @@ shiro.loginUrl = /login.jsp # REST api and api documentation /apidoc = anon /rest/shared/** = anon, ssl -/rest/** = authc, ssl + +# Version 2 of the Blockly API +# /rest/v2/** = authc, ssl +/rest/v2/** = anon, ssl + +# Version 1 of the Blockly API +#/rest/** = authc, ssl +/rest/** = anon, ssl + + + # Authorized projects /createproject = authc, noSessionCreation, ssl