diff --git a/.gitignore b/.gitignore index 7c438576..bf6ce5ad 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,9 @@ nb* # /src/main/java/com/parallax/server/blocklyprop/db/generated/* # +*.sh + + ################# ## NetBeans ################# 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/pom.xml b/pom.xml index 6fc0a3f6..2656d85a 100644 --- a/pom.xml +++ b/pom.xml @@ -549,5 +549,12 @@ 4.12 test - + + + + org.jetbrains + annotations + 17.0.0 + + \ No newline at end of file diff --git a/src/main/java/com/parallax/server/blocklyprop/SessionData.java b/src/main/java/com/parallax/server/blocklyprop/SessionData.java index 978b0894..0ef95882 100644 --- a/src/main/java/com/parallax/server/blocklyprop/SessionData.java +++ b/src/main/java/com/parallax/server/blocklyprop/SessionData.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; import com.google.inject.servlet.SessionScoped; @@ -13,15 +29,21 @@ * User session details. * * This class contains the fields used to manage the client's session with - * the application. + * the application. The @SessionScoped decorator will enforce a single + * instance per session. * * @author Michel */ @SessionScoped public class SessionData implements Serializable { + // A cloud session user profile object private User user; + + // A cloud session user profile primary key ID private Long idUser; + + // A locale string for this user private String locale; /** @@ -54,6 +76,11 @@ public void setLocale(String locale) { this.locale = locale; } + /** + * Override the default toString method to enumerate all fields + * + * @return string representation of SessionData fields + */ @Override public String toString() { return "SessionData{" + "user=" + user + ", idUser=" + idUser + ", locale=" + locale + '}'; diff --git a/src/main/java/com/parallax/server/blocklyprop/TableOrder.java b/src/main/java/com/parallax/server/blocklyprop/TableOrder.java index bce5651d..3b597529 100644 --- a/src/main/java/com/parallax/server/blocklyprop/TableOrder.java +++ b/src/main/java/com/parallax/server/blocklyprop/TableOrder.java @@ -1,17 +1,32 @@ /* - * 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; /** + * Enumberate possible sort orders for project listings * * @author Michel */ public enum TableOrder { - asc, desc - } diff --git a/src/main/java/com/parallax/server/blocklyprop/TableSort.java b/src/main/java/com/parallax/server/blocklyprop/TableSort.java index c7991bbe..c4b4520e 100644 --- a/src/main/java/com/parallax/server/blocklyprop/TableSort.java +++ b/src/main/java/com/parallax/server/blocklyprop/TableSort.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; import com.parallax.server.blocklyprop.db.generated.Tables; @@ -10,6 +26,7 @@ import org.jooq.TableField; /** + * A list of the possible fields on which to sort project data * * @author Michel */ @@ -21,14 +38,14 @@ public enum TableSort { user(Tables.PROJECT.ID_USER), modified(Tables.PROJECT.MODIFIED); + // Map this enum to a Field in the JooQ ProjectRecord class private final TableField field; - private TableSort(TableField field) { + TableSort(TableField field) { this.field = field; } public TableField getField() { return field; } - } 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/ServletsModule.java b/src/main/java/com/parallax/server/blocklyprop/config/ServletsModule.java index 5ff2e753..66eca1ea 100644 --- a/src/main/java/com/parallax/server/blocklyprop/config/ServletsModule.java +++ b/src/main/java/com/parallax/server/blocklyprop/config/ServletsModule.java @@ -27,8 +27,6 @@ import com.parallax.server.blocklyprop.servlets.PrivacyPolicyServlet; import com.parallax.server.blocklyprop.servlets.ConfirmRequestServlet; import com.parallax.server.blocklyprop.servlets.ConfirmServlet; -import com.parallax.server.blocklyprop.servlets.HelpSearchServlet; -import com.parallax.server.blocklyprop.servlets.HelpServlet; import com.parallax.server.blocklyprop.servlets.NewOAuthUserServlet; import com.parallax.server.blocklyprop.servlets.OAuthGoogleServlet; import com.parallax.server.blocklyprop.servlets.PasswordResetRequestServlet; @@ -130,9 +128,6 @@ protected void configureServlets() { serve("/public/clientinstructions").with(TextileClientInstructionsServlet.class); serve("/public/changelog").with(TextileChangeLogServlet.class); - // Help - serve("/public/help").with(HelpServlet.class); - serve("/public/helpsearch").with(HelpSearchServlet.class); // OAuth serve("/oauth/newuser").with(NewOAuthUserServlet.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 f4e28f13..fd830cdb 100644 --- a/src/main/java/com/parallax/server/blocklyprop/config/SetupConfig.java +++ b/src/main/java/com/parallax/server/blocklyprop/config/SetupConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018 Parallax Inc. + * 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 @@ -26,12 +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 com.parallax.server.blocklyprop.utils.HelpFileInitializer; + 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,8 +40,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -// import java.sql.SQLException; -// import ch.qos.logback.classic.LoggerContext; /** @@ -50,16 +48,18 @@ */ 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(); @@ -68,12 +68,12 @@ 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(HelpFileInitializer.class).asEagerSingleton(); bind(Monitor.class).asEagerSingleton(); // Configure the backend data store @@ -85,7 +85,6 @@ protected void configure() { install(new ServletsModule()); install(new RestModule()); } - }); } @@ -123,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/converter/UserConverter.java b/src/main/java/com/parallax/server/blocklyprop/converter/UserConverter.java index 48a5f892..ad43b9de 100644 --- a/src/main/java/com/parallax/server/blocklyprop/converter/UserConverter.java +++ b/src/main/java/com/parallax/server/blocklyprop/converter/UserConverter.java @@ -10,6 +10,7 @@ import com.parallax.server.blocklyprop.db.generated.tables.records.UserRecord; /** + * Convert user object to a Json string * * @author Michel */ 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/ProjectSharingDao.java b/src/main/java/com/parallax/server/blocklyprop/db/dao/ProjectSharingDao.java index b613d88d..4d74e827 100644 --- a/src/main/java/com/parallax/server/blocklyprop/db/dao/ProjectSharingDao.java +++ b/src/main/java/com/parallax/server/blocklyprop/db/dao/ProjectSharingDao.java @@ -5,6 +5,7 @@ */ package com.parallax.server.blocklyprop.db.dao; +import com.parallax.server.blocklyprop.db.generated.tables.ProjectSharing; import com.parallax.server.blocklyprop.db.generated.tables.records.ProjectSharingRecord; import java.util.List; @@ -14,17 +15,64 @@ */ public interface ProjectSharingDao { + /** + * Retrieve a project + * @param idProject + * @param accessKey + * @return + */ ProjectSharingRecord getProject(Long idProject, String accessKey); + + /** + * Share an existing project + * @param idProject + * @param shareKey + * @return + */ ProjectSharingRecord shareProject(Long idProject, String shareKey); + + /** + * Disable the shared link to a project + * @param idProject + * @return + */ int revokeSharing(Long idProject); - public List getSharingInfo(Long idProject); + + /** + * Get a project sharing record + * @param idProject + * @return + */ + List getSharingInfo(Long idProject); // Set the active flag in an existing shared project record - public ProjectSharingRecord activateProject(Long idProject); + + /** + * Enable the project sharing link + * + * @param idProject + * @return + */ + ProjectSharingRecord activateProject(Long idProject); // Remove a project sharing link record - public boolean deleteProjectSharingRecord(Long idProject); + + /** + * Delete a project sharing record + * + * @param idProject + * @return + */ + boolean deleteProjectSharingRecord(Long idProject); + + + /** + * Is the project sharing feature enabled for a project + * @param idProject + * @return + */ + boolean isProjectSharingActive(Long idProject); } diff --git a/src/main/java/com/parallax/server/blocklyprop/db/dao/UserDao.java b/src/main/java/com/parallax/server/blocklyprop/db/dao/UserDao.java index e199c047..779246b9 100644 --- a/src/main/java/com/parallax/server/blocklyprop/db/dao/UserDao.java +++ b/src/main/java/com/parallax/server/blocklyprop/db/dao/UserDao.java @@ -18,9 +18,24 @@ public interface UserDao { @Deprecated UserRecord create(Long idCloudSession); - + + /** + * Update the blockly user screen name + * + * @param idUser - is the long integer id for the blockly user record + * @param screenName - is ghe new screen name to store in the user record + */ + void updateScreenName(Long idUser, String screenName); + + /** + * + * @param idCloudSession + * @param screenName + * @return + */ UserRecord create(Long idCloudSession, String screenName); + /** * Retrieve a BP user record * @@ -38,16 +53,34 @@ public interface UserDao { */ UserRecord getUser(Long idCloudSession, String screenName); - + /** + * + * @return + */ List getAll(); + /** + * + * @param idUser + * @param roles + */ void setRoles(Long idUser, Set roles); + /** + * + * @param idUser + * @return + */ List getRoles(Long idUser); - Long getUserIdForCloudSessionUserId(Long id); - - @Deprecated - public void updateScreenname(Long idUser, String screenname); - + /** + * Obtain the blocklyprop user ID from the supplied cloud session user id + * + * @param idCloudSession + * The user profile ID + * + * @return + * Returns a Long integer blocklyprop user ID if successful, otherwise returns zero + */ + Long getUserIdForCloudSessionUserId(Long idCloudSession); } 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 2dbbaf5f..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; @@ -13,10 +28,14 @@ import com.parallax.server.blocklyprop.db.generated.tables.records.ProjectRecord; import com.parallax.server.blocklyprop.security.BlocklyPropSecurityUtils; +//import com.parallax.server.blocklyprop.services.impl.ProjectSharingServiceImpl; +import com.parallax.server.blocklyprop.services.ProjectSharingService; + import com.google.inject.Inject; import com.google.inject.Singleton; import java.util.GregorianCalendar; import java.util.List; + import org.apache.shiro.authz.UnauthorizedException; import org.jooq.Condition; import org.jooq.DSLContext; @@ -28,7 +47,7 @@ /** * DAO interface to the blocklyprop.project table. - * + * * @author Michel * */ @@ -41,38 +60,46 @@ 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; } - + + private ProjectSharingService projectSharingService; + + @Inject + public void setProjectSharingContext(ProjectSharingService projectSharingService) { + this.projectSharingService = projectSharingService; + } + + /** * * Retrieve a new project record based from an existing project. @@ -86,12 +113,12 @@ public void setDSLContext(DSLContext dsl) { * * @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; @@ -105,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. @@ -134,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 @@ -159,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()); @@ -205,7 +326,7 @@ record = create LOG.error("Error Message: {}", ex.getMessage()); return null; } - + return record; } @@ -236,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); } @@ -341,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 * @@ -358,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; } @@ -386,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(); } @@ -412,17 +579,18 @@ public List getUserProjects( * @param order * @param limit * @param offset - * @param idUser * @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) { @@ -431,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) @@ -450,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(); @@ -480,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; } /** @@ -497,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. @@ -538,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); } @@ -552,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) { @@ -563,24 +736,49 @@ public boolean deleteProject(Long idProject) { } /** - * TODO: add details. + * Update the code block in the specified project * * @param idProject * @param code + * * @return + * Returns the specified project record, otherwise it returns a null if + * the current user does not own the project and the project is not shared + * or public, or the requested project record was not found. + * + * @implNote This method will actually create a new project record based on the + * existing project under specific conditions. Since this is an update record method, + * the creation of a new project my be unexpected at higher layers of the application. */ @Override public ProjectRecord updateProjectCode(Long idProject, String code) { LOG.info("Update code for project {}.", idProject); + + // Retrieve the specified project ProjectRecord record = create.selectFrom(Tables.PROJECT) .where(Tables.PROJECT.ID.equal(idProject)) .fetchOne(); + // Get a timestamp used to update the modified field of the project record GregorianCalendar cal = new GregorianCalendar(); cal.setTime(new java.util.Date()); if (record != null) { + // Found the project. Verify that the current user owns it Long idUser = BlocklyPropSecurityUtils.getCurrentUserId(); + + // TODO: Detecting a zero user id + if (idUser == 0) { + LOG.error("Detected current user ID is zero for project {}", idProject); + return null; + } + + if (record.getIdUser() == 0) { + LOG.error("Detected project user ID is zero for project {}", idProject); + return null; + } + + // Update the project if the current user owns it if (record.getIdUser().equals(idUser)) { record.setCode(code); record.setModified(cal); @@ -588,14 +786,18 @@ public ProjectRecord updateProjectCode(Long idProject, String code) { record.update(); return record; } else { + // If the project is a shared project, allow the current user + // to clone the project into their library if (record.getShared()) { ProjectRecord cloned = doProjectClone(record); cloned.setCode(code); cloned.setModified(cal); cloned.setCodeBlockVersion(BLOCKLY_LIBRARY_VERSION); + cloned.setIdUser(idUser); // The logged in user owns this copy of the project cloned.update(); return cloned; } + LOG.error("User {} tried and failed to update project {}.", idUser, idProject); throw new UnauthorizedException(); } @@ -605,6 +807,8 @@ public ProjectRecord updateProjectCode(Long idProject, String code) { } } + + /** * Save the current project as a new project * @@ -616,29 +820,40 @@ public ProjectRecord updateProjectCode(Long idProject, String code) { */ @Override public ProjectRecord saveProjectCodeAs(Long idProject, String code, String newName, String newBoard) { - - LOG.info("Saving project code as '{}'", newName); + + LOG.info("Saving project code from project {} as '{}'", idProject, newName); // Retreive the source project ProjectRecord original = getProject(idProject); + if (original == null) { LOG.error("Original project {} is missing. Unable to save code as...", idProject); throw new NullPointerException("Project doesn't exist"); - } else if (newBoard == null) { + } + + // Use the board type from the parent project if it was not provided + if (newBoard == null) { 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 // shared or community project // -------------------------------------------------------------------- - if (original.getIdUser().equals(idUser) || original.getShared()) { + 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 + ProjectRecord cloned = createProject( - newName, + newName, original.getDescription(), original.getDescriptionHtml(), code, @@ -648,7 +863,13 @@ public ProjectRecord saveProjectCodeAs(Long idProject, String code, String newNa false, // Set project unshared original.getId()); + if (cloned == null) { + LOG.warn("Unable to create a copy of the project."); + } return cloned; + } else { + LOG.warn("Unable to copy the project. UID: {}, PUID: {}, Shared: {}", + idUser, original.getIdUser(), original.getShared()); } } else { LOG.info("Unable to retreive BP user id"); @@ -658,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 @@ -689,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(), @@ -700,34 +921,35 @@ private ProjectRecord doProjectClone(ProjectRecord original) { original.getBoard(), original.getPrivate(), original.getShared(), - original.getId() + 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 @@ -736,7 +958,7 @@ private GregorianCalendar getCurrentTimestamp() { // horribly wrong with the string conversions. // private ProjectRecord alterReadRecord(ProjectRecord record) { - + String currentCode, newCode; @@ -750,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 @@ -762,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; @@ -787,7 +1009,7 @@ private ProjectRecord alterReadRecord(ProjectRecord record) { return record; } - /** + /** * * Create a random string to use as a blockID. * @@ -829,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; @@ -929,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\""); @@ -962,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/ProjectSharingDaoImpl.java b/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/ProjectSharingDaoImpl.java index 4a0804a7..b962e71c 100644 --- a/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/ProjectSharingDaoImpl.java +++ b/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/ProjectSharingDaoImpl.java @@ -7,9 +7,11 @@ import com.google.inject.Inject; import com.google.inject.Singleton; + import com.parallax.server.blocklyprop.db.dao.ProjectSharingDao; import com.parallax.server.blocklyprop.db.generated.Tables; import com.parallax.server.blocklyprop.db.generated.tables.records.ProjectSharingRecord; + import java.util.HashSet; import java.util.List; import java.util.Set; @@ -61,7 +63,8 @@ public ProjectSharingRecord getProject(Long idProject, String accessKey) { /** - * + * Create a project sharing record + * * @param idProject * @param shareKey * @return @@ -178,4 +181,32 @@ public boolean deleteProjectSharingRecord(Long idProject) { } + + /** + * Determine the on/off state of the project's shared link URL + * + * @param idProject + * @return + */ + @Override + public boolean isProjectSharingActive(Long idProject) { + + LOG.info("Retrieving sharing record for project {}", idProject); + + ProjectSharingRecord project = create + .selectFrom(Tables.PROJECT_SHARING) + .where((Tables.PROJECT_SHARING.ID_PROJECT + .equal(idProject))) + .fetchOne(); + + if (project == null) { + LOG.info("The sharing record for project {} was not found", idProject); + // Record not found + return false; + } + + LOG.info("Project {} sharing is {}", idProject, project.getActive()); + return project.getActive(); + } + } 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 42bf8faa..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 @@ -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 @@ /** + * This is the session database layer. * * @author Michel */ @@ -32,37 +49,43 @@ 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.info("Create a session. Timeout set to: {}", session.getTimeout()); + LOG.debug("Create a session. Timeout set to: {}", session.getTimeout()); // Log session details if the configuration file permits it printSessionInfo("create", session); @@ -91,47 +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"); - SessionRecord sessionRecord = null; - + LOG.debug("Getting session {} details", idSession); + 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()); - } - finally { - return sessionRecord; + throw new NullPointerException("Session not found"); } } /** - * - * @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()); @@ -148,30 +177,44 @@ 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).where(Tables.SESSION.IDSESSION.eq(idSession)).execute(); + + create.deleteFrom(Tables.SESSION) + .where(Tables.SESSION.IDSESSION.eq(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 { @@ -183,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 ef0e7814..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 */ @@ -125,18 +142,29 @@ public UserRecord create(Long idCloudSession, String screenName) { return record; } - @Override public List getAll() { - return create.selectFrom(Tables.USER).fetch(); + return create + .selectFrom(Tables.USER) + .fetch(); } + /** + * Get a Blockly UserRecord + * + * @param idUser + * Long integer ID of the user record to retrieve + * + * @return + * Returns a UserRecord object if successful, otherwise returns a null + */ @Override public UserRecord getUser(Long idUser) { return create .selectFrom(Tables.USER) - .where(Tables.USER.ID.equal(idUser)) + .where(Tables.USER.ID + .equal(idUser)) .fetchOne(); } @@ -145,8 +173,10 @@ public UserRecord getUser(Long idCloudSession, String screenName) { // Obtain the BP user id from the CS user id return create .selectFrom(Tables.USER) - .where(Tables.USER.IDCLOUDSESSION.eq(idCloudSession)) - .and(Tables.USER.SCREENNAME.eq(screenName)) + .where(Tables.USER.IDCLOUDSESSION + .eq(idCloudSession)) + .and(Tables.USER.SCREENNAME + .eq(screenName)) .fetchOne(); } @@ -167,30 +197,45 @@ public void setRoles(Long idUser, Set roles) { if (!roles.contains(roleRecord.getName())) { create .delete(Tables.SEC_USER_ROLE) - .where(Tables.SEC_USER_ROLE.ID_USER.equal(idUser)) - .and(Tables.SEC_USER_ROLE.ID_ROLE.equal(roleRecord.getId())) + .where(Tables.SEC_USER_ROLE.ID_USER + .equal(idUser)) + .and(Tables.SEC_USER_ROLE.ID_ROLE + .equal(roleRecord.getId())) .execute(); } } for (Role role : roles) { if (!currentAssignedRoles.getValues(Tables.SEC_ROLE.NAME).contains(role)) { - Long idRole = create.select(Tables.SEC_ROLE.ID).from(Tables.SEC_ROLE).where(Tables.SEC_ROLE.NAME.equal(role)).fetchOne(Tables.SEC_ROLE.ID); + Long idRole = create + .select(Tables.SEC_ROLE.ID) + .from(Tables.SEC_ROLE) + .where(Tables.SEC_ROLE.NAME.equal(role)) + .fetchOne(Tables.SEC_ROLE.ID); + if (idRole == null || idRole == 0) { SecRoleRecord roleRecord = createRole(role); idRole = roleRecord.getId(); } - create.insertInto(Tables.SEC_USER_ROLE, Tables.SEC_USER_ROLE.ID_USER, Tables.SEC_USER_ROLE.ID_ROLE) - .values(idUser, idRole).execute(); + create.insertInto(Tables.SEC_USER_ROLE, Tables.SEC_USER_ROLE.ID_USER, Tables.SEC_USER_ROLE.ID_ROLE) + .values(idUser, idRole) + .execute(); } } } private Result getRawRoles(Long idUser) { - Result currentAssignedRoles = create.select(Tables.SEC_ROLE.ID, Tables.SEC_ROLE.NAME).from(Tables.SEC_ROLE) - .join(Tables.SEC_USER_ROLE).on(Tables.SEC_USER_ROLE.ID_ROLE.equal(Tables.SEC_ROLE.ID)) - .where(Tables.SEC_USER_ROLE.ID_USER.equal(idUser)).fetch().into(Tables.SEC_ROLE); + Result currentAssignedRoles = create + .select(Tables.SEC_ROLE.ID, Tables.SEC_ROLE.NAME) + .from(Tables.SEC_ROLE) + .join(Tables.SEC_USER_ROLE) + .on(Tables.SEC_USER_ROLE.ID_ROLE + .equal(Tables.SEC_ROLE.ID)) + .where(Tables.SEC_USER_ROLE.ID_USER + .equal(idUser)) + .fetch() + .into(Tables.SEC_ROLE); return currentAssignedRoles; } @@ -201,8 +246,11 @@ public List getRoles(Long idUser) { } private SecRoleRecord createRole(Role role) { - SecRoleRecord record = create.insertInto(Tables.SEC_ROLE, Tables.SEC_ROLE.NAME) - .values(role).returning().fetchOne(); + SecRoleRecord record = create + .insertInto(Tables.SEC_ROLE, Tables.SEC_ROLE.NAME) + .values(role) + .returning() + .fetchOne(); return record; } @@ -216,14 +264,14 @@ private SecRoleRecord createRole(Role role) { * @return The BP user id */ @Override - @Deprecated public Long getUserIdForCloudSessionUserId(Long id) { // Obtain the BP user id from the CS user id Long idUser = create .select(Tables.USER.ID) .from(Tables.USER) - .where(Tables.USER.IDCLOUDSESSION.eq(id)) + .where(Tables.USER.IDCLOUDSESSION + .eq(id)) .fetchOneInto(Long.class); if (idUser == null) { @@ -234,23 +282,40 @@ public Long getUserIdForCloudSessionUserId(Long id) { } } + + /** + * Replace the blockly user screen name + * + * @param idUser - is the long integer id for the blockly user record + * @param screenName - is ghe new screen name to store in the user record + */ @Override - @Deprecated - public void updateScreenname(Long idUser, String screenname) { + public void updateScreenName(Long idUser, String screenName) { LOG.info("Attempting to update screen name for user: {} ", idUser); - - UserRecord user = create.selectFrom(Tables.USER) - .where(Tables.USER.ID.eq(idUser)) + + // Fetch the blockly user record + UserRecord user = create + .selectFrom(Tables.USER) + .where(Tables.USER.ID + .eq(idUser)) .fetchOne(); if (user != null) { - if ( ! Objects.equals(user.getScreenname(), screenname)) { - LOG.info("Changing screen name from {} to {}", user.getScreenname(), screenname); + // Compare the existing screen name with the proposed screen name + if ( ! Objects.equals(user.getScreenname(), screenName)) { + LOG.info("Changing screen name from {} to {}", user.getScreenname(), screenName); + + create.update(Tables.USER) + .set(Tables.USER.SCREENNAME, screenName) + .where(Tables.USER.ID.eq(idUser)) + .execute(); - user.setScreenname(screenname); - user.update(); + LOG.info("The screen name is now {}", user.getScreenname()); } } + else { + LOG.warn("Unable to locate a blockly user record for blockly id {}", idUser); + } } } 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/RestCompile.java b/src/main/java/com/parallax/server/blocklyprop/rest/RestCompile.java index 8a3c3ec5..ac04be78 100644 --- a/src/main/java/com/parallax/server/blocklyprop/rest/RestCompile.java +++ b/src/main/java/com/parallax/server/blocklyprop/rest/RestCompile.java @@ -18,28 +18,18 @@ import com.parallax.client.cloudcompiler.objects.CompilationResult; import com.parallax.client.cloudcompiler.objects.CompileAction; import com.parallax.client.cloudsession.CloudSessionBucketService; -import com.parallax.client.cloudsession.exceptions.EmailNotConfirmedException; -import com.parallax.client.cloudsession.exceptions.InsufficientBucketTokensException; -import com.parallax.client.cloudsession.exceptions.ServerException; -import com.parallax.client.cloudsession.exceptions.UnknownBucketTypeException; -import com.parallax.client.cloudsession.exceptions.UnknownUserIdException; -import com.parallax.client.cloudsession.exceptions.UserBlockedException; +import com.parallax.client.cloudsession.exceptions.*; import com.parallax.server.blocklyprop.rest.typewrappers.CompileActionTypeWrapper; import com.parallax.server.blocklyprop.services.impl.SecurityServiceImpl; -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.FormParam; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; import org.apache.commons.configuration.Configuration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.*; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + /** * * @author Michel @@ -72,12 +62,22 @@ public void setConfiguration(Configuration configuration) { @Name("compile") @Produces("text/plain") public Response get(@QueryParam("test") String testString) { - LOG.info("REST:/rest/compile/ Get request received"); - return Response.ok("Hello " + testString).build(); } +/* + @GET + @Detail("Get Simple Libraries version") + @Name("version") + @Produces("text/plain") + public Response version() { + LOG.info("REST:/rest/compile/ Get request received"); + return Response.ok("v0.0.0").build(); + } +*/ + + @POST @Path("/spin/{action}") @Detail("Spin compile") diff --git a/src/main/java/com/parallax/server/blocklyprop/rest/RestProfile.java b/src/main/java/com/parallax/server/blocklyprop/rest/RestProfile.java index aa065c82..c85ce814 100644 --- a/src/main/java/com/parallax/server/blocklyprop/rest/RestProfile.java +++ b/src/main/java/com/parallax/server/blocklyprop/rest/RestProfile.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.rest; import com.cuubez.visualizer.annotation.Detail; @@ -23,6 +39,9 @@ import com.parallax.client.cloudsession.exceptions.WrongAuthenticationSourceException; import com.parallax.client.cloudsession.exceptions.EmailNotConfirmedException; import com.parallax.client.cloudsession.objects.User; + +import com.parallax.server.blocklyprop.services.UserService; + import javax.ws.rs.FormParam; import javax.ws.rs.POST; import javax.ws.rs.Path; @@ -46,6 +65,7 @@ public class RestProfile { private CloudSessionLocalUserService cloudSessionLocalUserService; private CloudSessionUserService cloudSessionUserService; private Configuration configuration; + private UserService userService; @Inject public void setConfiguration(Configuration configuration) { @@ -54,6 +74,15 @@ public void setConfiguration(Configuration configuration) { cloudSessionUserService = new CloudSessionUserService(configuration.getString("cloudsession.baseurl")); } + /** + * Inject a user service object so we can update the screen name + * @param userService + */ + @Inject + public void setUserService(UserService userService) { + this.userService = userService; + } + @POST @Path("/base") @Detail("Save base profile data") @@ -68,14 +97,29 @@ public Response saveBase( LOG.info("REST:/rest/profile/base/ Post request received"); JsonObject result = new JsonObject(); + if (Strings.isNullOrEmpty(screenname)) { result.addProperty("success", false); - result.addProperty("message", "fields-missing"); + result.addProperty("message", "screen-name--missing"); return Response.ok(result.toString()).build(); } else { try { + // Contact the cloud session server to update the user profile User user = cloudSessionUserService.changeUserInfo(id, screenname); if (user != null) { + // The update was successful, update the screen name in the + // blocklyprop user table + // TODO: Update the screen name field in blocklyprop.user table + + Long idUser = userService.getIdUser(user.getId()); + if (idUser > 0) { + userService.setScreenName(idUser, screenname); + LOG.info("Screen name for {} has been changed to {}", username, screenname); + } + else { + LOG.warn("Unable to locate blockly user record for cloudSession id {}", user.getId()); + } + result.addProperty("success", true); result.addProperty("screenname", user.getScreenname()); return Response.ok(result.toString()).build(); 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 e0c73742..2cb71f1d 100644 --- a/src/main/java/com/parallax/server/blocklyprop/rest/RestProject.java +++ b/src/main/java/com/parallax/server/blocklyprop/rest/RestProject.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.rest; import com.cuubez.visualizer.annotation.Detail; @@ -11,16 +27,21 @@ 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; 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 java.util.Arrays; import java.util.List; import javax.ws.rs.FormParam; import javax.ws.rs.GET; @@ -30,7 +51,10 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; 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; @@ -39,52 +63,89 @@ * 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") @Group(name = "/project", title = "Project management") @HttpCode("500>Internal Server Error,200>Success Response") public class RestProject { + // 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 + + //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 + * + * @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 + * + * @param projectConverter + * An instance of the ProjectConverter object */ @Inject public void setProjectConverter(ProjectConverter projectConverter) { this.projectConverter = projectConverter; } + /** * 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 - * - * @return JSON formatted list of project details + * 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 */ @GET @Path("/list") - @Detail("Get all projects for the authenticated user") + @Detail("Retrieve a list of projects for the authenticated user") @Name("ListProjects") @Produces("application/json") public Response get( @@ -92,56 +153,89 @@ public Response get( @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) { - - LOG.info("REST:/rest/project/list/ Get request received"); - + + String endPoint = "REST:/rest/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(); - - if (idUser == 0) { + + // Return FORBIDDEN 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.NOT_FOUND).build(); + return Response.status(Response.Status.UNAUTHORIZED).build(); } //Sanity checks - is the request reasonable - if (sort == null) - sort = TableSort.modified; - - if (order == null) + + // 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; - - if (limit == null) - limit = 20; - - if (offset == null) + } + + // 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; + } - List userProjects = - projectService.getUserProjects(idUser, sort, order, limit, offset); - - int projectCount = projectService.countUserProjects(idUser); + // Obtain a list of the user's projects + List userProjects = projectService.getUserProjects(idUser, sort, order, limit, offset); - JsonObject result = new JsonObject(); - JsonArray jsonProjects = new JsonArray(); - for (ProjectRecord project : userProjects) { - jsonProjects.add(projectConverter.toListJson(project)); + // Tell the caller that there is nothing to see here + if (userProjects == null) { + return Response.status(Response.Status.NOT_FOUND).build(); } - result.add("rows", jsonProjects); - result.addProperty("total", projectCount); + LOG.info("Returning {} projects for user {}", userProjects.size(), idUser); - return Response.ok(result.toString()).build(); + 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()); return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); } - } + + /** + * 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 + */ @GET @Path("/get/{id}") @Detail("Get project by id") @@ -177,14 +271,18 @@ public Response get(@PathParam("id") @ParameterDetail("Project identifier") Long } } + /** * Update the code in an existing project. * * This assumes that the project already exists. * - * @param idProject - * @param code - * @return + * @param idProject The project key ID + * @param code the project blocks code string + * + * @return + * Returns a Json string containing the project details if the update was successful + * or an error message upon failure */ @POST @Path("/code") @@ -198,15 +296,18 @@ public Response saveProjectCode( LOG.info("REST:/rest/project/code/ POST request received for project '{}'", idProject); try { - ProjectRecord savedProject = projectService.saveProjectCode(idProject, code); - LOG.debug("Code for project {} has been saved", idProject); - JsonObject result = projectConverter.toJson(savedProject,false); - LOG.debug("Returning JSON: {}", result); - - result.addProperty("success", true); + /* WARNING: + * ================================================================================= + * 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); - return Response.ok(result.toString()).build(); + LOG.debug("Code for project {} has been saved", idProject); + return Response.ok(buildConvertedResponse(savedProject)).build(); } catch (AuthorizationException ae) { LOG.warn("Project code not saved. Not Authorized"); return Response.status(Response.Status.UNAUTHORIZED).build(); @@ -217,6 +318,19 @@ public Response saveProjectCode( } } + + /** + * Create a new project from an existing project + * + * @param idProject The project key ID + * @param code the project blocks code string + * @param newName the name to assign to the newly created project + * @param newBoard the board type assigned to the new project + * + * @return + * Returns a Json string containing the project details if the update was successful + * or an error message upon failure + */ @POST @Path("/code-as") @Detail("Save project code") @@ -238,26 +352,44 @@ public Response saveProjectCodeAs( code, newName, newBoard); + LOG.debug("Code for project {} has been saved as {}", idProject, newName); - +/* JsonObject result = projectConverter.toJson(savedProject,false); LOG.debug("Returning JSON: {}", result); - result.addProperty("success", true); - return Response.ok(result.toString()).build(); - } catch (AuthorizationException ae) { +*/ + return Response.ok(buildConvertedResponse(savedProject)).build(); + } + catch (AuthorizationException ae) { LOG.warn("Project code not saved. Not Authorized"); return Response.status(Response.Status.UNAUTHORIZED).build(); } catch (Exception ex) { LOG.error("General exception encountered. Message is: ", ex.getMessage()); + LOG.error("Error: {}", Arrays.toString(ex.getStackTrace())); return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); } } + + /** + * Update the details of an existing project + * + * @param idProject the project key ID + * @param name the name assigned to the project + * @param description a text description of the project + * @param descriptionHtml the same project description expressed in HTML + * @param projectSharing a boolean flag indicating the public accessibility of the project + * @param type is the classification of the project's language (c or spin) + * @param board is the type of hardware associated with the project + * + * @return + * Returns a Json string containing the project details if the update was successful + * or an error message upon failure + */ @POST - @Path("/") @Detail("Save project") @Name("Save project") @Produces("application/json") @@ -308,4 +440,58 @@ public Response saveProject( 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(); + + 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); + } + + 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(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(); + } + } diff --git a/src/main/java/com/parallax/server/blocklyprop/rest/RestSharedProject.java b/src/main/java/com/parallax/server/blocklyprop/rest/RestSharedProject.java index e053fa9f..c757d8cd 100644 --- a/src/main/java/com/parallax/server/blocklyprop/rest/RestSharedProject.java +++ b/src/main/java/com/parallax/server/blocklyprop/rest/RestSharedProject.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018 Parallax Inc. + * 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 @@ -16,7 +16,7 @@ * 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. + * SOFTWARE. */ package com.parallax.server.blocklyprop.rest; @@ -28,11 +28,14 @@ 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; import com.parallax.server.blocklyprop.converter.ProjectConverter; import com.parallax.server.blocklyprop.db.generated.tables.records.ProjectRecord; import com.parallax.server.blocklyprop.services.ProjectService; + import java.util.List; import javax.ws.rs.GET; import javax.ws.rs.HeaderParam; @@ -42,35 +45,74 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** + * Manage requests for public projects * * @author Michel + * + * NOTE: + * The concept of 'shared' projects has changed over time. A project + * can be private or public. A project can also be associated with a + * specific project sharing URL, regardless of its public/private status. */ @Path("/shared/project") @Group(name = "/shared/project", title = "Project management") @HttpCode("500>Internal Server Error,200>Success Response") public class RestSharedProject { + /** + * Get a connection to the logging system + */ private static final Logger LOG = LoggerFactory.getLogger(RestSharedProject.class); + + /** + * Get a handle to project services + */ private ProjectService projectService; + + /** + * Get a handle to a project converter + */ private ProjectConverter projectConverter; + + /** + * Limit the number of records that can be returned in list functions + */ + final int REQUEST_LIMIT = 100; + + + /** + * Inject project services + * + * @param projectService + * An instance of the ProjectService object + */ @Inject public void setProjectService(ProjectService projectService) { this.projectService = projectService; } + + /** + * Inject project conversion services + * @param projectConverter + * + * An instance of the ProjectConverter object + */ @Inject public void setProjectConverter(ProjectConverter projectConverter) { this.projectConverter = projectConverter; } + /** * Return a list of community projects. * @@ -101,76 +143,68 @@ public Response get( @QueryParam("limit") Integer limit, @QueryParam("offset") Integer offset) { - LOG.info("REST:/shared/project/list/ endpoint activated"); - LOG.debug("REST:/shared/project/list/ Sort parameter is '{}'", sort); - LOG.debug("REST:/shared/project/list/ Sort parameter is '{}'", sort); - boolean parametersValid = false; - + String endPoint = "REST:/shared/project/list/"; + LOG.info("{} endpoint activated", endPoint); + // Sort flag evaluation - if (sort != null) { - for (TableSort t : TableSort.values()) { - LOG.debug("REST:/shared/project/list/ Sort test for '{}'", t); - - if (sort == t) { - parametersValid = true; - break; - } - } - - if (parametersValid == false) { - LOG.warn("REST:/shared/project/list/ Sort parameter failed"); - return Response.status(Response.Status.NOT_ACCEPTABLE).build(); - } + if (!RestProjectUtils.ValidateSortType(sort)) { + LOG.warn("{} Sort parameter failed. Defaulting to sort by project name", endPoint); + sort = TableSort.name; } // Sort order evaluation - if (order != null) { - parametersValid = false; - LOG.debug("REST:/shared/project/list/ Checking order"); - - for (TableOrder t : TableOrder.values()) { - if (order == t) { - parametersValid = true; - break; - } - } - - if (parametersValid == false) { - return Response.status(Response.Status.NOT_ACCEPTABLE).build(); - } + 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 > 50)) { - LOG.info("REST:/shared/project/list/ Limit throttle to 50 entries"); - limit = 50; + if ( (limit == null) || (limit > REQUEST_LIMIT)) { + LOG.info("{} Limit throttle to {} entries", endPoint, REQUEST_LIMIT); + limit = REQUEST_LIMIT; } // Check ofset from the beginning of the record set if ((offset == null) || (offset < 0)) { offset = 0; } - - List projects - = projectService.getSharedProjects(sort, order, limit, offset); - - // Obtain a count of the total number of community projects available - int projectCount = projectService.countSharedProjects(); - JsonObject result = new JsonObject(); - JsonArray jsonProjects = new JsonArray(); - - for (ProjectRecord project : projects) { - jsonProjects.add(projectConverter.toListJson(project)); - } + // Get a block of projects + List projects = projectService.getSharedProjects(sort, order, limit, offset); - result.add("rows", jsonProjects); - result.addProperty("total", projectCount); + // Tell the caller that there is nothing to see here + if (projects == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } - return Response.ok(result.toString()).build(); + return Response.ok( + returnProjectsJson( + projects, + projectService.countSharedProjects())) + .build(); } + + /** + * Get a list of projects owned by a specific 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 + */ @GET @Path("/list/user/{id}") @Detail("Get shared projects by user") @@ -183,25 +217,63 @@ public Response get( @QueryParam("offset") Integer offset, @PathParam("id") Long idUser) { - LOG.info("REST:/shared/project/list/user/ Get request received for user '{}'", idUser); + RestProjectUtils restProjectUtils = new RestProjectUtils(); - List projects = projectService.getSharedProjectsByUser(sort, order, limit, offset, idUser); - int projectCount = projectService.countSharedProjectsByUser(idUser); + String endPoint = "REST:/shared/project/list/user/"; + LOG.info("{} Get request received for user '{}'", endPoint, idUser); - JsonObject result = new JsonObject(); - JsonArray jsonProjects = new JsonArray(); - - for (ProjectRecord project : projects) { - jsonProjects.add(projectConverter.toListJson(project)); + // Sort flag evaluation + if (!restProjectUtils.ValidateSortType(sort)) { + LOG.warn("{} Sort parameter failed", endPoint); + return Response.status(Response.Status.NOT_ACCEPTABLE).build(); } - result.add("rows", jsonProjects); - result.addProperty("total", projectCount); + // Sort order evaluation + if (!restProjectUtils.ValidateSortOrder(order)) { + LOG.warn("{} Sort order parameter failed", endPoint); + return Response.status(Response.Status.NOT_ACCEPTABLE).build(); + } + + // Limit result set value + if ( (limit == null) || (limit > REQUEST_LIMIT)) { + LOG.info("{} Limit throttle to {} entries", endPoint, REQUEST_LIMIT); + limit = REQUEST_LIMIT; + } + + // Check ofset from the beginning of the record set + if ((offset == null) || (offset < 0)) { + offset = 0; + } + + List projects = projectService.getSharedProjectsByUser(sort, order, limit, offset, idUser); - return Response.ok(result.toString()).build(); + // Tell the caller that there is nothing to see here + if (projects == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + + return Response.ok( + returnProjectsJson( + projects, + projectService.countSharedProjectsByUser(idUser))) + .build(); } + /** + * + * @param authorization + * Authorization header token + * + * @param timestamp + * A timestamp + * + * @param idProject + * The project key ID + * + * @return + * Returns a Json string containing the project details + */ @GET @Path("/get/{id}") @Detail("Get project by id") @@ -212,10 +284,10 @@ public Response get( @HeaderParam("X-Timestamp") Long timestamp, @PathParam("id") Long idProject) { - LOG.info("REST:/rest/shared/project/get/ Get request received for projecet '{}'", idProject); + String endPoint = "REST:/rest/shared/project/get/"; + LOG.info("{} Get request received for project '{}'", endPoint, idProject); try { - LOG.info("Getting project record."); ProjectRecord project = projectService.getProject(idProject); if (project == null) { @@ -223,9 +295,11 @@ public Response get( return Response.status(Response.Status.NOT_FOUND).build(); } - LOG.info("Converting project to JSON string"); + LOG.info("Converting project {} to JSON string", idProject); + JsonObject result = projectConverter.toJson(project, false); - LOG.info("REST: /get/" + idProject.toString() + "/ returning project {}.", project.getId()); + + LOG.info("{}" + idProject.toString() + "/ returning project {}.", endPoint, project.getId()); return Response.ok(result.toString()).build(); } @@ -235,6 +309,22 @@ public Response get( } } + + /** + * Get project details, including the project code payload + * + * @param authorization + * Request authorization header + * + * @param timestamp + * A timestamp + * + * @param idProject + * The project key ID + * + * @return + * A string containing a Json object representing the requested project + */ @GET @Path("/editor/{id}") @Detail("Get project by id for editor") @@ -267,4 +357,32 @@ public Response getEditor( } } + + /** + * 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(); + } + } 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 4d7656aa..ed35eb13 100644 --- a/src/main/java/com/parallax/server/blocklyprop/rest/RestUser.java +++ b/src/main/java/com/parallax/server/blocklyprop/rest/RestUser.java @@ -1,17 +1,35 @@ /* - * 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.rest; import com.cuubez.visualizer.annotation.Detail; import com.cuubez.visualizer.annotation.Group; import com.cuubez.visualizer.annotation.HttpCode; import com.cuubez.visualizer.annotation.Name; + import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.inject.Inject; + import com.parallax.server.blocklyprop.converter.UserConverter; import com.parallax.server.blocklyprop.db.generated.tables.pojos.User; import com.parallax.server.blocklyprop.db.generated.tables.records.UserRecord; @@ -27,6 +45,7 @@ /** + * Respond to REST /user endpoint requests * * @author Michel */ @@ -35,9 +54,9 @@ @HttpCode("500>Internal Server Error,200>Success Response") public class RestUser { + // Logger handle private static final Logger LOG = LoggerFactory.getLogger(RestUser.class); - private UserService userService; @Inject @@ -45,20 +64,29 @@ public void setUserService(UserService userService) { this.userService = userService; } + + /** + * List of all user objects + * + * @return + * Returns a list of all user objects. + */ @GET - @Path("/") +// @Path("/") @Detail("Get all users") @Name("Get all users") @Produces("application/json") public Response get() { + //FixMe: Endpoint /rest/user/ returns a list of ALL users. This needs to be regulated. LOG.info("REST:/rest/user/ Get request received"); - + JsonArray result = new JsonArray(); List users = userService.getAllUsers(); - JsonArray result = new JsonArray(); - for (UserRecord user : users) { - result.add(UserConverter.toJson(user)); + if (users != null) { + for (UserRecord user : users) { + result.add(UserConverter.toJson(user)); + } } return Response.ok(result.toString()).build(); 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/security/BlocklyPropSecurityUtils.java b/src/main/java/com/parallax/server/blocklyprop/security/BlocklyPropSecurityUtils.java index a17d1e27..17538a5b 100644 --- a/src/main/java/com/parallax/server/blocklyprop/security/BlocklyPropSecurityUtils.java +++ b/src/main/java/com/parallax/server/blocklyprop/security/BlocklyPropSecurityUtils.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.security; import com.parallax.client.cloudsession.objects.User; @@ -10,17 +26,36 @@ import com.parallax.server.blocklyprop.services.impl.SecurityServiceImpl; import org.apache.shiro.SecurityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * * @author Michel */ public class BlocklyPropSecurityUtils extends SecurityUtils { + // Get a logger instance + private static final Logger LOG = LoggerFactory.getLogger(BlocklyPropSecurityUtils.class); + + /** + * + * @return + */ public static Long getCurrentUserId() { - SessionData sessionData = SecurityServiceImpl.getSessionData(); - if (sessionData != null) { - return sessionData.getIdUser(); + + try { + SessionData sessionData = SecurityServiceImpl.getSessionData(); + if (sessionData != null) { + return sessionData.getIdUser(); + } } + catch (Exception ex) { + LOG.info("Exception trapped. Message is: {}.", ex.getMessage()); + } + + LOG.info("Session data not found"); + return null; } diff --git a/src/main/java/com/parallax/server/blocklyprop/security/BlocklyPropSessionDao.java b/src/main/java/com/parallax/server/blocklyprop/security/BlocklyPropSessionDao.java index 87282b2f..84eec6ac 100644 --- a/src/main/java/com/parallax/server/blocklyprop/security/BlocklyPropSessionDao.java +++ b/src/main/java/com/parallax/server/blocklyprop/security/BlocklyPropSessionDao.java @@ -7,18 +7,17 @@ import com.parallax.server.blocklyprop.db.generated.tables.records.SessionRecord; import com.parallax.server.blocklyprop.services.impl.SessionServiceImpl; + import java.io.Serializable; import java.sql.Timestamp; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.UUID; +import java.util.*; + import org.apache.commons.lang.SerializationUtils; import org.apache.shiro.session.Session; import org.apache.shiro.session.UnknownSessionException; import org.apache.shiro.session.mgt.SimpleSession; import org.apache.shiro.session.mgt.eis.SessionDAO; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,50 +25,101 @@ * Session persistence manager * * @author Michel + * + * @implNote + * Notes from the Shiro JavaDocs: + * Data Access Object design pattern specification to enable Session access to an EIS + * (Enterprise Information System). It provides your four typical CRUD methods: + * create(org.apache.shiro.session.Session), + * readSession(java.io.Serializable), + * update(org.apache.shiro.session.Session), + * and delete(org.apache.shiro.session.Session). + * + * The remaining getActiveSessions() method exists as a support mechanism to pre-emptively + * orphaned sessions, typically by ValidatingSessionManagers), and should be as efficient + * as possible, especially if there are thousands of active sessions. Large scale/high + * performance implementations will often return a subset of the total active sessions and + * perform validation a little more frequently, rather than return a massive set and + * infrequently validate. */ public class BlocklyPropSessionDao implements SessionDAO { - // Get a logger instance + + /** + * Get an instance of the logger initialized to this class + */ private static final Logger LOG = LoggerFactory.getLogger(SessionServiceImpl.class); /** - * + * Inserts a new Session record into the underling EIS (a relational database in this + * implementation). + * * @param session - * @return + * the Session object to create in the EIS. + * + * @return + * the EIS id (e.g. primary key) of the created Session object. + * + * @implNote + * After this method is invoked, the Session.getId() method executed on the argument + * must return a valid session identifier. That is, the following should always be + * true: + * + * Serializable id = create( session ); + * id.equals( session.getId() ) == true + * + * Implementations are free to throw any exceptions that might occur due to integrity + * violation constraints or other EIS related errors. */ @Override public Serializable create(Session session) { - LOG.debug("Create BlocklyProp session"); + LOG.trace("Create BlocklyProp session"); // Set session timeout for 8 hours session.setTimeout(28800000); SimpleSession simpleSession = (SimpleSession) session; + + // Create a unique string and save into the session object String uuid = UUID.randomUUID().toString(); - simpleSession.setId(uuid); + + // Get a reference to the static session service and create + // a session record from the session object and store it in the + // sessionDao backing store SessionServiceImpl.getSessionService().create(convert(simpleSession)); - LOG.debug("Session timeout is: {}", simpleSession.getTimeout()); LOG.info("Creating session: {}", simpleSession.getId()); + // Return a unique session identifier return uuid; } /** - * + * Retrieves the session from the EIS uniquely identified by the specified sessionId. + * * @param sessionId + * the system-wide unique identifier of the Session object to retrieve from the EIS. + * * @return - * @throws UnknownSessionException + * the persisted session in the EIS identified by sessionId. + * + * @throws UnknownSessionException + * if there is no EIS record for any session with the specified sessionId */ @Override public Session readSession(Serializable sessionId) throws UnknownSessionException { - LOG.debug("Reading session: {}", sessionId); + + LOG.trace("Reading session: {}", sessionId); + + // Check parameter for sanity + if (sessionId == null) { + LOG.warn("Attempt to retrieve session with a null UUID parameter"); + throw new UnknownSessionException(); + } try { // Obtain an existing session object SessionRecord sessionRecord - = SessionServiceImpl - .getSessionService() - .readSession(sessionId.toString()); + = SessionServiceImpl.getSessionService().readSession(sessionId.toString()); if (sessionRecord != null) { return convert(sessionRecord); @@ -84,13 +134,26 @@ public Session readSession(Serializable sessionId) throws UnknownSessionExceptio } /** - * + * Updates (persists) data from a previously created Session instance in the EIS identified + * by {@link Session#getId() session.getId()}. This effectively propagates the data in the + * argument to the EIS record previously saved. + * * @param session - * @throws UnknownSessionException + * session - the Session to update + * + * @throws UnknownSessionException + * if no existing EIS session record exists with the identifier of session.getSessionId() + * + * @implNote + * In addition to UnknownSessionException, implementations are free to throw any other + * exceptions that might occur due to integrity violation constraints or other EIS related + * errors. */ @Override public void update(Session session) throws UnknownSessionException { - LOG.debug("Update session: {}", session.getId()); + + LOG.trace("Update session: {}", session.getId()); + try { // updateSession() can throw a NullPointerException if something goes wrong SessionServiceImpl.getSessionService().updateSession(convert(session)); @@ -102,55 +165,116 @@ public void update(Session session) throws UnknownSessionException { } /** - * - * @param session + * Deletes the associated EIS record of the specified session. If there never existed a + * session EIS record with the identifier of session.getId(), then this method does nothing. + * + * @param session + * session - the session to delete. */ @Override public void delete(Session session) { - LOG.debug("Removing session {}", session.getId()); + + LOG.trace("Removing session {}", session.getId()); + SessionServiceImpl.getSessionService().deleteSession(session.getId().toString()); } /** - * - * @return + * Returns all sessions in the EIS that are considered active, meaning all sessions that + * have not been stopped or expired. This is primarily used to validate potential orphans. + * + * @return + * a Collection of Sessions that are considered active, or an empty collection or null if + * there are no active sessions. + * + * @implNote + * This method should be as efficient as possible, especially in larger systems where there + * might be thousands of active sessions. Large scale/high performance implementations will + * often return a subset of the total active sessions and perform validation a little more + * frequently, rather than return a massive set and validate infrequently. If efficient and + * possible, it would make sense to return the oldest unstopped sessions available, ordered + * by lastAccessTime. + * + * Ideally this method would only return active sessions that the EIS was certain should be + * invalided. Typically that is any session that is not stopped and where its + * lastAccessTimestamp is older than the session timeout. For example, if sessions were + * backed by a relational database or SQL-92 'query-able' enterprise cache, you might return + * something similar to the results returned by this query (assuming SimpleSessions were + * being stored): + * + * select * + * from sessions s + * where s.lastAccessTimestamp < ? and s.stopTimestamp is null + * + * where the ? parameter is a date instance equal to 'now' minus the session timeout + * (e.g. now - 30 minutes). */ @Override public Collection getActiveSessions() { - LOG.debug("Getting all active sessions"); + + LOG.trace("Getting all active sessions"); Collection sessionRecords = SessionServiceImpl.getSessionService().getActiveSessions(); - List sessions = new ArrayList(); + List sessions = new ArrayList<>(); + for (SessionRecord sessionRecord : sessionRecords) { sessions.add(convert(sessionRecord)); } + return sessions; } - protected SessionRecord convert(Session session) { - LOG.debug("Converting session {} to a SessionRecord object", session.getId()); - + + /** + * Convert a Session object into a SessionRecord object + * + * @param session + * the session to convert into a SessionRecord + * + * @return + * a SessionRecord object containing the details necessary to persist the object + * into an EIS. + */ + private SessionRecord convert(Session session) { + LOG.trace("Converting session {} to a SessionRecord object", session.getId()); + + // Cast the Session parameter into a SimpleSession reference SimpleSession ssession = (SimpleSession) session; + SessionRecord sessionRecord = new SessionRecord(); sessionRecord.setIdsession(session.getId().toString()); sessionRecord.setStarttimestamp(new Timestamp(session.getStartTimestamp().getTime())); sessionRecord.setLastaccesstime(new Timestamp(session.getLastAccessTime().getTime())); sessionRecord.setTimeout(session.getTimeout()); sessionRecord.setHost(session.getHost()); + + // Gather the session attributes into a HashMap that can be persisted into the + // SessionRecord object if (ssession.getAttributes() != null) { HashMap attributes = (HashMap) ssession.getAttributes(); + + // Logging attributes + // LOG.debug("Session attributes:"); + // attributes.forEach( (k,v) -> LOG.debug("Key: {}, Value: {}", k, v)); + sessionRecord.setAttributes(SerializationUtils.serialize(attributes)); } + return sessionRecord; } /** - * + * Concert a SessionRecord object to a Session object + * * @param sessionRecord - * @return + * the SessionRecord object to convert + * + * @return + * a Session object. The session object attributes may be missing if the original + * SessionRecord object contained non-string data. */ - protected Session convert(SessionRecord sessionRecord) { - LOG.debug("Converting SessionRecord {} into a SimpleSession object", sessionRecord.getIdsession()); + private Session convert(SessionRecord sessionRecord) { + LOG.trace("Converting SessionRecord {} into a SimpleSession object", sessionRecord.getIdsession()); SimpleSession ssession = new SimpleSession(); ssession.setId(sessionRecord.getIdsession()); @@ -158,14 +282,24 @@ protected Session convert(SessionRecord sessionRecord) { ssession.setLastAccessTime(sessionRecord.getLastaccesstime()); ssession.setTimeout(sessionRecord.getTimeout()); ssession.setHost(sessionRecord.getHost()); - + + // Gather the session attributes into a HashMap that can be persisted into the + // Session object if (sessionRecord.getAttributes() != null) { - HashMap attributes - = (HashMap) - SerializationUtils.deserialize(sessionRecord.getAttributes()); - - ssession.setAttributes(attributes); + // In case there is something in the session attributes that isn't a string value + // We can trap the issue here and deal with it. The @SuppressWarnings tells the IDE + // that we have thought about this and taken appropriate defensive measures. + try { + @SuppressWarnings("unchecked") + HashMap attributes + = (HashMap) SerializationUtils.deserialize(sessionRecord.getAttributes()); + ssession.setAttributes(attributes); + } + catch (ClassCastException ex) { + LOG.warn("Unable to convert SessionRecord attributes in session {}", sessionRecord.getIdsession() ); + } } + return ssession; } diff --git a/src/main/java/com/parallax/server/blocklyprop/security/CloudSessionAuthenticationRealm.java b/src/main/java/com/parallax/server/blocklyprop/security/CloudSessionAuthenticationRealm.java index c4d242b4..7a4e4db6 100644 --- a/src/main/java/com/parallax/server/blocklyprop/security/CloudSessionAuthenticationRealm.java +++ b/src/main/java/com/parallax/server/blocklyprop/security/CloudSessionAuthenticationRealm.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018 Parallax Inc. + * 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 @@ -66,11 +66,10 @@ */ public class CloudSessionAuthenticationRealm extends AuthorizingRealm { - /** - * Class logging handle - */ + // Get a handle to a logger for this class private static Logger LOG = LoggerFactory.getLogger(CloudSessionAuthenticationRealm.class); + /** * Convenience implementation that returns * getAuthenticationTokenClass().isAssignableFrom( token.getClass() ); @@ -90,6 +89,7 @@ public boolean supports(AuthenticationToken token) { return true; } + /** * Retrieves the AuthorizationInfo for the given principals from the * underlying data store. @@ -112,6 +112,7 @@ protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal return authorizationInfo; } + /** * Retrieves authentication data from an implementation-specific data source * (RDBMS, LDAP, etc) for the given authentication token. @@ -141,12 +142,11 @@ protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { - /* - * Any leading and/or trailing white space contained in the credentials + LOG.info("Obtaining authentication info"); + + /* Any leading and/or trailing white space contained in the credentials * (password) has been stripped out before it gets here. */ - LOG.info("Obtaining authentication info"); - try { if (token instanceof OAuthToken) { // Principal = email @@ -157,7 +157,6 @@ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) token.getCredentials(), "CloudSession"); } else { - LOG.info("Authentication is using local login authority"); // Principal = login @@ -166,6 +165,8 @@ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) // Credentials = password String credentials = new String((char[]) token.getCredentials()); + LOG.info("Authenticating user '{}'", principal); + // Thia can throw a NullPointerException User user = SecurityServiceImpl.authenticateLocalUserStatic( principal, @@ -176,6 +177,8 @@ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) return null; } + LOG.info("User {} is authenticated", principal); + try { return new SimpleAccount( token.getPrincipal(), @@ -185,32 +188,34 @@ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) LOG.error("Unexpected exception creating account object", t); } } - return null; - } catch (UnknownUserException ex) { + throw new AuthenticationException("Unable to authenticate token"); + } + catch (UnknownUserException ex) { LOG.warn("Authentication failed. Message: {}", ex.getMessage()); throw new AuthenticationException(ex.getMessage()); - - } catch (UserBlockedException ex) { + } + catch (UserBlockedException ex) { LOG.warn("Blocked user {}", ex); throw new AuthenticationException(ex.getMessage()); - - } catch (EmailNotConfirmedException ex) { + } + catch (EmailNotConfirmedException ex) { LOG.warn("Authentication failed. Message: {}", ex.getMessage()); throw new AuthenticationException("EmailNotConfirmed"); - - } catch (InsufficientBucketTokensException ex) { + } + catch (InsufficientBucketTokensException ex) { LOG.info("Insufficient bucket tokens: {}", ex.getMessage()); throw new AuthenticationException(ex.getMessage()); - - } catch (NullPointerException npe) { + } + catch (NullPointerException npe) { LOG.warn("NullPointer", npe); throw new AuthenticationException(npe.getMessage()); - - } catch (Throwable t) { + } + catch (Throwable t) { // This is a catchall exception handler that kicks the can back // to the caller LOG.warn("Throwable", t); } + return null; } diff --git a/src/main/java/com/parallax/server/blocklyprop/security/CloudSessionCredentialsMatcher.java b/src/main/java/com/parallax/server/blocklyprop/security/CloudSessionCredentialsMatcher.java index eb9e6aaa..13ef2ba6 100644 --- a/src/main/java/com/parallax/server/blocklyprop/security/CloudSessionCredentialsMatcher.java +++ b/src/main/java/com/parallax/server/blocklyprop/security/CloudSessionCredentialsMatcher.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.security; import java.util.Arrays; @@ -10,14 +26,29 @@ import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.credential.SimpleCredentialsMatcher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + /** * * @author Michel */ public class CloudSessionCredentialsMatcher extends SimpleCredentialsMatcher { + // Get a handle to a logger for this class + private static Logger LOG = LoggerFactory.getLogger(CloudSessionCredentialsMatcher.class); + + /** + * + * @param tokenCredentials + * @param accountCredentials + * @return + */ @Override protected boolean equals(Object tokenCredentials, Object accountCredentials) { + LOG.info("Testing for equivalent credentials"); + if (isByteSource(tokenCredentials) && isByteSource(accountCredentials)) { byte[] tokenBytes = toBytes(tokenCredentials); byte[] accountBytes = toBytes(accountCredentials); @@ -27,11 +58,17 @@ protected boolean equals(Object tokenCredentials, Object accountCredentials) { } } + /** + * + * @param token + * @param info + * @return + */ @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { + LOG.info("Testing auth token against auth information"); Object tokenCredentials = getCredentials(token); Object accountCredentials = getCredentials(info); return equals(tokenCredentials, accountCredentials); } - } diff --git a/src/main/java/com/parallax/server/blocklyprop/services/AuthenticationService.java b/src/main/java/com/parallax/server/blocklyprop/services/AuthenticationService.java index 2d5fbfbc..61b4faab 100644 --- a/src/main/java/com/parallax/server/blocklyprop/services/AuthenticationService.java +++ b/src/main/java/com/parallax/server/blocklyprop/services/AuthenticationService.java @@ -1,18 +1,43 @@ /* - * 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.client.cloudsession.objects.User; /** + * Interface for user authentication * * @author Michel */ public interface AuthenticationService { - public User authenticate(String username, String password); + /** + * Process a user authentication request + * + * @param username - user email address + * @param password - password submitted in the request + * + * @return - a user profile object if successful, otherwise return a null + */ + User authenticate(String username, String password); } 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/ProjectSharingService.java b/src/main/java/com/parallax/server/blocklyprop/services/ProjectSharingService.java index de0e017c..19d4ced0 100644 --- a/src/main/java/com/parallax/server/blocklyprop/services/ProjectSharingService.java +++ b/src/main/java/com/parallax/server/blocklyprop/services/ProjectSharingService.java @@ -28,6 +28,9 @@ public interface ProjectSharingService { ProjectRecord getSharedProject(Long idProject, String shareKey); // Delete a project sharing record - public boolean deleteSharedProject(Long idProject); + boolean deleteSharedProject(Long idProject); + + // Get current active state of a project share link + boolean isProjectShared(Long idProject); } diff --git a/src/main/java/com/parallax/server/blocklyprop/services/SessionService.java b/src/main/java/com/parallax/server/blocklyprop/services/SessionService.java index b61606a6..dd70a989 100644 --- a/src/main/java/com/parallax/server/blocklyprop/services/SessionService.java +++ b/src/main/java/com/parallax/server/blocklyprop/services/SessionService.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.db.generated.tables.records.SessionRecord; @@ -23,5 +39,4 @@ public interface SessionService { void deleteSession(String idSession); Collection getActiveSessions(); - } diff --git a/src/main/java/com/parallax/server/blocklyprop/services/UserService.java b/src/main/java/com/parallax/server/blocklyprop/services/UserService.java index 8a975e48..78f58d7a 100644 --- a/src/main/java/com/parallax/server/blocklyprop/services/UserService.java +++ b/src/main/java/com/parallax/server/blocklyprop/services/UserService.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.db.generated.tables.pojos.User; @@ -16,12 +32,27 @@ public interface UserService { User getUser(Long idUser); - + + @Deprecated User getUser(Long idCloudSessionUser, String screenName); + /** + * Get the blockly user id from the user profile cloud session id + * + * @param idCloudSession user profile primary key + * + * @return the blockly user id + */ + Long getIdUser(Long idCloudSession); + + + List getAllUsers(); - public String getUserScreenName(Long idUser); + String getUserScreenName(Long idUser); + + void setScreenName(Long idUser, String screenName); + void setLocale(String locale); diff --git a/src/main/java/com/parallax/server/blocklyprop/services/impl/AuthenticationServiceImpl.java b/src/main/java/com/parallax/server/blocklyprop/services/impl/AuthenticationServiceImpl.java index 56bfb8ec..c7d44811 100644 --- a/src/main/java/com/parallax/server/blocklyprop/services/impl/AuthenticationServiceImpl.java +++ b/src/main/java/com/parallax/server/blocklyprop/services/impl/AuthenticationServiceImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018 Parallax Inc. + * 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 @@ -25,18 +25,22 @@ import com.google.inject.Provider; import com.google.inject.Singleton; import com.google.inject.persist.Transactional; + import com.parallax.client.cloudsession.CloudSessionUserService; import com.parallax.client.cloudsession.exceptions.ServerException; import com.parallax.client.cloudsession.exceptions.UnknownUserException; import com.parallax.client.cloudsession.objects.User; import com.parallax.server.blocklyprop.services.AuthenticationService; import com.parallax.server.blocklyprop.services.TokenGeneratorService; + import javax.servlet.http.HttpSession; import org.apache.commons.configuration.Configuration; + import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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 0cf372c8..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,21 +455,49 @@ 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); } + + /** + * Update the code block in the specified project + * + * @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) { return projectDao.updateProjectCode(idProject, code); } + + /** + * Create a new project, specifying a new project name and board type, based on an existing project + * + * @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/services/impl/ProjectSharingServiceImpl.java b/src/main/java/com/parallax/server/blocklyprop/services/impl/ProjectSharingServiceImpl.java index 16de708c..a6e7ec32 100644 --- a/src/main/java/com/parallax/server/blocklyprop/services/impl/ProjectSharingServiceImpl.java +++ b/src/main/java/com/parallax/server/blocklyprop/services/impl/ProjectSharingServiceImpl.java @@ -32,16 +32,34 @@ public class ProjectSharingServiceImpl implements ProjectSharingService { */ private static final Logger LOG = LoggerFactory.getLogger(ProjectSharingService.class); + + /** + * + */ private ProjectDao projectDao; - private ProjectSharingDao projectSharingDao; - // Inject dao connection to the project table + + /** + * Inject dao connection to the project table + * @param projectDao + */ @Inject public void setProjectDao(ProjectDao projectDao) { this.projectDao = projectDao; } - // Inject connection to the project_sharing table + + /** + * + */ + private ProjectSharingDao projectSharingDao; + + + /** + * Inject connection to the project_sharing table + * + * @param projectSharingDao + */ @Inject public void setProjectSharingDao(ProjectSharingDao projectSharingDao) { this.projectSharingDao = projectSharingDao; @@ -154,4 +172,16 @@ public boolean deleteSharedProject(Long idProject) { LOG.info("Deleting project share link for project {}", idProject); return projectSharingDao.deleteProjectSharingRecord(idProject); } + + + /** + * + * @param idProject + * @return + */ + @Override + public boolean isProjectShared(Long idProject) { + LOG.info("Evaluating project {} sharing status.", idProject); + return projectSharingDao.isProjectSharingActive(idProject); + } } diff --git a/src/main/java/com/parallax/server/blocklyprop/services/impl/SecurityServiceImpl.java b/src/main/java/com/parallax/server/blocklyprop/services/impl/SecurityServiceImpl.java index 1a939325..04a34c5d 100644 --- a/src/main/java/com/parallax/server/blocklyprop/services/impl/SecurityServiceImpl.java +++ b/src/main/java/com/parallax/server/blocklyprop/services/impl/SecurityServiceImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018 Parallax Inc. + * 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 @@ -21,16 +21,10 @@ package com.parallax.server.blocklyprop.services.impl; -import com.google.common.base.Preconditions; -import com.google.common.base.Strings; -import com.google.inject.Inject; -import com.google.inject.Provider; -import com.google.inject.Singleton; -import com.google.inject.persist.Transactional; - import com.parallax.client.cloudsession.CloudSessionAuthenticateService; import com.parallax.client.cloudsession.CloudSessionRegisterService; import com.parallax.client.cloudsession.CloudSessionUserService; +import com.parallax.client.cloudsession.objects.User; import com.parallax.client.cloudsession.exceptions.EmailNotConfirmedException; import com.parallax.client.cloudsession.exceptions.InsufficientBucketTokensException; import com.parallax.client.cloudsession.exceptions.NonUniqueEmailException; @@ -42,19 +36,25 @@ import com.parallax.client.cloudsession.exceptions.UnknownUserIdException; import com.parallax.client.cloudsession.exceptions.UserBlockedException; import com.parallax.client.cloudsession.exceptions.WrongAuthenticationSourceException; -import com.parallax.client.cloudsession.objects.User; - import com.parallax.server.blocklyprop.SessionData; -import com.parallax.server.blocklyprop.db.dao.UserDao; import com.parallax.server.blocklyprop.services.SecurityService; - +import com.parallax.server.blocklyprop.services.SessionService; +import com.parallax.server.blocklyprop.db.dao.UserDao; import com.parallax.server.blocklyprop.db.generated.tables.records.UserRecord; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import com.google.inject.persist.Transactional; import java.util.Calendar; -import java.util.Set; + + import org.apache.commons.configuration.Configuration; import org.apache.commons.validator.routines.EmailValidator; import org.apache.shiro.SecurityUtils; +// import org.apache.shiro.session.Session; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -67,56 +67,51 @@ @Transactional public class SecurityServiceImpl implements SecurityService { - /** - * Handle to logging facility - */ + // Handle to logging facility for the SecurityServiceImpl class private static final Logger LOG = LoggerFactory.getLogger(SecurityServiceImpl.class); - - /** - * - */ - private static SecurityServiceImpl instance; - /** - * Web client session details - */ - private Provider sessionData; + // A static instance of this object + private static SecurityServiceImpl instance; - /** - * Application configuration settings - */ + // Application configuration settings private Configuration configuration; - - /** - * - */ - private EmailValidator emailValidator = EmailValidator.getInstance(); - /** - * Interface to the Cloud Session user account registration service - */ + // Interface to the Cloud Session user account registration service private CloudSessionRegisterService registerService; - - /** - * Interface to the Cloud Session user authentication service - */ + + // Interface to the Cloud Session user authentication service private CloudSessionAuthenticateService authenticateService; - - /** - * Interface to the Cloud Session user account/profile services - */ + + // Interface to the Cloud Session user account/profile services private CloudSessionUserService userService; + // Injects an instance of SessionData here + private Provider sessionData; + + // Access to the BlocklyProp user details + private UserDao userDao; + + private SessionService sessionService; + + @Inject + public void setSessionService(SessionService sessionService) { + this.sessionService = sessionService; + } + /** - * Access to the BlocklyProp user details + * Get a static instance of the EmailValidator object. + * + * @implNote + * The default configuration for the validator is to not allow local and TLDs */ - private UserDao userDao; - + private EmailValidator emailValidator = EmailValidator.getInstance(); + + /** * Constructor - * */ public SecurityServiceImpl() { + // // TODO: Correct the 'this' construct in the constructor // // Notes from: https://www.securecoding.cert.org/confluence/display/java/TSM01-J.+Do+not+let+the+this+reference+escape+during+object+construction @@ -127,38 +122,46 @@ public SecurityServiceImpl() { instance = this; } + /** - * Set the session's data provider + * Implements the Providers setSessionDataProvider interface * * This is a callback used by the Shiro package to provide a connection * between the application and the Shiro session management services. * - * @param sessionDataProvider + * @param sessionDataProvider + * is a class that models the session data + * */ @Inject public void setSessionDataProvider(Provider sessionDataProvider) { this.sessionData = sessionDataProvider; } + /** * Set the session's user database object in the blocklyprop system. * - * @param userDao + * @param userDao + * is the DAO interface to User data instance store */ @Inject public void setUserDao(UserDao userDao) { this.userDao = userDao; } + /** * Configure cloud session service endpoints * - * @param configuration + * @param configuration + * A application configuration object */ - @Inject public void setConfiguration(Configuration configuration) { + LOG.debug("Setting cloud session configuration"); + this.configuration = configuration; // Set the source for the cloud session registration services @@ -177,6 +180,7 @@ public void setConfiguration(Configuration configuration) { configuration.getString("cloudsession.baseurl")); } + /** * Validate new user data and create a new user account * @@ -194,12 +198,23 @@ public void setConfiguration(Configuration configuration) { * @param birthYear int Year component of the user's birthday. COPPA field * @param parentEmail String sponsor email address. COPPA field * @param parentEmailSource int Sponsor classification. COPPA - * @return + * + * @return The cloud session user ID if successful or zero upon failure + * * @throws NonUniqueEmailException + * A user account with the provided email address already exists + * * @throws PasswordVerifyException + * The two password values provided do not match + * * @throws PasswordComplexityException + * The provided password does not meet the requirements for a secure password + * * @throws ScreennameUsedException - * @throws IllegalStateException + * A user account with the provided screen name already exists + * + * @throws IllegalStateException + * */ @Override public Long register( @@ -217,71 +232,60 @@ public Long register( ScreennameUsedException, IllegalStateException{ + LOG.info("Registering a new user: {}({})", email, screenname); + + // Instantiate a new cloud session user profile object User user = new User(); - // Log a few things - LOG.debug("In register: parameter screen name: {}", screenname); - LOG.debug("In register: parameter email: {}", email); - LOG.debug("In register: parameter month: {}", birthMonth); - LOG.debug("In register: parameter year: {}", birthYear); - LOG.debug("In register: parameter sponsor email: {}", parentEmail); - LOG.debug("In register: parameter sponsor type selection: {}", parentEmailSource); - // Perform basic sanity checks on inputs // Throws NullPointerException if screenname is null - LOG.debug("Resgistering new user: {}", screenname); Preconditions.checkNotNull(screenname, "ScreenNameNull"); // User email address is required and must be reasonably valid - LOG.debug("Verifying email address has been supplied"); Preconditions.checkNotNull(email, "UserEmailNull"); - - LOG.debug("Verifying email address is reasonable"); Preconditions.checkState( emailValidator.isValid(email), "Email address format is incorrect"); - LOG.debug("Verifying that a password was provided"); + // The password fields must contain something Preconditions.checkNotNull(password, "PasswordIsNull"); - - LOG.debug("Verify that second copy of password was provided"); Preconditions.checkNotNull(passwordConfirm, "PasswordConfirmIsNull"); - + // Verify that we have valid COPPA data before continuing // Birth month Preconditions.checkNotNull(birthMonth, "BirthMonthNull"); - LOG.debug("Verify that month is provided: {}", birthMonth); Preconditions.checkState((birthMonth != 0), "BirthMonthNotSet"); // Birth year Preconditions.checkNotNull(birthYear, "BirthYearNull"); - LOG.debug("Verify that year is provided: {}", birthYear); Preconditions.checkState( (Calendar.getInstance().get(Calendar.YEAR) != birthYear), "BirthYearNotSet"); // Get additional information if the registrant is under 13 years old if (user.isCoppaEligible(birthMonth, birthYear)) { - LOG.debug("User is subject to COPPA regulations"); - // We must have a sponsor email address for COPPA eligible users Preconditions.checkNotNull( parentEmail, "SponsorEmailNull"); - + // Verify that the sponsor email address is reasonable if (parentEmail != null && parentEmail.length() > 0) { - LOG.debug("Verify that optional user email address is reasonable"); Preconditions.checkState( - emailValidator.isValid(parentEmail), - "SponsorEmail"); + emailValidator.isValid(parentEmail), + "SponsorEmail"); } + + LOG.info("User is COPPA restricted"); + LOG.info("Sponsor email address is: {}", parentEmail); } + /* ------------------------------------------------------------------ + * Attempt to register the user account data with the cloud session + * service. If successful, the method call will return a cloud + * session user id for the newly created account + * -----------------------------------------------------------------*/ try { - // Attempt to register the user account data with the cloud session - // service. If successful, the method call will return a cloud - // session user id for the newly created account LOG.info("Registering user account with cloud-service"); Long idCloudSessionUser = registerService.registerUser( email, @@ -296,6 +300,7 @@ public Long register( // Create a BlocklyProp user account record if (idCloudSessionUser > 0) { + LOG.info("Creating matching blocklyprop user record for {}", screenname); userDao.create(idCloudSessionUser, screenname); } @@ -341,7 +346,7 @@ public static User authenticateLocalUserStatic( } /** - * Authenticate a user from the provided userID + * Get an instance of an authenticated user object * * @param idUser * @@ -389,7 +394,6 @@ public User authenticateLocalUser(String email, String password) throws // Query Cloud Session interface User user = authenticateService.authenticateLocalUser(email, password); - LOG.info("User authenticated"); return user; } catch (UnknownUserException uue) { @@ -412,7 +416,7 @@ public User authenticateLocalUser(String email, String password) throws throw wase; } catch (NullPointerException npe) { - LOG.error("Authetication threw Null Pointer Exception"); + LOG.error("Authentication threw Null Pointer Exception"); throw npe; } catch (ServerException se) { @@ -442,7 +446,9 @@ public User authenticateLocalUser(Long idUser) throws UnknownUserIdException, UserBlockedException, EmailNotConfirmedException { - + + // FixMe: UserBlockledException is never thrown in client.cloudsession. + try { User user = userService.getUser(idUser); LOG.info("User authenticated"); @@ -461,20 +467,26 @@ public User authenticateLocalUser(Long idUser) throws * Return user session data * * @return SessionData object containing user session details or null + * + * @implNote + * The SessionData object stores three attributes: + * user - A cloud session user profile object + * idUser - the blocklprop user primary key ID + * locale - the locale string used for this session */ public static SessionData getSessionData() { LOG.debug("Getting user session data"); - + SessionData sessionData = instance.sessionData.get(); - + if (sessionData == null) { LOG.warn("Error obtaining session data"); + return null; } - - LOG.debug("Session data - {}", sessionData.toString()); - + // Check for a BP user id if (sessionData.getIdUser() == null) { + LOG.debug("No user ID is associated with the current session"); // No BP user id found, is the user in this session authenticated? if (SecurityUtils.getSubject().isAuthenticated()) { @@ -482,59 +494,87 @@ public static SessionData getSessionData() { // The user identified by this session is authenticated. Perform // a fun exercise to locate the BP user id for this authenticated // user. - LOG.debug("Session data missing a valid BP id for an authenticated user"); + LOG.debug("Obtaining session data for authenticated user"); try { // Getting a user record using the account email address String principal = (String) SecurityUtils.getSubject().getPrincipal(); // Display the user's email address - LOG.debug("Getting pricipal: {}", principal ); + LOG.debug("Principal is: {}", principal ); // Get the user account/profile record + String emailAddress = (String) SecurityUtils.getSubject().getPrincipal(); + LOG.debug("Getting user profile for {}", emailAddress); + + // Retrieve a blocky user record using an email address User user = instance.userService.getUser( (String) SecurityUtils.getSubject().getPrincipal()); - + // Did we get a user account object if (user != null) { - LOG.debug("Session User: {}", user.getScreenname()); - LOG.debug("Session UserId: {}", user.getId()); - LOG.debug("Session locale: {}", user.getLocale()); - + LOG.debug("User Profile: {}({}), ID: {}", + user.getEmail(), + user.getScreenname(), + user.getId()); + LOG.debug("Session Locale is: {}",sessionData.getLocale()); + // Yes, User account local may have changed if (!Strings.isNullOrEmpty(sessionData.getLocale())) { if (!sessionData.getLocale().equals(user.getLocale())) { try { // User locale changed. Let's update the user // account with new locale - LOG.info("Changing user {} locale", user.getScreenname()); + LOG.debug("Changing user {} locale", user.getScreenname()); user = instance.userService.changeUserLocale( user.getId(), sessionData.getLocale()); + } catch (UnknownUserIdException ex) { LOG.error("UnknownUserId exception detected. {}", ex.getMessage()); } } } - - LOG.debug("Setting session user data for {}", user.getScreenname()); - sessionData.setUser(user); - - LOG.debug("Getting BP user id"); - UserRecord bpUser = instance.userDao.getUser(user.getId(), user.getScreenname()); + + // Store the user profile into the session + sessionData.setUser(user); + + LOG.debug("Checking {}", sessionData.getUser().getScreenname()); + + // Getting the blocklyprop user record + Long idBlocklyUser = instance.userDao.getUserIdForCloudSessionUserId(user.getId()); + + LOG.debug("Obtained BlocklyProp user id {} from cloud session id {} ", + idBlocklyUser, + user.getId() ); + + UserRecord bpUser = instance.userDao.getUser(idBlocklyUser); + if (bpUser != null) { - LOG.debug("Setting BP user id to: {}", bpUser.getId()); - sessionData.setIdUser(bpUser.getId()); + LOG.debug("Retrieved blockly user record: bpID: {}, csID: {}, Name: {}", + bpUser.getId(), + bpUser.getIdcloudsession(), + bpUser.getScreenname() ); + + + // Verify that the screen name matches in both databases + if (! bpUser.getScreenname().equals(user.getScreenname())) { + LOG.info("Updating bp screen name from {} to {}", + bpUser.getScreenname(), + user.getScreenname()); + + instance.userDao.updateScreenName( + bpUser.getId(), + user.getScreenname()); + } }else{ LOG.warn("Warning! Setting BP user id to zero"); sessionData.setIdUser(0L); } - /* - * This should never be necessary until the user profile page - * offers the capability to change the user's screen name - */ -// instance.userDao.updateScreenname( -// sessionData.getIdUser(), -// user.getScreenname()); + sessionData.setIdUser(idBlocklyUser); + sessionData.setLocale(user.getLocale()); + + + //TODO: Persist the updated sessionData } } catch (UnknownUserException ex) { LOG.error("Unknown user ID. {}", ex); @@ -543,6 +583,8 @@ public static SessionData getSessionData() { } } } + + LOG.debug("Returning session data"); return sessionData; } } diff --git a/src/main/java/com/parallax/server/blocklyprop/services/impl/SessionServiceImpl.java b/src/main/java/com/parallax/server/blocklyprop/services/impl/SessionServiceImpl.java index caaab48f..45ead890 100644 --- a/src/main/java/com/parallax/server/blocklyprop/services/impl/SessionServiceImpl.java +++ b/src/main/java/com/parallax/server/blocklyprop/services/impl/SessionServiceImpl.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.impl; import com.google.inject.Inject; @@ -11,7 +27,6 @@ import com.parallax.server.blocklyprop.db.dao.SessionDao; import com.parallax.server.blocklyprop.db.generated.tables.records.SessionRecord; import com.parallax.server.blocklyprop.services.SessionService; -//import java.util.Arrays; import java.util.Collection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,7 +45,7 @@ public class SessionServiceImpl implements SessionService { // Retain session state private static SessionService sessionService; - // Session database acceess object + // Session database access object private SessionDao sessionDao; public SessionServiceImpl() { @@ -44,8 +59,8 @@ public void setSessionDao(SessionDao sessionDao) { @Override public void create(SessionRecord session) { - log.info("Creating a new user session with timeout set to {}", session.getTimeout()); - + log.debug("Creating a new user session"); + //TODO: Verify session attributes element has data when saving. sessionDao.create(session); } @@ -77,10 +92,10 @@ public Collection getActiveSessions() { return sessionDao.getActiveSessions(); } + public static SessionService getSessionService() { log.debug("Get current session service instance"); return sessionService; } - } diff --git a/src/main/java/com/parallax/server/blocklyprop/services/impl/UserServiceImpl.java b/src/main/java/com/parallax/server/blocklyprop/services/impl/UserServiceImpl.java index bcb0dffa..c3198dd1 100644 --- a/src/main/java/com/parallax/server/blocklyprop/services/impl/UserServiceImpl.java +++ b/src/main/java/com/parallax/server/blocklyprop/services/impl/UserServiceImpl.java @@ -1,21 +1,40 @@ /* - * 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.impl; import com.google.inject.Inject; import com.google.inject.Singleton; import com.google.inject.persist.Transactional; + import com.parallax.client.cloudsession.CloudSessionUserService; import com.parallax.client.cloudsession.exceptions.ServerException; import com.parallax.client.cloudsession.exceptions.UnknownUserIdException; + import com.parallax.server.blocklyprop.db.dao.UserDao; import com.parallax.server.blocklyprop.db.generated.tables.pojos.User; import com.parallax.server.blocklyprop.db.generated.tables.records.UserRecord; import com.parallax.server.blocklyprop.security.BlocklyPropSecurityUtils; import com.parallax.server.blocklyprop.services.UserService; + import java.util.List; import org.apache.commons.configuration.Configuration; import org.slf4j.Logger; @@ -33,26 +52,47 @@ public class UserServiceImpl implements UserService { private static UserService USER_SERVICE; - private Configuration configuration; - private UserDao userDao; + /** + * User profile + */ private CloudSessionUserService userService; public UserServiceImpl() { UserServiceImpl.USER_SERVICE = this; } + /** + * BlocklyProp user record + */ + private UserDao userDao; + @Inject public void setUserDao(UserDao userDao) { this.userDao = userDao; } + /** + * Application configuration object + */ + private Configuration configuration; + @Inject public void setConfiguration(Configuration configuration) { this.configuration = configuration; userService = new CloudSessionUserService(configuration.getString("cloudsession.baseurl")); } + + /** + * Locate a blocklyprop user record from the bp user ID + * + * @param idUser + * The blocklyprop user ID + * + * @return + * A populated blocklyprop user instance + */ @Override public User getUser(Long idUser) { if (userDao != null) { @@ -62,9 +102,34 @@ public User getUser(Long idUser) { LOG.error("UserDAO is not initialized before first use!"); return null; } - } - + + + /** + * Look up the blockly user id from the user profile cloud session id + * + * @param idCloudSession + * The user profile key id + * + * @return + * Returns a Long integer representing the blockly record primary key id + */ + public Long getIdUser(Long idCloudSession) { + return userDao.getUserIdForCloudSessionUserId(idCloudSession); + } + + + /** + * Locate a blocklyprop user record from the user profile ID and screen name + * + * @param idCloudSessionUser + * + * @param screenName + * + * @return + * + */ + @Deprecated @Override public User getUser(Long idCloudSessionUser, String screenName) { if (userDao != null) { @@ -76,6 +141,13 @@ public User getUser(Long idCloudSessionUser, String screenName) { } } + + /** + * Get a list of blocklyprop user objects + * + * @return + * Returns a list of blocklyprop user objects + */ @Override public List getAllUsers() { if (userDao != null) { @@ -87,6 +159,17 @@ public List getAllUsers() { } } + + /** + * Get the blocklyprop user screen name + * + * @param idUser + * Provide the blocklyprop user ID + * + * @return + * Returns a string containing the user's screen name or an empty string + * if the blocklyprop user record was not found + */ @Override public String getUserScreenName(Long idUser) { @@ -101,21 +184,31 @@ public String getUserScreenName(Long idUser) { } } catch (NullPointerException ex) { - LOG.error("Error retreiving name for userID: {}", idUser); + LOG.error("Error retrieving name for userID: {}", idUser); } return name; } @Override public void setLocale(String locale) { + LOG.info("Setting locale {}", locale); + if (SecurityServiceImpl.getSessionData() != null) { + LOG.info("Retrieved SessionData object"); + LOG.info("SessionData {}",SecurityServiceImpl.getSessionData().toString() ); + if (SecurityServiceImpl.getSessionData().getUser() != null) { + LOG.info("Retrieved SessionData User object"); try { + LOG.info("Loading user profile to update locale"); com.parallax.client.cloudsession.objects.User user = BlocklyPropSecurityUtils.getUserInfo(); - if (!user.getLocale().equals(locale)) { - LOG.info("Setting user locale: {} - {}", user.getId(), locale); - user = userService.changeUserLocale(user.getId(), locale); - BlocklyPropSecurityUtils.setUserInfo(user); + + if (user != null) { + if (!user.getLocale().equals(locale)) { + LOG.info("Setting user locale: {} - {}", user.getId(), locale); + user = userService.changeUserLocale(user.getId(), locale); + BlocklyPropSecurityUtils.setUserInfo(user); + } } } catch (UnknownUserIdException uuie) { LOG.error("Unknown user id", uuie); @@ -128,6 +221,23 @@ public void setLocale(String locale) { } } + // + + /** + * Update the user screen name stored in the blockyprop.users table + * + * @param idUser is the blocklyprop user primary key + * + * @param screenName is the new screen name text to store + */ + public void setScreenName(Long idUser, String screenName) { + userDao.updateScreenName(idUser, screenName); + + // TODO: Set session screen name attribute + + } + + public static UserService getUserService() { return UserServiceImpl.USER_SERVICE; } diff --git a/src/main/java/com/parallax/server/blocklyprop/servlets/AuthenticationServlet.java b/src/main/java/com/parallax/server/blocklyprop/servlets/AuthenticationServlet.java index 91670fe6..ab1f9f18 100644 --- a/src/main/java/com/parallax/server/blocklyprop/servlets/AuthenticationServlet.java +++ b/src/main/java/com/parallax/server/blocklyprop/servlets/AuthenticationServlet.java @@ -27,11 +27,9 @@ import com.parallax.client.cloudsession.objects.User; import com.parallax.server.blocklyprop.services.AuthenticationService; import java.io.IOException; -import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.apache.commons.configuration.Configuration; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.web.util.SavedRequest; import org.apache.shiro.web.util.WebUtils; @@ -50,39 +48,16 @@ @Singleton public class AuthenticationServlet extends HttpServlet { - /** - * Handle for any logging activity - */ + // Handle for any logging activity private final Logger LOG = LoggerFactory.getLogger(AuthenticationServlet.class); - - /** - * Application configuration settings - */ - private Configuration configuration; - - - /** - * An instance of this class - */ + //An instance of this class private AuthenticationService authenticationService; - - /** - * Initialize the application configuration - * - * @param configuration - */ - @Inject - public void setConfiguration(Configuration configuration) { - this.configuration = configuration; - } - - /** * Initialize an instance of the Authentication service * - * @param authenticationService + * @param authenticationService - inject an authentication service object */ @Inject public void setAuthenticationService(AuthenticationService authenticationService) { @@ -91,17 +66,25 @@ public void setAuthenticationService(AuthenticationService authenticationService } - + /** + * Process the authentication post request + * + * @param request - Http request object + * @param resp - Http response returned to the caller + * + * @throws IOException - an I/O error was detected + */ @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) - throws ServletException, IOException { + protected void doPost(HttpServletRequest request, HttpServletResponse resp) + throws IOException { LOG.info("REST:/authenticate/ Post request received"); + // Set the content type of the Http response resp.setContentType("application/json"); - String username = req.getParameter("username"); - String password = req.getParameter("password"); + String username = request.getParameter("username"); + String password = request.getParameter("password"); LOG.info("Authenticating user '{}'", username); @@ -120,14 +103,21 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) } if (user != null) { - SavedRequest savedRequest = WebUtils.getAndClearSavedRequest(req); + // Authentication succeeded + + /* A SavedRequest object maintains request data for a request that was + * redirected, so that after authentication the user can be redirected + * to the originally requested page. + */ + SavedRequest savedRequest = WebUtils.getAndClearSavedRequest(request); + if (savedRequest != null) { LOG.info("Redirecting to third-part authenticator"); resp.sendRedirect(savedRequest.getRequestUrl()); } else { - JsonObject response = new JsonObject(); response.addProperty("success", true); + JsonObject userJson = new JsonObject(); userJson.addProperty("id-user", user.getId()); userJson.addProperty("screenname", user.getScreenname()); @@ -153,5 +143,4 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) resp.getWriter().write(response.toString()); } } - } diff --git a/src/main/java/com/parallax/server/blocklyprop/servlets/ConfirmRequestServlet.java b/src/main/java/com/parallax/server/blocklyprop/servlets/ConfirmRequestServlet.java index fd3afd17..5241b3ea 100644 --- a/src/main/java/com/parallax/server/blocklyprop/servlets/ConfirmRequestServlet.java +++ b/src/main/java/com/parallax/server/blocklyprop/servlets/ConfirmRequestServlet.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.servlets; import com.google.common.base.Strings; diff --git a/src/main/java/com/parallax/server/blocklyprop/servlets/ConfirmServlet.java b/src/main/java/com/parallax/server/blocklyprop/servlets/ConfirmServlet.java index f34f0c1f..dd240019 100644 --- a/src/main/java/com/parallax/server/blocklyprop/servlets/ConfirmServlet.java +++ b/src/main/java/com/parallax/server/blocklyprop/servlets/ConfirmServlet.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.servlets; import com.google.common.base.Strings; @@ -33,6 +49,9 @@ @Singleton public class ConfirmServlet extends HttpServlet { + /** + * + */ private static Logger LOG = LoggerFactory.getLogger(ConfirmServlet.class); private final TextileReader textileFileReader = new TextileReader(); @@ -76,21 +95,22 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) public void confirmToken(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - // Retreive the registration token + // Retrieve the registration token String token = req.getParameter("token"); req.setAttribute("token", token == null ? "" : token); - // Retreive the requestor's email address + // Retrieve the requester's email address String email = req.getParameter("email"); req.setAttribute("email", email == null ? "" : email); // Return to the confirmation web page is we're missing data if (Strings.isNullOrEmpty(token) || Strings.isNullOrEmpty(email)) { + LOG.info("Confirmation data for {} is incomplete. Reloading request page", email); req.getRequestDispatcher("WEB-INF/servlet/confirm/confirm.jsp") .forward(req, resp); } else { try { - LOG.info("Trying to confirm: {}, {}", email, token); + LOG.info("Trying to confirm: {}", email); // Validate the email and token with the Cloud Session server if (cloudSessionLocalUserService.doConfirm(email, token)) { @@ -101,6 +121,7 @@ public void confirmToken(HttpServletRequest req, HttpServletResponse resp) // req.getRequestDispatcher("WEB-INF/servlet/confirm/confirmed.jsp").forward(req, resp); showTextilePage(req, resp, ConfirmPage.CONFIRMED); } else { + LOG.info("Failed to verify the token for email address {}", email); req.setAttribute("invalidToken", "Invalid token"); req.getRequestDispatcher("WEB-INF/servlet/confirm/confirm.jsp").forward(req, resp); } diff --git a/src/main/java/com/parallax/server/blocklyprop/servlets/HelpSearchServlet.java b/src/main/java/com/parallax/server/blocklyprop/servlets/HelpSearchServlet.java index 0d5b1ba7..644048f5 100644 --- a/src/main/java/com/parallax/server/blocklyprop/servlets/HelpSearchServlet.java +++ b/src/main/java/com/parallax/server/blocklyprop/servlets/HelpSearchServlet.java @@ -33,7 +33,11 @@ /** * * @author Michel + * + * @deprecated The help system has been moved to learn.parallax.com + * */ +@Deprecated @Singleton public class HelpSearchServlet extends HttpServlet { diff --git a/src/main/java/com/parallax/server/blocklyprop/servlets/HelpServlet.java b/src/main/java/com/parallax/server/blocklyprop/servlets/HelpServlet.java index cf1f42e8..55a85432 100644 --- a/src/main/java/com/parallax/server/blocklyprop/servlets/HelpServlet.java +++ b/src/main/java/com/parallax/server/blocklyprop/servlets/HelpServlet.java @@ -23,7 +23,10 @@ /** * * @author Michel + * + * @deprecated Help system is now hosted on learn.parallax.com */ +@Deprecated @Singleton public class HelpServlet extends HttpServlet { diff --git a/src/main/java/com/parallax/server/blocklyprop/servlets/PasswordResetRequestServlet.java b/src/main/java/com/parallax/server/blocklyprop/servlets/PasswordResetRequestServlet.java index f1f9e4f5..08b69e2d 100644 --- a/src/main/java/com/parallax/server/blocklyprop/servlets/PasswordResetRequestServlet.java +++ b/src/main/java/com/parallax/server/blocklyprop/servlets/PasswordResetRequestServlet.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.servlets; import com.google.common.base.Strings; diff --git a/src/main/java/com/parallax/server/blocklyprop/servlets/PasswordResetServlet.java b/src/main/java/com/parallax/server/blocklyprop/servlets/PasswordResetServlet.java index 9bc747be..0949d461 100644 --- a/src/main/java/com/parallax/server/blocklyprop/servlets/PasswordResetServlet.java +++ b/src/main/java/com/parallax/server/blocklyprop/servlets/PasswordResetServlet.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.servlets; import com.google.common.base.Strings; @@ -57,6 +73,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se String email = req.getParameter("email"); req.setAttribute("token", token == null ? "" : token); req.setAttribute("email", email == null ? "" : email); + + LOG.info("Redirecting to the reset password page"); req.getRequestDispatcher("WEB-INF/servlet/password-reset/do-reset.jsp").forward(req, resp); } @@ -69,8 +87,12 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S String email = req.getParameter("email"); req.setAttribute("token", token == null ? "" : token); req.setAttribute("email", email == null ? "" : email); + + String password = req.getParameter("password"); String confirmPassword = req.getParameter("confirmpassword"); + + LOG.info("Processing the results from the reset pasword page"); if (Strings.isNullOrEmpty(token) || Strings.isNullOrEmpty(email) || Strings.isNullOrEmpty(password) || Strings.isNullOrEmpty(confirmPassword)) { req.getRequestDispatcher("WEB-INF/servlet/password-reset/do-reset.jsp").forward(req, resp); @@ -107,8 +129,16 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S } } - public void showTextilePage(HttpServletRequest req, HttpServletResponse resp, PasswordResetPage passwordResetPage) throws ServletException, IOException { - String html = textileFileReader.readFile("password-reset/" + passwordResetPage.getPage(), ServletUtils.getLocale(req), req.isSecure()); + public void showTextilePage( + HttpServletRequest req, + HttpServletResponse resp, + PasswordResetPage passwordResetPage) throws ServletException, IOException { + + String html = textileFileReader.readFile( + "password-reset/" + passwordResetPage.getPage(), + ServletUtils.getLocale(req), + req.isSecure()); + req.setAttribute("html", html); req.getRequestDispatcher("/WEB-INF/servlet/html.jsp").forward(req, resp); } diff --git a/src/main/java/com/parallax/server/blocklyprop/servlets/PrivacyPolicyServlet.java b/src/main/java/com/parallax/server/blocklyprop/servlets/PrivacyPolicyServlet.java index 7ca31a87..5a1e7deb 100644 --- a/src/main/java/com/parallax/server/blocklyprop/servlets/PrivacyPolicyServlet.java +++ b/src/main/java/com/parallax/server/blocklyprop/servlets/PrivacyPolicyServlet.java @@ -1,40 +1,53 @@ /* - * 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.servlets; import com.google.inject.Singleton; import java.io.IOException; -import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; + /** + * Return the privacy policy page * * @author developer */ @Singleton public class PrivacyPolicyServlet extends HttpServlet { - - /** - * Application logging system access - */ - private static Logger LOG = LoggerFactory.getLogger(PrivacyPolicyServlet.class); - - // + // Application logging system access + private static final Logger LOG = LoggerFactory.getLogger(PrivacyPolicyServlet.class); + /** * Handles the HTTP GET method. * * @param request servlet request * @param response servlet response - * * @throws ServletException if a servlet-specific error occurs * @throws IOException if an I/O error occurs */ @@ -43,10 +56,10 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { LOG.debug("Requesting child privacy page"); - + request.getRequestDispatcher( "WEB-INF/servlet/coppa/privacy-policy.jsp") - .forward(request, response); + .forward(request, response); } /** @@ -57,6 +70,5 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) @Override public String getServletInfo() { return "Parallax COPPA policy page"; - }// - + } } diff --git a/src/main/java/com/parallax/server/blocklyprop/servlets/ProfileServlet.java b/src/main/java/com/parallax/server/blocklyprop/servlets/ProfileServlet.java index ae769816..bd5119b1 100644 --- a/src/main/java/com/parallax/server/blocklyprop/servlets/ProfileServlet.java +++ b/src/main/java/com/parallax/server/blocklyprop/servlets/ProfileServlet.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.servlets; import com.google.common.base.Strings; @@ -55,8 +71,12 @@ public class ProfileServlet extends HttpServlet { @Inject public void setConfiguration(Configuration configuration) { this.configuration = configuration; - cloudSessionLocalUserService = new CloudSessionLocalUserService(configuration.getString("cloudsession.server"), configuration.getString("cloudsession.baseurl")); - cloudSessionUserService = new CloudSessionUserService(configuration.getString("cloudsession.baseurl")); + cloudSessionLocalUserService = new CloudSessionLocalUserService( + configuration.getString("cloudsession.server"), + configuration.getString("cloudsession.baseurl")); + + cloudSessionUserService = new CloudSessionUserService( + configuration.getString("cloudsession.baseurl")); } @Inject @@ -75,6 +95,10 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se LOG.info("REST:/profile/ Get request received"); User user = BlocklyPropSecurityUtils.getUserInfo(); + if (user == null) { + req.setAttribute("base-error", "Unknown user"); + req.getRequestDispatcher("WEB-INF/servlet/profile/profile.jsp").forward(req, resp); + } req.setAttribute("id", user.getId()); req.setAttribute("email", user.getEmail()); req.setAttribute("screenname", user.getScreenname()); diff --git a/src/main/java/com/parallax/server/blocklyprop/servlets/ProjectServlet.java b/src/main/java/com/parallax/server/blocklyprop/servlets/ProjectServlet.java index b6bfbb25..3a2cbeb9 100644 --- a/src/main/java/com/parallax/server/blocklyprop/servlets/ProjectServlet.java +++ b/src/main/java/com/parallax/server/blocklyprop/servlets/ProjectServlet.java @@ -31,74 +31,118 @@ public class ProjectServlet extends HttpServlet { // Get a logger instance private static final Logger LOG = LoggerFactory.getLogger(ProjectServlet.class); + // Object to store the injected project service object private ProjectService projectService; - + + /** + * Inject project services access + * + * @param projectService - object to hold injected service class + */ @Inject public void setProjectService(ProjectService projectService) { this.projectService = projectService; } + /** - * - * @param req - * @param resp - * @throws ServletException - * @throws IOException + * Process an HTTP GET request + * + * @param request is an HttpServlet Request object + * @param response is an HttpServlet Response object + * + * @throws ServletException panic if something goes wrong in the servlet code + * @throws IOException - Really panic if there is an I/O issue. */ @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { LOG.info("REST:/project/ Get request received"); - - String clone = req.getParameter("clone"); + + /* ----------------------------------------------------- + * The request object holds parameters that indicate the + * action that is to be taken in this call. Each key is + * paired with a value that contains the id of the project + * to at upon. + * + * 'clone' - create a copy of the project specified + * 'delete' - destroy the project specified. Note that this + * is only allowed if the currently logged in + * user owns the project to be destroyed. + * ------------------------------------------------------*/ + String clone = request.getParameter("clone"); + if (!Strings.isNullOrEmpty(clone)) { - clone(Long.parseLong(clone), req, resp); + clone(Long.parseLong(clone), request, response); } - String delete = req.getParameter("delete"); + String delete = request.getParameter("delete"); + if (!Strings.isNullOrEmpty(delete)) { - delete(Long.parseLong(delete), req, resp); + delete(Long.parseLong(delete), request, response); } } - private void clone(Long idProject, HttpServletRequest req, HttpServletResponse resp) + + /** + * Create a copy of an existing project and assign it to the currently logged in user + * + * @param idProject The primary key id of the project to copy + * @param request is an HttpServlet Request object + * @param response is an HttpServlet Response object + * + * @throws ServletException panic if something goes wrong in the servlet code + * @throws IOException Really panic if there is an I/O issue. + */ + private void clone(Long idProject, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { LOG.info("Cloning project {}", idProject); try { ProjectRecord clonedProject = projectService.cloneProject(idProject); + if (clonedProject == null) { - req.getRequestDispatcher("WEB-INF/servlet/project/not-authorized.jsp").forward(req, resp); + request.getRequestDispatcher( + "WEB-INF/servlet/project/not-authorized.jsp").forward(request, response); } else { - resp.sendRedirect("my/projects.jsp#" + clonedProject.getId()); + response.sendRedirect("my/projects.jsp#" + clonedProject.getId()); } } catch (NullPointerException npe) { - req.getRequestDispatcher("WEB-INF/servlet/project/not-found.jsp").forward(req, resp); + request.getRequestDispatcher("WEB-INF/servlet/project/not-found.jsp").forward(request, response); } } - private void delete(Long idProject, HttpServletRequest req, HttpServletResponse resp) + + /** + * Destroy an existing project only when the currently logged in user owns the target project + * + * @param idProject The primary key id of the project to copy + * @param request is an HttpServlet Request object + * @param response is an HttpServlet Response object + * @throws ServletException panic if something goes wrong in the servlet code + * @throws IOException Really panic if there is an I/O issue. + */ + private void delete(Long idProject, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { LOG.info("Deleting project {}", idProject); - try { - try { - ProjectRecord project = projectService.getProjectOwnedByThisUser(idProject); - if (project == null) { - req.getRequestDispatcher("WEB-INF/servlet/project/not-found.jsp").forward(req, resp); - } - projectService.deleteProject(idProject); - resp.sendRedirect("my/projects.jsp"); - } catch (UnauthorizedException ue) { - req.getRequestDispatcher("WEB-INF/servlet/project/not-authorized.jsp").forward(req, resp); - } - } catch (NullPointerException npe) { - req.getRequestDispatcher("WEB-INF/servlet/project/not-found.jsp").forward(req, resp); - } - } + try { + ProjectRecord project = projectService.getProjectOwnedByThisUser(idProject); + + if (project == null) { + request.getRequestDispatcher("WEB-INF/servlet/project/not-found.jsp").forward(request, response); + } + projectService.deleteProject(idProject); + response.sendRedirect("my/projects.jsp"); + + } + catch (UnauthorizedException ue) { + request.getRequestDispatcher("WEB-INF/servlet/project/not-authorized.jsp").forward(request, response); + } + } } diff --git a/src/main/java/com/parallax/server/blocklyprop/servlets/PublicProfileServlet.java b/src/main/java/com/parallax/server/blocklyprop/servlets/PublicProfileServlet.java index f5a1e44e..09052bd5 100644 --- a/src/main/java/com/parallax/server/blocklyprop/servlets/PublicProfileServlet.java +++ b/src/main/java/com/parallax/server/blocklyprop/servlets/PublicProfileServlet.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.servlets; import com.google.common.base.Strings; @@ -12,7 +28,6 @@ import com.parallax.client.cloudsession.exceptions.EmailNotConfirmedException; import com.parallax.client.cloudsession.exceptions.ServerException; import com.parallax.client.cloudsession.exceptions.UnknownUserIdException; -import com.parallax.client.cloudsession.exceptions.EmailNotConfirmedException; import com.parallax.server.blocklyprop.db.generated.tables.pojos.User; import com.parallax.server.blocklyprop.services.UserService; import com.parallax.server.blocklyprop.services.impl.SecurityServiceImpl; @@ -50,7 +65,9 @@ public void setConfiguration(Configuration configuration) { } @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws + ServletException, IOException { + String idUserString = req.getParameter("id-user"); Long idUser = null; @@ -61,14 +78,14 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se if (SecurityUtils.getSubject().isAuthenticated()) { idUser = SecurityServiceImpl.getSessionData().getIdUser(); } else { - LOG.info("Getting current user while not authenticated"); + LOG.warn("Getting current user while not authenticated"); resp.sendError(404); } } else { idUser = Long.parseLong(idUserString); } } catch (NumberFormatException nfe) { - LOG.info("id-user is not a valid number: {}", idUserString); + LOG.warn("id-user is not a valid number: {}", idUserString); resp.sendError(500); } try { @@ -77,27 +94,39 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se if (user == null) { LOG.info("Get public profile for user {} (Does not exist!)", idUser); resp.sendError(404); - return; } - LOG.info("Get public profile for user {}: Cloud-session user: {}", idUser, user.getIdcloudsession()); - com.parallax.client.cloudsession.objects.User cloudSessionUser = cloudSessionUserService.getUser(user.getIdcloudsession()); + // It is possible to receive an empty, non-null object + if (cloudSessionUser == null) { + LOG.warn("User object is null"); + resp.sendError(404, "User profile is unavailable"); + } + if (cloudSessionUser.getScreenname() == null ) { + LOG.warn("Unable to decode result from Cloud Session call"); + resp.sendError(404, "User object is empty"); + } + req.setAttribute("screenname", cloudSessionUser.getScreenname()); req.getRequestDispatcher("/WEB-INF/servlet/public-profile.jsp").forward(req, resp); - } catch (EmailNotConfirmedException ex) { - LOG.info("User not known in cloud-session"); + } + catch (EmailNotConfirmedException ex) { + LOG.info("User email is unconfirmed cloud-session"); resp.sendError(404); - } catch (UnknownUserIdException ex) { + } + catch (UnknownUserIdException ex) { LOG.info("User not known in cloud-session"); resp.sendError(404); - } catch (ServerException ex) { + } + catch (NullPointerException ex) { + LOG.warn("Unexpected Null Pointer Exception encountered. Message is: {}", ex.getMessage()); + resp.sendError(404, "NPE error. User not found"); + } + catch (ServerException ex) { LOG.error("Communication problem with Cloud-session", ex); resp.sendError(500); } - } - } diff --git a/src/main/java/com/parallax/server/blocklyprop/servlets/TextileIndexServlet.java b/src/main/java/com/parallax/server/blocklyprop/servlets/TextileIndexServlet.java index 9ab3314c..3a76743e 100644 --- a/src/main/java/com/parallax/server/blocklyprop/servlets/TextileIndexServlet.java +++ b/src/main/java/com/parallax/server/blocklyprop/servlets/TextileIndexServlet.java @@ -1,52 +1,69 @@ /* - * 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.servlets; import com.google.inject.Singleton; -import com.parallax.server.blocklyprop.utils.ServletUtils; -import com.parallax.server.blocklyprop.utils.TextileReader; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.apache.shiro.session.UnknownSessionException; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** + * Display the BlocklyProp home page * * @author Michel + * + * TODO: Refactor this into a file in the application servlets directory */ @Singleton public class TextileIndexServlet extends HttpServlet { - /** - * Application logging facility - */ + // Application logging facility private static final Logger LOG = LoggerFactory.getLogger(TextileIndexServlet.class); - private final TextileReader textileFileReader = new TextileReader(); - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - + + LOG.info("Processing a Http request for '/'"); + try { - String html = textileFileReader.readFile("index", ServletUtils.getLocale(req), req.isSecure()); - req.setAttribute("html", html); - req.getRequestDispatcher("/WEB-INF/servlet/index.jsp").forward(req, resp); + // Direct the request to the home page + request.getRequestDispatcher( + "WEB-INF/servlet/index.jsp") + .forward(request, response); } - catch (UnknownSessionException ex) { - // Redrect the session back to the home page. - LOG.error("Session has expired. Request was for: {}", req.getRequestURI()); - resp.flushBuffer(); - req.getRequestDispatcher("/WEB-INF/servlet/index.jsp").forward(req, resp); + catch (ServletException se) { + LOG.warn("Servlet exception encountered while serving /index.jsp"); + LOG.warn("Error message is: {}", se.getMessage()); + } + catch (IOException eio) { + LOG.warn("I/O exception encountered while serving /index/jsp"); + LOG.warn("Error message is: {}", eio.getMessage()); } } - } diff --git a/src/main/java/com/parallax/server/blocklyprop/utils/HelpFileInitializer.java b/src/main/java/com/parallax/server/blocklyprop/utils/HelpFileInitializer.java index efbdfede..256707bb 100644 --- a/src/main/java/com/parallax/server/blocklyprop/utils/HelpFileInitializer.java +++ b/src/main/java/com/parallax/server/blocklyprop/utils/HelpFileInitializer.java @@ -37,7 +37,10 @@ /** * * @author Michel + * + * @deprecated The help system has been moved to learn.parallax.com */ +@Deprecated @Singleton public class HelpFileInitializer { diff --git a/src/main/java/com/parallax/server/blocklyprop/utils/RestProjectUtils.java b/src/main/java/com/parallax/server/blocklyprop/utils/RestProjectUtils.java new file mode 100644 index 00000000..69e7f113 --- /dev/null +++ b/src/main/java/com/parallax/server/blocklyprop/utils/RestProjectUtils.java @@ -0,0 +1,98 @@ +/* + * 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.utils; + +import com.parallax.server.blocklyprop.TableOrder; +import com.parallax.server.blocklyprop.TableSort; +import com.parallax.server.blocklyprop.db.enums.ProjectType; + + +/** + * 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. + * + * @return a boolean true if the supplied value matches a sortable column, otherwise + * return a boolean false value + */ + public static boolean ValidateSortType(TableSort sort) { + + if (sort != null) { + for (TableSort t : TableSort.values()) { + if (sort == t) { + return true; + } + } + } + + 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) { + + if (order != null) { + for (TableOrder t : TableOrder.values()) { + if (order == t) { + return true; + } + } + } + + 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/com/parallax/server/blocklyprop/internationalization/translations.properties b/src/main/resources/com/parallax/server/blocklyprop/internationalization/translations.properties index 245ed8c0..3f0e100d 100644 --- a/src/main/resources/com/parallax/server/blocklyprop/internationalization/translations.properties +++ b/src/main/resources/com/parallax/server/blocklyprop/internationalization/translations.properties @@ -27,8 +27,8 @@ footer.clientdownloadlink = BlocklyProp-client # Application version numbers. application.major = 1 -application.minor = 1 -application.build = 449 +application.minor = 2 +application.build = 458 html.content_missing = Content missing diff --git a/src/main/resources/documents/index.textile b/src/main/resources/documents/index.textile deleted file mode 100644 index d3f86c60..00000000 --- a/src/main/resources/documents/index.textile +++ /dev/null @@ -1,14 +0,0 @@ - -

- - - -*Blockly for Propeller Multicore:* Making amazing projects and learning to code just became easier - -!(cdn full-width)/images/home-banner.png! - -
diff --git a/src/main/resources/shiro.ini b/src/main/resources/shiro.ini index 22d40217..dddfe00e 100644 --- a/src/main/resources/shiro.ini +++ b/src/main/resources/shiro.ini @@ -1,5 +1,5 @@ # -# Copyright (c) 2018 Parallax Inc. +# 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 @@ -21,64 +21,98 @@ [main] -# Supports direct (plain) comparison for credentials of type byte[], char[], -# and Strings, and if the arguments do not match these types, then reverts back -# to simple Object.equals comparison. -credentialsMatcher = org.apache.shiro.authc.credential.SimpleCredentialsMatcher +# --------------- +# Session Manager +# --------------- +# Web-application capable SessionManager implementation. This provides a +# handler for HttpRequest and HttpResponse integrated with the Shiro system. +# The security manager will use the DefaultWebSessionManager interface. +# -------------------------------------------------------------------------- +sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager +securityManager.sessionManager = $sessionManager +# ------------------------- +# Enable session ID cookies +# ------------------------- +securityManager.sessionManager.sessionIdCookieEnabled = true -# A credentials matcher that always returns true when matching credentials no matter -# what arguments are passed in. This can be used for testing or when credentials are -# implicitly trusted for a particular Realm. -allow_all_credentialsMatcher = org.apache.shiro.authc.credential.AllowAllCredentialsMatcher +# -------------------------------------------------------------------------- +# Disable session management when operating within a multi-host environment +# -------------------------------------------------------------------------- +securityManager.sessionManager.deleteInvalidSessions = false +securityManager.sessionManager.sessionValidationSchedulerEnabled = false +securityManager.sessionManager.globalSessionTimeout = 28800000 -# cloudSessionMatcher = com.parallax.server.blocklyprop.security.CloudSessionCredentialsMatcher +# Attach the default session validation scheduler +sessionValidationScheduler = org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler +securityManager.sessionManager.sessionValidationScheduler = $sessionValidationScheduler +# Run once per day +securityManager.sessionManager.sessionValidationScheduler.interval = 86400000 -# Define interface to the backend storage to hold user credentials -cloudsessionRealm = com.parallax.server.blocklyprop.security.CloudSessionAuthenticationRealm +# ---------------- +# Backing Storage +# ---------------- +# +# Configure the SessionDao to use the BlocklyPropSessionDao class to implement +# the SessionDao interface. Configure the Security Manager to use this interface +# to persist session data to a backend EIS +# ------------------------------------------------------------------------------- +sessionDao = com.parallax.server.blocklyprop.security.BlocklyPropSessionDao +securityManager.sessionManager.sessionDAO = $sessionDao -# Tell the Shiro security manager to use the CloudSession Realm -securityManager.realms = $cloudsessionRealm -# Configure the session manager -# Web-application capable SessionManager implementation. -sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager -securityManager.sessionManager = $sessionManager +# Security Manager Backend Storage (User Accounts) +# ------------------------------------------------- +# Define interface to the backend storage to hold user credentials. This is the +# Cloud Session interface for user authentication +cloudsessionRealm = com.parallax.server.blocklyprop.security.CloudSessionAuthenticationRealm +securityManager.realms = $cloudsessionRealm -# Configure a SessionDAO and then set it: -sessionDao = com.parallax.server.blocklyprop.security.BlocklyPropSessionDao -securityManager.sessionManager.sessionDAO = $sessionDao -securityManager.sessionManager.sessionIdCookieEnabled = true -# Attach the default session validation scheduler -sessionValidationScheduler = org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler +# Credential Matching +# ------------------- +# Supports direct (plain) comparison for credentials of type byte[], char[], +# and Strings, and if the arguments do not match these types, then reverts back +# to simple Object.equals comparison. +credentialsMatcher = org.apache.shiro.authc.credential.SimpleCredentialsMatcher -# Run once per day -sessionValidationScheduler.interval = 86400000 +#============================================ +# TODO: Determine if this should be wired up +#============================================ +# This appears to be implemented and not referenced in this configuration file +cloudSessionMatcher = com.parallax.server.blocklyprop.security.CloudSessionCredentialsMatcher -# TESTING - Run every 30 seconds -# sessionValidationScheduler.interval = 300000 -securityManager.sessionManager.sessionValidationScheduler = $sessionValidationScheduler +####### TESTING AND DEV SETTINGS ######## +# A credentials matcher that always returns true when matching credentials no matter +# what arguments are passed in. This can be used for testing or when credentials are +# implicitly trusted for a particular Realm. +# ----------------------------------------------------------------------------------- +# allow_all_credentialsMatcher = org.apache.shiro.authc.credential.AllowAllCredentialsMatcher +#-########################################################################################## -# -------------------------------------------------------------------------- -# Disable session management when operating within a multi-host environment -# -------------------------------------------------------------------------- -securityManager.sessionManager.deleteInvalidSessions = false -securityManager.sessionManager.sessionValidationSchedulerEnabled = false -# Set global default session timeout to eight hours -securityManager.sessionManager.globalSessionTimeout = 28800000 ssl.enabled = false shiro.loginUrl = /login.jsp +# Static user accounts go here +# ---------------------------- +[users] + +# Authorization through roles is implemented here +# ----------------------------------------------- +[roles] + + +# Manager access to urls and url groups +# -------------------------------------- [urls] # # A list of accessable URLs @@ -102,7 +136,10 @@ shiro.loginUrl = /login.jsp # Public pages / = anon, ssl /index = anon, ssl + +# Display community projects /projects.jsp = anon, ssl + /public/** = anon /ping = anon /sessionapi = anon @@ -120,7 +157,17 @@ shiro.loginUrl = /login.jsp # REST api and api documentation /apidoc = anon /rest/shared/** = anon, 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 @@ -130,3 +177,5 @@ shiro.loginUrl = /login.jsp /** = authc, user, ssl #Testing + + diff --git a/src/main/webapp/WEB-INF/includes/pageparts/menu.jsp b/src/main/webapp/WEB-INF/includes/pageparts/menu.jsp index efd2df01..91605ca3 100644 --- a/src/main/webapp/WEB-INF/includes/pageparts/menu.jsp +++ b/src/main/webapp/WEB-INF/includes/pageparts/menu.jsp @@ -1,7 +1,30 @@ +<%-- + ~ 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. + --%> + <%-- Document : menu Created on : 4-nov-2015, 20:39:22 Author : Michel + + Display the horizontal menu across the top of the banner --%> <%@page contentType="text/html" pageEncoding="UTF-8"%> <%@ include file="/WEB-INF/includes/include.jsp"%> @@ -18,51 +41,53 @@ "> - " target="oauth">Log in using Google - - - <%@ include file="/WEB-INF/includes/pageparts/footer.jsp"%> - \ No newline at end of file diff --git a/src/main/webapp/my/projects.jsp b/src/main/webapp/my/projects.jsp index 1f4af0d9..c98da1c8 100644 --- a/src/main/webapp/my/projects.jsp +++ b/src/main/webapp/my/projects.jsp @@ -5,8 +5,8 @@ --%> <%@page contentType="text/html" pageEncoding="UTF-8"%> <%@ include file="/WEB-INF/includes/include.jsp"%> - - + + @@ -30,32 +30,25 @@ + My Projects - <%@ include file="/WEB-INF/includes/pageparts/menu.jsp"%> -
-

- -
- - <%@ include file="/WEB-INF/includes/pageparts/footer.jsp"%> - \ No newline at end of file diff --git a/src/main/webapp/projects.jsp b/src/main/webapp/projects.jsp index 991e687f..ed6df55a 100644 --- a/src/main/webapp/projects.jsp +++ b/src/main/webapp/projects.jsp @@ -1,5 +1,5 @@ <%-- - ~ Copyright (c) 2018 Parallax Inc. + ~ 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 @@ -20,9 +20,11 @@ --%> <%-- - Document : projects + Document : projects.jsp Created on : 24-mei-2015, 18:41:02 Author : Michel + Notes : Display the community projects in a table format + TODO: UI-Convert to client side content with project data sourced from server endpoint. --%> <%@page contentType="text/html" pageEncoding="UTF-8"%>