From 707ba881eacf81c803a7a474d8460bdf58047ae6 Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Wed, 20 Feb 2019 12:58:39 -0800 Subject: [PATCH 01/19] Add a settings field for the user profile profile table. --- db-updates/0014-add-project-settings.sql | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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; + From 93195708712ca8e385c920c66c01b95b9f2c0ede Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Wed, 20 Feb 2019 13:01:17 -0800 Subject: [PATCH 02/19] Insert copyright notice. --- .../blocklyprop/db/dao/impl/UserDaoImpl.java | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/UserDaoImpl.java b/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/UserDaoImpl.java index be056238..f3e7702d 100644 --- a/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/UserDaoImpl.java +++ b/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/UserDaoImpl.java @@ -1,8 +1,24 @@ /* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. + * Copyright (c) 2019 Parallax Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the “Software”), to deal in the Software without + * restriction, including without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. */ + package com.parallax.server.blocklyprop.db.dao.impl; import com.google.inject.Inject; @@ -24,6 +40,7 @@ import org.slf4j.LoggerFactory; /** + * Blockly user * * @author Michel */ From 8089bdbe753c593fe7a39b820acf58beca5bb49e Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Wed, 20 Feb 2019 13:24:25 -0800 Subject: [PATCH 03/19] Add inline documentation. Insert copyright notice. --- .../server/blocklyprop/db/dao/ProjectDao.java | 220 +++++++++++++++++- 1 file changed, 209 insertions(+), 11 deletions(-) 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..6c32b43d 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 @@ -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; import com.parallax.server.blocklyprop.TableOrder; @@ -17,11 +33,11 @@ * @author Michel * * Fields: - * id: Unique record number within the table. + * id: Unique record number within the table * - * id_user: Link to primary key in the blocklyprop.user table. + * id_user: Link to primary key in the blocklyprop.user table * - * id_clouduser: Link to the primary key in the cloudsession.user table. + * id_clouduser: Link to the primary key in the cloudsession.user table * * name: Project name * @@ -29,18 +45,18 @@ * * description_html:Project description formatted in HTML * - * code: XML content that hold the project block structure. + * code: XML content that holds the project block structure * * type: Project source language (SPIN or PROPC) * - * board: Descriptor for the target device the project will use. + * 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 + * 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' + * anyone. This flag is mutually exclusive with the 'private' * flag. * * created: Timestamp indicating when the project record was created. @@ -53,8 +69,52 @@ */ public interface ProjectDao { + /** + * Retrieve a project based on the supplied project id + * + * @param idProject - unique project key id + * + * @return - a ProjectRecord object if the project is available or + * a null if the project was not found or is not accessible to the + * the user in the current session. + */ ProjectRecord getProject(Long idProject); + + + /** + * Create a new project record from the supplied details + * + * @param name + * Project name + * + * @param description + * Project description formatted in plain text + * + * @param descriptionHtml + * Project description formatted in HTML + * + * @param code + * XML content that holds the project block structure + * + * @param type + * Project source language (SPIN or PROPC) + * + * @param board + * Descriptor for the target device the project will use + * + * @param privateProject + * Flag to indicate if the project is visible to anyone but the project owner + * + * @param sharedProject + * Flag to indicate if the project is available for viewing by anyone + * + * @param idProjectBasedOn + * The id from the project that is the parent of the current project record + * + * @return + * a ProjectRecord object if the project is available, otherwise return a null + */ ProjectRecord createProject( String name, String description, @@ -66,6 +126,19 @@ ProjectRecord createProject( boolean sharedProject, Long idProjectBasedOn); + + + /** + * + * @param name + * @param description + * @param descriptionHtml + * @param type + * @param board + * @param privateProject + * @param sharedProject + * @return + */ ProjectRecord createProject( String name, String description, @@ -75,6 +148,18 @@ ProjectRecord createProject( boolean privateProject, boolean sharedProject); + + + /** + * + * @param idProject + * @param name + * @param description + * @param descriptionHtml + * @param privateProject + * @param sharedProject + * @return + */ ProjectRecord updateProject( Long idProject, String name, @@ -83,6 +168,19 @@ ProjectRecord updateProject( boolean privateProject, boolean sharedProject); + + + /** + * + * @param idProject + * @param name + * @param description + * @param descriptionHtml + * @param code + * @param privateProject + * @param sharedProject + * @return + */ ProjectRecord updateProject( Long idProject, String name, @@ -92,10 +190,33 @@ ProjectRecord updateProject( boolean privateProject, boolean sharedProject); + + + /** + * Update the code in the selected project + * TODO: The IDE is reporting that this method is never used. Verify and deprecate as needed. + * + * @param idProject + * @param code + * @return + */ ProjectRecord saveCode( Long idProject, String code); + + + /** + * List community projects owned by a selected user + * TODO: This appears to do the same work as the getSharedProjectsByUser method. + * Identify a need for both to exist or deprecate one of the methods. + * @param idUser + * @param sort + * @param order + * @param limit + * @param offset + * @return + */ List getUserProjects( Long idUser, TableSort sort, @@ -103,13 +224,37 @@ List getUserProjects( Integer limit, Integer offset); - // Return a list of community projects + + + /** + * Return a list of community projects + * + * @param sort + * @param order + * @param limit + * @param offset + * @return + */ List getSharedProjects( TableSort sort, TableOrder order, Integer limit, Integer offset); + + + /** + * List community projects owned by a selected user + * TODO: This appears to do the same work as the overloaded getUserProjects. + * Figure it out and remove one. + * + * @param sort + * @param order + * @param limit + * @param offset + * @param idUser + * @return + */ List getSharedProjectsByUser( TableSort sort, TableOrder order, @@ -117,20 +262,73 @@ List getSharedProjectsByUser( Integer offset, Long idUser); + + + /** + * + * @param idUser + * @return + */ int countUserProjects(Long idUser); + + + /** + * + * @param idUser + * @return + */ int countSharedProjects(Long idUser); + + + /** + * + * @param idUser + * @return + */ int countSharedProjectsByUser(Long idUser); + + + /** + * + * @param idProject + * @return + */ ProjectRecord cloneProject(Long idProject); + + + /** + * + * @param idProject + * @return + */ boolean deleteProject(Long idProject); + + + /** + * + * @param idProject + * @param code + * @return + */ ProjectRecord updateProjectCode( Long idProject, String code); + + + /** + * + * @param idProject + * @param code + * @param newName + * @param newBoard + * @return + */ ProjectRecord saveProjectCodeAs( Long idProject, String code, From a9adb38f66626d5449a2463fc289a8671a24359c Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Wed, 20 Feb 2019 14:19:51 -0800 Subject: [PATCH 04/19] Add support for a project settings field in a project. This is designed to hold a JSON formatted string that contains settings as defined by the client UI. --- .../server/blocklyprop/db/dao/ProjectDao.java | 50 ++++++ .../db/dao/impl/ProjectDaoImpl.java | 160 ++++++++++++++++-- .../db/generated/tables/Project.java | 7 +- .../db/generated/tables/pojos/Project.java | 17 +- .../tables/records/ProjectRecord.java | 63 +++++-- 5 files changed, 271 insertions(+), 26 deletions(-) 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 6c32b43d..ab8a85df 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 @@ -81,6 +81,56 @@ public interface ProjectDao { ProjectRecord getProject(Long idProject); + /** + * Create a new project record from the supplied details + * + * @param name + * Project name + * + * @param description + * Project description formatted in plain text + * + * @param descriptionHtml + * Project description formatted in HTML + * + * @param code + * XML content that holds the project block structure + * + * @param type + * Project source language (SPIN or PROPC) + * + * @param board + * Descriptor for the target device the project will use + * + * @param privateProject + * Flag to indicate if the project is visible to anyone but the project owner + * + * @param sharedProject + * Flag to indicate if the project is available for viewing by anyone + * + * @param idProjectBasedOn + * The id from the project that is the parent of the current project record + * + * @param projectSettings + * A JSON formatted string containing project settings. + * + * @return + * a ProjectRecord object if the project is available, otherwise return a null + */ + + ProjectRecord createProject( + String name, + String description, + String descriptionHtml, + String code, + ProjectType type, + String board, + boolean privateProject, + boolean sharedProject, + Long idProjectBasedOn, + String projectSettings); + + /** * Create a new project record from the supplied details diff --git a/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/ProjectDaoImpl.java b/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/ProjectDaoImpl.java index 93c7308a..ac5afbce 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 @@ -43,7 +43,7 @@ public class ProjectDaoImpl implements ProjectDao { * The absolute lowest number of bytes that can exist in an * un-populated project. */ - private static final int Min_BlocklyCodeSize = 48; + private static final int MIN_BLOCKLY_CODE_SIZE = 48; /** @@ -79,6 +79,8 @@ public void setDSLContext(DSLContext dsl) { private ProjectSharingService projectSharingService; + + @Inject public void setProjectSharingContext(ProjectSharingService projectSharingService) { this.projectSharingService = projectSharingService; @@ -86,7 +88,6 @@ public void setProjectSharingContext(ProjectSharingService projectSharingService /** - * * Retrieve a new project record based from an existing project. * * Note: There are private getProject methods that retrieve a project record @@ -131,7 +132,117 @@ record = create return alterReadRecord(record); } - + + /** + * Create a complete project record. All parameters are supplied by the caller. + * + * @param name + * Project name + * + * @param description + * Project description formatted in plain text + * + * @param descriptionHtml + * Project description formatted in HTML + * + * @param code + * XML content that holds the project block structure + * + * @param type + * Project source language (SPIN or PROPC) + * + * @param board + * Descriptor for the target device the project will use + * + * @param privateProject + * Flag to indicate if the project is visible to anyone but the project owner + * + * @param sharedProject + * Flag to indicate if the project is available for viewing by anyone + * + * @param idProjectBasedOn + * The id from the project that is the parent of the current project record + * + * @param projectSettings + * A JSON formatted string containing project settings. + * + * @return + */ + public ProjectRecord createProject( + String name, + String description, + String descriptionHtml, + String code, + ProjectType type, + String board, + boolean privateProject, + boolean sharedProject, + Long idProjectBasedOn, + String projectSettings) { + + 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, + projectSettings) + .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. @@ -161,6 +272,11 @@ public ProjectRecord createProject( boolean sharedProject, Long idProjectBasedOn) { + return createProject( + name, description, descriptionHtml, code, type, board, + privateProject, sharedProject, idProjectBasedOn, null); + +/* LOG.info("Creating a new project with existing code."); ProjectRecord record = null; @@ -219,8 +335,11 @@ record = create } return record; + */ } + + /** * * Create a new project with a blank canvas (no blocks). @@ -436,7 +555,7 @@ public List getSharedProjects( Integer limit, Integer offset) { - LOG.info("Retreive shared projects."); + LOG.info("Retrieve shared projects."); SortField orderField = sort == null ? Tables.PROJECT.NAME.asc() : sort.getField().asc(); if (TableOrder.desc == order) { @@ -718,6 +837,8 @@ public ProjectRecord saveProjectCodeAs(Long idProject, String code, String newNa return null; } + + // Private over-ride of the public getProject() // // @@ -750,9 +871,17 @@ private ProjectRecord getProject(Long idProject, boolean toEdit) { return alterReadRecord(record); } + + + /** + * + * @param original + * @return + */ private ProjectRecord doProjectClone(ProjectRecord original) { // TODO: Add based_on parameter as last argument + // TODO: Add parameter to support project settings ProjectRecord cloned = createProject( original.getName(), original.getDescription(), @@ -768,7 +897,7 @@ private ProjectRecord doProjectClone(ProjectRecord original) { // cloned.setBasedOn(original.getId()); // cloned.update(); - // WHAT IS THIS DOING? + // TODO WHAT IS THIS DOING? create.update(Tables.PROJECT) .set(Tables.PROJECT.BASED_ON, original.getId()) .where(Tables.PROJECT.ID.equal(cloned.getId())); @@ -777,7 +906,13 @@ private ProjectRecord doProjectClone(ProjectRecord original) { } - // Produce a current timestamp + + + /** + * Produce a current timestamp + * + * @return a GregorianCalendar time stamp + */ private GregorianCalendar getCurrentTimestamp() { GregorianCalendar cal = new GregorianCalendar(); cal.setTime(new java.util.Date()); @@ -787,9 +922,7 @@ private GregorianCalendar getCurrentTimestamp() { - - - + // Evaluate project code and replace any deprecated or updated blocks // // Return a ProjectRecord object. The code field may be altered to correct @@ -798,7 +931,6 @@ private GregorianCalendar getCurrentTimestamp() { // horribly wrong with the string conversions. // private ProjectRecord alterReadRecord(ProjectRecord record) { - String currentCode, newCode; @@ -825,7 +957,7 @@ private ProjectRecord alterReadRecord(ProjectRecord record) { return record; } - if (currentCode.length() < Min_BlocklyCodeSize ) { + if (currentCode.length() < MIN_BLOCKLY_CODE_SIZE) { LOG.warn("Project code appears to be empty. Code size:{}",currentCode.length()); return record; } @@ -849,7 +981,9 @@ private ProjectRecord alterReadRecord(ProjectRecord record) { return record; } - /** + + + /** * * Create a random string to use as a blockID. * @@ -867,6 +1001,8 @@ private String randomString(int len) { return sb.toString(); } + + // Correct depricated block details related to Spin blocks @Deprecated private String fixSpinProjectBlocks(String newCode) { 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/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/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); } } From 13c146f5a0c7035c233e505cf5609c05b01016e6 Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Wed, 20 Feb 2019 14:20:04 -0800 Subject: [PATCH 05/19] Add inline documentation. Insert copyright notice. --- .../parallax/server/blocklyprop/db/generated/tables/Motd.java | 2 +- .../server/blocklyprop/db/generated/tables/pojos/Motd.java | 2 +- .../blocklyprop/db/generated/tables/records/MotdRecord.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/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/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 +} From 0753132548d8a395db61695febd6633257311174 Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Thu, 21 Feb 2019 10:55:27 -0800 Subject: [PATCH 06/19] Implement project settings support. Arrange methods based on functional groups. Added to documentation effort. --- .../db/dao/impl/ProjectDaoImpl.java | 660 +++++++++--------- 1 file changed, 336 insertions(+), 324 deletions(-) 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 ac5afbce..6999ef3a 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 @@ -39,54 +39,51 @@ @Singleton public class ProjectDaoImpl implements ProjectDao { - /** - * The absolute lowest number of bytes that can exist in an - * un-populated project. - */ + // The absolute lowest number of bytes that can exist in an un-populated project. private static final int MIN_BLOCKLY_CODE_SIZE = 48; - /** - * Application logging facility - */ + // Application logging facility private static final Logger LOG = LoggerFactory.getLogger(ProjectDao.class); - /** - * Database connection - */ + // 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; + // Constant to identify the current version of the blockly block library; public static final short BLOCKLY_LIBRARY_VERSION = 1; - + + // Inject a database connection @Inject public void setDSLContext(DSLContext dsl) { this.create = dsl; } + // Injecting the project sharing service + // TODO: Figure out why the projected sharing service is being injected into + // the DAO layer. That is just weird private ProjectSharingService projectSharingService; - - @Inject public void setProjectSharingContext(ProjectSharingService projectSharingService) { this.projectSharingService = projectSharingService; } + /************************************************************************** + * + * GET methods + * + **************************************************************************/ + /** * Retrieve a new project record based from an existing project. * @@ -122,6 +119,7 @@ record = create } 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("Error Message: {}", ex.getMessage()); @@ -133,6 +131,124 @@ record = create } + /** + * TODO: add details. + * + * @param idUser + * @param sort + * @param order + * @param limit + * @param offset + * @return + */ + @Override + public List getUserProjects( + Long idUser, + TableSort sort, + TableOrder order, + Integer limit, + Integer offset) { + + LOG.info("Retreive projects for user {}.", idUser); + + SortField orderField = Tables.PROJECT.NAME.asc(); + if (TableOrder.desc == order) { + orderField = Tables.PROJECT.NAME.desc(); + } + + return create + .selectFrom(Tables.PROJECT) + .where(Tables.PROJECT.ID_USER.equal(idUser)) + .orderBy(orderField).limit(limit).offset(offset) + .fetch(); + } + + /** + * Return a list of community projects constrained by the parameters + * + * @param sort + * @param order + * @param limit + * @param offset + + * @return + * Returns a list of ProjectRecord objects corresponding to the projects + * matching the selection creiteria + */ + @Override + public List getSharedProjects( + TableSort sort, + TableOrder order, + Integer limit, + Integer offset) { + + LOG.info("Retrieve shared projects."); + + SortField orderField = sort == null ? Tables.PROJECT.NAME.asc() : sort.getField().asc(); + + if (TableOrder.desc == order) { + orderField = sort == null ? Tables.PROJECT.NAME.desc() : sort.getField().desc(); + } + + // 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) + .fetch(); + } + + + + /** + * TODO: add details. + * + * @param sort + * @param order + * @param limit + * @param offset + * @param idUser + * @return + */ + @Override + public List getSharedProjectsByUser( + 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(); + + if (TableOrder.desc == order) { + orderField = sort == null ? Tables.PROJECT.NAME.desc() : sort.getField().desc(); + } + + Condition conditions = Tables.PROJECT.SHARED.eq(Boolean.TRUE); + + if (idUser != null) { + conditions = conditions.and(Tables.PROJECT.ID_USER.eq(idUser)); + } + + return create + .selectFrom(Tables.PROJECT) + .where(conditions) + .orderBy(orderField).limit(limit).offset(offset) + .fetch(); + } + + + + /************************************************************************** + * + * POST methods + * + **************************************************************************/ + /** * Create a complete project record. All parameters are supplied by the caller. * @@ -167,7 +283,9 @@ record = create * A JSON formatted string containing project settings. * * @return + * Returns a ProjectRecord object, including the new project settings string */ + @Override public ProjectRecord createProject( String name, String description, @@ -244,7 +362,6 @@ record = create /** - * * Create a new project with supplied code. * * @param name Project name @@ -259,6 +376,8 @@ record = create * from another project * * @return a fully formed ProjectRecord or a null if an error is detected. + * + * @implNote This is an override of the base createProject class. */ @Override public ProjectRecord createProject( @@ -276,66 +395,6 @@ public ProjectRecord createProject( name, description, descriptionHtml, code, type, board, privateProject, sharedProject, idProjectBasedOn, null); -/* - 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) - .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()); - return null; - } catch (Exception ex) { - LOG.error("Unexpected exception creating a project record"); - LOG.error("Error Message: {}", ex.getMessage()); - return null; - } - - return record; - */ } @@ -344,9 +403,6 @@ record = create * * Create a new project with a blank canvas (no blocks). * - * This is an overload of the base createProject method that omits - * the code block. - * * @param name * @param description * @param descriptionHtml @@ -355,6 +411,9 @@ record = create * @param privateProject * @param sharedProject * @return + * + * @implNote + * This is an overload of the base createProject method that omits the code block. */ @Override public ProjectRecord createProject( @@ -366,21 +425,50 @@ public ProjectRecord createProject( boolean privateProject, boolean sharedProject) { - LOG.info("Creating a new, empty project from existing project."); + LOG.info("Creating a new, empty project."); // TODO: Add based_on field to end of argument list return createProject( - name, - description, - descriptionHtml, - "", - type, - board, - privateProject, - sharedProject, - null); + name, description, descriptionHtml, "", type, board, + privateProject, sharedProject, null); } + /** + * TODO: add details. + * + * @param idProject + * @return + */ + @Override + public ProjectRecord cloneProject(Long idProject) { + + LOG.info("Clone existing project {} to a new project.", idProject); + + // Retrieve the project to be copied + ProjectRecord projectRecord = getProject(idProject); + + if (projectRecord == null) { + throw new NullPointerException("Project doesn't exist"); + } + + // Allow a copy only if the project is shared (public) or if the + // currently logged in user owns the project + Long idUser = BlocklyPropSecurityUtils.getCurrentUserId(); + + if (projectRecord.getIdUser().equals(idUser) || projectRecord.getShared()) { + return doProjectClone(projectRecord); + } + + return null; + } + + + /************************************************************************** + * + * UPDATE methods + * + **************************************************************************/ + /** * @@ -409,20 +497,27 @@ public ProjectRecord updateProject( LOG.info("Update project {}.", idProject); ProjectRecord record = getProject(idProject, EDIT_MODE_ON); + if (record == null) { LOG.warn("Unable to locate project {} to update it.", idProject); return null; } + // Update the fields with data from the caller record.setName(name); record.setDescription(description); record.setDescriptionHtml(descriptionHtml); record.setPrivate(privateProject); record.setShared(sharedProject); + + // Add some housekeeping meta data record.setModified(getCurrentTimestamp()); record.setCodeBlockVersion(BLOCKLY_LIBRARY_VERSION); + + // Persist the updated record record.update(); + return record; } @@ -454,166 +549,153 @@ public ProjectRecord updateProject( 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.setModified(getCurrentTimestamp()); - record.setCodeBlockVersion(BLOCKLY_LIBRARY_VERSION); - record.update(); - - return record; + + if (record == null) { + LOG.warn("Unable to update project {}", idProject); + return null; } - LOG.warn("Unable to update project {}", idProject); - return null; - } + record.setCode(code); + + record.setName(name); + record.setDescription(description); + record.setDescriptionHtml(descriptionHtml); + record.setPrivate(privateProject); + record.setShared(sharedProject); + + record.setModified(getCurrentTimestamp()); + record.setCodeBlockVersion(BLOCKLY_LIBRARY_VERSION); + + record.update(); + + return record; + } /** * Update the code blocks for a project * - * @param idProject - * @param code - * @return + * @param idProject the project primary key ID for the project that will receive the update + * + * @param code the code blocks to update within the project + * + * @return an updated ProjectRecord that contains the new code blocks */ @Override public ProjectRecord saveCode(Long idProject, String code) { LOG.info("Saving code for project {}.", idProject); ProjectRecord record = getProject(idProject, EDIT_MODE_ON); - if (record != null) { - // Update project record details - record.setCode(code); - record.setCodeBlockVersion(BLOCKLY_LIBRARY_VERSION); - record.setModified(getCurrentTimestamp()); - - // Update the record - ProjectRecord returningRecord = create - .update(Tables.PROJECT) - .set(record) - .returning() - .fetchOne(); + + if (record == null) { + LOG.error("Unable to save code for project {}", idProject); + return null; + } + + // Update project record details + record.setCode(code); + record.setCodeBlockVersion(BLOCKLY_LIBRARY_VERSION); + record.setModified(getCurrentTimestamp()); - // Return a copy of the updated project record - return returningRecord; + // Update the record and then return the updated record + return create + .update(Tables.PROJECT) + .set(record) + .returning() + .fetchOne(); } - - LOG.error("Unable to save code for project {}", idProject); - return null; - } + + /** - * TODO: add details. + * Update the code block in the specified project + * + * @param idProject + * @param code * - * @param idUser - * @param sort - * @param order - * @param limit - * @param offset * @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 List getUserProjects( - Long idUser, - TableSort sort, - TableOrder order, - Integer limit, - Integer offset) { - - LOG.info("Retreive projects for user {}.", idUser); + public ProjectRecord updateProjectCode(Long idProject, String code) { - SortField orderField = Tables.PROJECT.NAME.asc(); - if (TableOrder.desc == order) { - orderField = Tables.PROJECT.NAME.desc(); - } + LOG.info("Update code for project {}.", idProject); - return create.selectFrom(Tables.PROJECT) - .where(Tables.PROJECT.ID_USER.equal(idUser)) - .orderBy(orderField).limit(limit).offset(offset) - .fetch(); - } + // Retrieve the specified project + ProjectRecord record = create.selectFrom(Tables.PROJECT) + .where(Tables.PROJECT.ID.equal(idProject)) + .fetchOne(); - /** - * Return a list of community projects constrained by the parameters - * - * @param sort - * @param order - * @param limit - * @param offset + if (record != null) { + // Found the project. Verify that the current user owns it + Long idUser = BlocklyPropSecurityUtils.getCurrentUserId(); - * @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, - Integer offset) { - - LOG.info("Retrieve shared projects."); + if (idUser == 0) { + LOG.error("User is not logged in. Current user ID is zero for project {}", idProject); + return null; + } - SortField orderField = sort == null ? Tables.PROJECT.NAME.asc() : sort.getField().asc(); - if (TableOrder.desc == order) { - orderField = sort == null ? Tables.PROJECT.NAME.desc() : sort.getField().desc(); - } + /* ------------------------------------------------------------------------ + // Is the user ID in the current project zero. This should never happen + // but if it does, the project is orphaned. This can happen if the project + // owner's user profile has been removed incorrectly. The orphaned project + // should be either removed or converted to a community project. + // -----------------------------------------------------------------------*/ + if (record.getIdUser() == 0) { + LOG.error("Detected project user ID is zero for project {}", idProject); + return null; + } - // 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) - .fetch(); - } + // Get a timestamp used to update the modified field of the project record + GregorianCalendar cal = new GregorianCalendar(); + cal.setTime(new java.util.Date()); - /** - * TODO: add details. - * - * @param sort - * @param order - * @param limit - * @param offset - * @param idUser - * @return - */ - @Override - public List getSharedProjectsByUser( - TableSort sort, - TableOrder order, - Integer limit, - Integer offset, - Long idUser) { - - LOG.info("Retreive shared projects."); + // Update the project if the current user owns it + if (record.getIdUser().equals(idUser)) { + record.setCode(code); + record.setModified(cal); + record.setCodeBlockVersion(BLOCKLY_LIBRARY_VERSION); + 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; + } - SortField orderField = sort == null ? Tables.PROJECT.NAME.asc() : sort.getField().asc(); - if (TableOrder.desc == order) { - orderField = sort == null ? Tables.PROJECT.NAME.desc() : sort.getField().desc(); - } - Condition conditions = Tables.PROJECT.SHARED.eq(Boolean.TRUE); - if (idUser != null) { - conditions = conditions.and(Tables.PROJECT.ID_USER.eq(idUser)); + LOG.error("User {} tried and failed to update project {}.", idUser, idProject); + throw new UnauthorizedException(); + } + } else { + LOG.warn("Unable to project {}. Unknown reason.", idProject); + return null; } - return create.selectFrom(Tables.PROJECT) - .where(conditions) - .orderBy(orderField).limit(limit).offset(offset) - .fetch(); } + + /** - * TODO: add details. + * Get the number of projects owned by a specified userId * - * @param idUser - * @return + * @param idUser the primary key ID of the user who onws the projects to be counted + * + * @return the number of rows found to be owned by the specified user */ @Override public int countUserProjects(Long idUser) { - LOG.info("Count project for user {}.", idUser); + LOG.debug("Count project for user {}.", idUser); return create.fetchCount(Tables.PROJECT, Tables.PROJECT.ID_USER.equal(idUser)); } @@ -654,118 +736,44 @@ public int countSharedProjectsByUser(Long idUser) { LOG.info("Count shared projects for user {}.", idUser); Condition conditions = Tables.PROJECT.SHARED.equal(Boolean.TRUE); + if (idUser != null) { conditions = conditions.and(Tables.PROJECT.ID_USER.eq(idUser)); } + return create.fetchCount(Tables.PROJECT, conditions); } - /** - * TODO: add details. - * - * @param idProject - * @return - */ - @Override - 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); - } - return null; - } + + + + /************************************************************************** + * + * DELETE methods + * + **************************************************************************/ /** - * TODO: add details. + * Delete a project * * @param idProject + * The project primary key if + * * @return + * True if the project was successfully removed, otherwise false. */ @Override public boolean deleteProject(Long idProject) { + LOG.info("Delete project {}.", idProject); + return create.deleteFrom(Tables.PROJECT) .where(Tables.PROJECT.ID.equal(idProject)) .execute() > 0; } - /** - * 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); - record.setCodeBlockVersion(BLOCKLY_LIBRARY_VERSION); - 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(); - } - } else { - LOG.warn("Unable to project {}. Unknown reason.", idProject); - return null; - } - } @@ -837,6 +845,11 @@ public ProjectRecord saveProjectCodeAs(Long idProject, String code, String newNa return null; } + /************************************************************************** + * + * PRIVATE methods + * + **************************************************************************/ // Private over-ride of the public getProject() @@ -874,15 +887,18 @@ private ProjectRecord getProject(Long idProject, boolean toEdit) { /** + * Create a copy of an existing project and add it to the current user's library + * + * @param original The source project to copy into a new project * - * @param original * @return + * A copy of the original project with ownership updated to the current user */ private ProjectRecord doProjectClone(ProjectRecord original) { - - // TODO: Add based_on parameter as last argument - // TODO: Add parameter to support project settings - ProjectRecord cloned = createProject( + + // Create a new project using the details from the project record + // that was passed in. + return createProject( original.getName(), original.getDescription(), original.getDescriptionHtml(), @@ -891,23 +907,13 @@ private ProjectRecord doProjectClone(ProjectRecord original) { original.getBoard(), original.getPrivate(), original.getShared(), - original.getId() // set the parent project id + original.getId(), // set the parent project id (idProjectBasedOn) + original.getSettings() ); - -// cloned.setBasedOn(original.getId()); -// cloned.update(); - - // TODO WHAT IS THIS DOING? - create.update(Tables.PROJECT) - .set(Tables.PROJECT.BASED_ON, original.getId()) - .where(Tables.PROJECT.ID.equal(cloned.getId())); - - return cloned; } - /** * Produce a current timestamp * @@ -922,7 +928,6 @@ private GregorianCalendar getCurrentTimestamp() { - // Evaluate project code and replace any deprecated or updated blocks // // Return a ProjectRecord object. The code field may be altered to correct @@ -994,7 +999,14 @@ private ProjectRecord alterReadRecord(ProjectRecord record) { * @return */ private String randomString(int len) { + + // Used by the randomString function + final String AB = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#()*-+./:;" + + "=@[]^_`{|}~"; + + Random rnd = new Random(); StringBuilder sb = new StringBuilder(len); + for (int i = 0; i < len; i++) { sb.append(AB.charAt(rnd.nextInt(AB.length()))); } From 27d3dcf77f957ba940db6a205be35e53bd16e95d Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Wed, 27 Feb 2019 15:03:02 -0800 Subject: [PATCH 07/19] Refactor common code constructs into separate methods. Add code to handle missing data in project fields. Improve documentation. --- .../converter/ProjectConverter.java | 177 +++++++++++------- 1 file changed, 109 insertions(+), 68 deletions(-) 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; + } } From 468f5f9b3f832815d9a5c31577ebcfd0e38cbd6d Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Wed, 27 Feb 2019 22:38:52 -0800 Subject: [PATCH 08/19] Create a Project V2 API draft. --- .../blocklyprop/rest/RestV2Project.java | 718 ++++++++++++++++++ 1 file changed, 718 insertions(+) create mode 100644 src/main/java/com/parallax/server/blocklyprop/rest/RestV2Project.java 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..5eec0a6d --- /dev/null +++ b/src/main/java/com/parallax/server/blocklyprop/rest/RestV2Project.java @@ -0,0 +1,718 @@ +/* + * 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.jetbrains.annotations.NotNull; +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.Arrays; +import java.util.List; + + +/** + * REST endpoints for project persistence + * Version 2 + * + * @author Michel, J. Ewald + * + * @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 + * + * Version 2 supported endpoints + * CRUD.. + * + * CREATE + * [POST] /v2/project/ Creates a new project and returns it in the response body + * [POST] /v2/project/code Create a new project based on details on the request body + * + * 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 + * [PUT] /v2/project/code/{id} Updates the code block of a specific project + * + * 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; + } + + + + /** + * 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(); + } + + + /** + * Create a new project + * + * + * @return + * Returns a Json string containing the project details, including the new project ID if successful + * or an error message upon failure + */ + @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; + } + + LOG.info("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); + LOG.info("Returning JSON: {}", result); + + 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(); + } + } + + + + + + + + + + + /** + * 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(); + } + } + + + /** + * 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/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(); + } + } + + + + + + + + + + + + + + + + + + + + + + + + + /** + * Update the code in an existing project. + * + * This assumes that the project already exists. + * + * @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") + @Detail("Save project code") + @Name("UpdateProjectCode") + @Produces("application/json") + public Response saveProjectCode( + @FormParam("id") @ParameterDetail("Project identifier") @M() Long idProject, + @FormParam("code") @ParameterDetail("Project code") @M() String code) { + + LOG.info("REST:/rest/project/code/ POST request received for project '{}'", idProject); + + try { + + /* WARNING: + * ================================================================================= + * This call can create a new project record under specific circumstances and does + * not appear to provide any notification that this has occurred. + * ================================================================================= + */ + ProjectRecord savedProject = projectService.saveProjectCode(idProject, code); + + LOG.debug("Code for project {} has been saved", idProject); +/* + JsonObject result = projectConverter.toJson(savedProject,false); + result.addProperty("success", true); + return Response.ok(result.toString()).build(); +*/ + return Response.ok(buildConvertedResponse(savedProject)).build(); + } catch (AuthorizationException ae) { + LOG.warn("Project code not saved. Not Authorized"); + 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(); + } + } + + + + + /** + * 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") + @Name("Save project code") + @Produces("application/json") + public Response saveProjectCodeAs( + @FormParam("id") Long idProject, + @FormParam("code") String code, + @FormParam("name") String newName, + @FormParam("board") String newBoard) { + + LOG.info("REST:/rest/project/code-as/ POST request received for project '{}'", idProject); + + try { + LOG.info("Saving project '{}', '{}' as a new project", idProject, newName); + + ProjectRecord savedProject = projectService.saveProjectCodeAs( + idProject, + 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(); +*/ + 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(); + } + } + + + + + + /*-------------------------------------------------------------------------+ + * VERB URI Notes: | + * ------- ---------------------- -------------------------------------- | + * [POST] /v2/project/ Creates a new project. The project | + * details, including the new project ID, | + * are returned to the caller. | + *-------------------------------------------------------------------------*/ + + + + + + + + /* + * Update the details of an existing project V1 implementation + * + * @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") + public Response saveProject( + @FormParam("id") Long idProject, + @FormParam("name") String name, + @FormParam("description") String description, + @FormParam("description-html") String descriptionHtml, + @FormParam("sharing") String projectSharing, + @FormParam("type") ProjectType type, + @FormParam("board") String board) { + + LOG.info("REST:/rest/project/ POST request received for project '{}'", idProject); + + try { + boolean privateProject = false; + boolean sharedProject = false; + + if ("private".equalsIgnoreCase(projectSharing)) { + privateProject = true; + } else if ("shared".equalsIgnoreCase(projectSharing)) { + sharedProject = true; + } + + ProjectRecord savedProject = projectService.saveProject( + idProject, + name, + description, + descriptionHtml, + privateProject, + sharedProject, + type, + board); + LOG.debug("Project {} has been saved.", idProject); + + JsonObject result = projectConverter.toJson(savedProject,false); + LOG.debug("Returning JSON: {}", result); + + result.addProperty("success", true); + + return Response.ok(result.toString()).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(); + } + } + +*/ + + /** + * 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(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(); + } + +} From 5f3f8953894d4da6bed08d24afe37bccac1ba92d Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Wed, 27 Feb 2019 23:03:04 -0800 Subject: [PATCH 09/19] Add inline documentation for the POST / endpoint. --- .../blocklyprop/rest/RestV2Project.java | 62 ++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/parallax/server/blocklyprop/rest/RestV2Project.java b/src/main/java/com/parallax/server/blocklyprop/rest/RestV2Project.java index 5eec0a6d..a2f6dcce 100644 --- a/src/main/java/com/parallax/server/blocklyprop/rest/RestV2Project.java +++ b/src/main/java/com/parallax/server/blocklyprop/rest/RestV2Project.java @@ -147,10 +147,68 @@ public Response get() { /** * Create a 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 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 + * 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 + * } */ @POST @Detail("Create a new project") From 001f03482190cc516c29bc45c323f55df2222714 Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Thu, 28 Feb 2019 03:35:46 -0800 Subject: [PATCH 10/19] Improve inline documentation. Add exception to trap case where logged in user was attempting to load a private project that was not theirs. --- .../server/blocklyprop/rest/RestProject.java | 56 +++++++++++++------ 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/parallax/server/blocklyprop/rest/RestProject.java b/src/main/java/com/parallax/server/blocklyprop/rest/RestProject.java index 9d490bd1..2cb71f1d 100644 --- a/src/main/java/com/parallax/server/blocklyprop/rest/RestProject.java +++ b/src/main/java/com/parallax/server/blocklyprop/rest/RestProject.java @@ -27,9 +27,11 @@ import com.cuubez.visualizer.annotation.Name; import com.cuubez.visualizer.annotation.M; import com.cuubez.visualizer.annotation.ParameterDetail; + import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.inject.Inject; + import com.parallax.server.blocklyprop.TableOrder; import com.parallax.server.blocklyprop.TableSort; import com.parallax.server.blocklyprop.utils.RestProjectUtils; @@ -51,6 +53,7 @@ import javax.ws.rs.core.Response; import org.apache.shiro.authz.AuthorizationException; +import org.apache.shiro.authz.UnauthorizedException; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -60,6 +63,20 @@ * REST endpoints for project persistence * * @author Michel + * + * @implNote + * + * Supported endpoints: + * [POST] /project/ Save current project + * [GET] /project/list List projects + * [GET] /project/get/{id} Retrieve a project + * [POST] /project/code Update project code + * [POST] /project/code-as Create new project from existing project + * + * The code block is not returned with most of these calls because it is only used when + * the user is viewing or editing a project. This eliminates the unnecessary overhead of + * moving a code block that could easily dwarf the size of the other elements in the project + * record. */ @Path("/project") @@ -153,7 +170,7 @@ public Response get( if (idUser == null || idUser == 0) { // Current session is not logged in. - return Response.status(Response.Status.FORBIDDEN).build(); + return Response.status(Response.Status.UNAUTHORIZED).build(); } //Sanity checks - is the request reasonable @@ -176,7 +193,7 @@ public Response get( limit = REQUEST_LIMIT; } - // Check ofset from the beginning of the record set + // Check offset from the beginning of the record set if ((offset == null) || (offset < 0)) { offset = 0; } @@ -189,13 +206,19 @@ public Response get( return Response.status(Response.Status.NOT_FOUND).build(); } + LOG.info("Returning {} projects for user {}", userProjects.size(), idUser); + return Response.ok( returnProjectsJson( userProjects, projectService.countUserProjects(idUser))) .build(); } - + catch(UnauthorizedException ex) { + // User is logged in but attempting to access a secured resource + LOG.warn("Unauthorized access attempted"); + return Response.status(Response.Status.FORBIDDEN).build(); + } catch(Exception ex) { LOG.warn("Unable to process REST request."); LOG.warn("Error is {}", ex.getMessage()); @@ -276,18 +299,14 @@ public Response saveProjectCode( /* WARNING: * ================================================================================= - * This call can create a new project record under specific circumstances and does - * not appear to provide any notification that this has occurred. + * The call to the saveProjectCode() method can create a new project record under + * specific circumstances and does not appear to provide any notification that this + * has occurred. * ================================================================================= */ ProjectRecord savedProject = projectService.saveProjectCode(idProject, code); LOG.debug("Code for project {} has been saved", idProject); -/* - JsonObject result = projectConverter.toJson(savedProject,false); - result.addProperty("success", true); - return Response.ok(result.toString()).build(); -*/ return Response.ok(buildConvertedResponse(savedProject)).build(); } catch (AuthorizationException ae) { LOG.warn("Project code not saved. Not Authorized"); @@ -371,7 +390,6 @@ public Response saveProjectCodeAs( * or an error message upon failure */ @POST - @Path("/") @Detail("Save project") @Name("Save project") @Produces("application/json") @@ -439,15 +457,19 @@ public Response saveProject( * A String containing the array of the converted Json objects */ private String returnProjectsJson(@NotNull List projects, int projectCount) { + JsonObject result = new JsonObject(); - JsonArray jsonProjects = new JsonArray(); - for (ProjectRecord project : projects) { - jsonProjects.add(projectConverter.toListJson(project)); - } + if (projects.size() > 0) { + JsonArray jsonProjects = new JsonArray(); + + for (ProjectRecord project : projects) { + jsonProjects.add(projectConverter.toListJson(project)); + } - result.add("rows", jsonProjects); - result.addProperty("total", projectCount); + result.add("rows", jsonProjects); + result.addProperty("total", projectCount); + } return result.toString(); } From 614dc23431aab3eebbbc7cd12855c538a8bdc0c5 Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Thu, 28 Feb 2019 12:21:22 -0800 Subject: [PATCH 11/19] Add inline documentation. Add static method to validate ProjectType parameter. --- .../blocklyprop/utils/RestProjectUtils.java | 52 +++++++++++++++---- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/parallax/server/blocklyprop/utils/RestProjectUtils.java b/src/main/java/com/parallax/server/blocklyprop/utils/RestProjectUtils.java index 014dbe67..69e7f113 100644 --- a/src/main/java/com/parallax/server/blocklyprop/utils/RestProjectUtils.java +++ b/src/main/java/com/parallax/server/blocklyprop/utils/RestProjectUtils.java @@ -23,15 +23,23 @@ import com.parallax.server.blocklyprop.TableOrder; import com.parallax.server.blocklyprop.TableSort; +import com.parallax.server.blocklyprop.db.enums.ProjectType; -public class RestProjectUtils { +/** + * Project REST endpoint static utility methods + */ +public class RestProjectUtils { /** + * Validate that the provided value represents a valid column name on which to sort + * a list of projects + * + * @param sort is the column name to sort on. Note that not all project columns are + * sortable. * - * @param sort - * @return - * Return true if the provided sort is a valid item, otherwise return false + * @return a boolean true if the supplied value matches a sortable column, otherwise + * return a boolean false value */ public static boolean ValidateSortType(TableSort sort) { @@ -46,21 +54,45 @@ public static boolean ValidateSortType(TableSort sort) { return false; } + /** + * Validate the provided sort order. + * + * @param order Project sort order + * + * @return true if the sort order is valid, otherwise false + * + */ public static boolean ValidateSortOrder(TableOrder order) { - boolean parametersValid = false; - if (order != null) { for (TableOrder t : TableOrder.values()) { if (order == t) { - parametersValid = true; - break; + return true; } } } - return parametersValid; + return false; } -} + /** + * Validate the project language + * + * @param type is a ProjectType that indicates the underlying language used for the project + * + * @return true if the project type is a known type, otherwise return false + */ + public static boolean ValidateProjectType(ProjectType type) { + + if (type != null) { + for (ProjectType p : ProjectType.values()) { + if (type == p) { + return true; + } + } + } + + return false; + } +} From ac738049024ffad816a2cd688d07f1132ab0c25e Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Fri, 1 Mar 2019 09:38:39 -0800 Subject: [PATCH 12/19] Refactor readSession to reduce code paths. Add inline documentation. --- .../db/dao/impl/SessionDaoImpl.java | 94 ++++++++++++------- 1 file changed, 58 insertions(+), 36 deletions(-) diff --git a/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/SessionDaoImpl.java b/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/SessionDaoImpl.java index edab7453..968a2bf9 100644 --- a/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/SessionDaoImpl.java +++ b/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/SessionDaoImpl.java @@ -40,6 +40,7 @@ /** + * This is the session database layer. * * @author Michel */ @@ -48,37 +49,42 @@ public class SessionDaoImpl implements SessionDao { // Get a logger instance private static final Logger LOG = LoggerFactory.getLogger(SessionDaoImpl.class); - /** - * - */ + // Database connection private DSLContext create; - /** - * An instance of the application configuration settings - */ + //An instance of the application configuration settings private Configuration configuration; + + /** + * Get a copy of the database context + * + * @param dsl is the context provided by JooQ + */ @Inject public void setDSLContext(DSLContext dsl) { this.create = dsl; } + /** - * - * @param configuration + * Get a copy of the application configuration + * + * @param configuration is the context provided by the configuration manager */ @Inject public void setConfiguration(Configuration configuration) { this.configuration = configuration; } + /** - * - * @param session + * Create a new session + * + * @param session is a initialized SessrionRecord object */ @Override public void create(SessionRecord session) { - LOG.debug("Create a session. Timeout set to: {}", session.getTimeout()); // Log session details if the configuration file permits it @@ -108,49 +114,53 @@ public void create(SessionRecord session) { } /** - * - * @param idSession - * @return - * @throws NullPointerException + * Retrieve a session + * + * @param idSession is the unique identifier for the requested session + * + * @return a SessionRecord, otherwise throws a NullPointerException + * + * @throws NullPointerException raise an NPE if the session is not found, + * otherwise the caller expects to receive a valid session record */ @Override public SessionRecord readSession(String idSession) throws NullPointerException { - LOG.debug("Getting session {} details", idSession); - SessionRecord sessionRecord = null; - try { - sessionRecord = create.selectFrom(Tables.SESSION) + SessionRecord sessionRecord = create + .selectFrom(Tables.SESSION) .where(Tables.SESSION.IDSESSION.eq(idSession)) .fetchOne(); // Log session details if the configuration file permits it printSessionInfo("read", sessionRecord); + return sessionRecord; } catch (org.jooq.exception.DataAccessException sqex) { LOG.error("Database exception {}", sqex.getMessage()); + throw new NullPointerException("Session not found"); } - - return sessionRecord; } /** - * - * @param session - * @throws NullPointerException + * Update an existing session + * + * @param session is the unique identifier for the requested session + * + * @throws NullPointerException raise an NPE if the session yo be updated + * is not found */ @Override public void updateSession(SessionRecord session) throws NullPointerException { - - LOG.debug("Update a session"); + LOG.debug("Update session {}", session.getIdsession()); try { // Get the current session record SessionRecord dbRecord = readSession(session.getIdsession()); if (dbRecord == null) { - throw new NullPointerException("Session not found"); + throw new NullPointerException("Invalid session"); } dbRecord.setStarttimestamp(session.getStarttimestamp()); @@ -167,17 +177,18 @@ public void updateSession(SessionRecord session) throws NullPointerException { } catch (org.jooq.exception.DataAccessException sqex) { LOG.error("Database exception {}", sqex.getMessage()); - throw new NullPointerException("Database error"); + throw new NullPointerException("Session not found"); } } + /** - * - * @param idSession + * Remove a session record from the backing store + * + * @param idSession is the unique identifier for the requested session */ @Override public void deleteSession(String idSession) { - LOG.info("Deleting session {}", idSession); create.deleteFrom(Tables.SESSION) @@ -185,16 +196,25 @@ public void deleteSession(String idSession) { .execute(); } + /** - * - * @return + * Get a list of all active (unexpired) sessions + * + * @return a collection of session records */ @Override public Collection getActiveSessions() { return Arrays.asList(create.selectFrom(Tables.SESSION).fetchArray()); } - + + /** + * Provide detailed logging for the specified session + * + * @param action is a message that is embedded in the log record + * + * @param session is the session record to to log. + */ private void printSessionInfo(String action, SessionRecord session) { if (configuration.getBoolean("debug.session", false)) { try { @@ -206,9 +226,11 @@ private void printSessionInfo(String action, SessionRecord session) { HashMap attributes = (HashMap) in.readObject(); LOG.info("Session info: {}:{}", action, attributes); } - } catch (IOException ex) { + } + catch (IOException ex) { LOG.error("I/O error. {}",ex.getMessage()); - } catch (ClassNotFoundException cnfe) { + } + catch (ClassNotFoundException cnfe) { LOG.error("Class not found. {}", cnfe.getMessage()); } } From 496ca5884962bea6845c5f61e3977acb027effcc Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Thu, 7 Mar 2019 10:43:30 -0800 Subject: [PATCH 13/19] Add version 2 Project REST endpoints. Work is still ongoing. --- .../server/blocklyprop/db/dao/ProjectDao.java | 14 + .../db/dao/impl/ProjectDaoImpl.java | 135 ++++++++- .../server/blocklyprop/rest/RestUser.java | 2 +- .../blocklyprop/rest/RestV2Project.java | 198 ++++++++++++- .../blocklyprop/services/ProjectService.java | 269 +++++++++++++++++- .../services/impl/ProjectServiceImpl.java | 233 +++++++++++++-- 6 files changed, 792 insertions(+), 59 deletions(-) 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..a6661a10 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 @@ -55,6 +55,20 @@ 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, + boolean sharedProject, + Long idProjectBasedOn, + String settings); + + @Deprecated ProjectRecord createProject( String name, String description, diff --git a/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/ProjectDaoImpl.java b/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/ProjectDaoImpl.java index 93c7308a..e31fe08b 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 @@ -65,7 +65,7 @@ public class ProjectDaoImpl implements ProjectDao { // 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; @@ -103,7 +103,7 @@ public void setProjectSharingContext(ProjectSharingService projectSharingService */ @Override public ProjectRecord getProject(Long idProject) { - LOG.info("Retreiving data for project #{}", idProject); + LOG.info("Retrieving data for project #{}", idProject); ProjectRecord record = null; @@ -122,15 +122,108 @@ record = create 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; + + } + + + + /** * @@ -149,6 +242,7 @@ record = create * * @return a fully formed ProjectRecord or a null if an error is detected. */ + @Deprecated @Override public ProjectRecord createProject( String name, @@ -404,16 +498,23 @@ public List getUserProjects( Integer limit, Integer offset) { - LOG.info("Retreive projects for user {}.", idUser); + LOG.info("Retrieve projects for user {}.", idUser); - SortField orderField = Tables.PROJECT.NAME.asc(); + 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(); } @@ -436,7 +537,7 @@ public List getSharedProjects( 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) { @@ -494,9 +595,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; } /** @@ -552,10 +655,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); } @@ -762,13 +868,14 @@ private ProjectRecord doProjectClone(ProjectRecord original) { original.getBoard(), original.getPrivate(), original.getShared(), - original.getId() // set the parent project id + original.getId(), // set the parent project id + original.getSettings() ); // cloned.setBasedOn(original.getId()); // cloned.update(); - // WHAT IS THIS DOING? + // TODO: URGENT - Evaluate query to verify that it is updating only one record create.update(Tables.PROJECT) .set(Tables.PROJECT.BASED_ON, original.getId()) .where(Tables.PROJECT.ID.equal(cloned.getId())); diff --git a/src/main/java/com/parallax/server/blocklyprop/rest/RestUser.java b/src/main/java/com/parallax/server/blocklyprop/rest/RestUser.java index ad881022..ed35eb13 100644 --- a/src/main/java/com/parallax/server/blocklyprop/rest/RestUser.java +++ b/src/main/java/com/parallax/server/blocklyprop/rest/RestUser.java @@ -72,7 +72,7 @@ public void setUserService(UserService userService) { * Returns a list of all user objects. */ @GET - @Path("/") +// @Path("/") @Detail("Get all users") @Name("Get all users") @Produces("application/json") diff --git a/src/main/java/com/parallax/server/blocklyprop/rest/RestV2Project.java b/src/main/java/com/parallax/server/blocklyprop/rest/RestV2Project.java index a2f6dcce..6cb098cb 100644 --- a/src/main/java/com/parallax/server/blocklyprop/rest/RestV2Project.java +++ b/src/main/java/com/parallax/server/blocklyprop/rest/RestV2Project.java @@ -38,7 +38,6 @@ import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.authz.UnauthorizedException; import org.apache.commons.lang.Validate; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,16 +47,19 @@ import java.util.Arrays; import java.util.List; +import org.jetbrains.annotations.NotNull; + /** - * REST endpoints for project persistence - * Version 2 + * 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 * - * Supported endpoints: + * Version 1 Supported endpoints: * [POST] /project/ Save current project * [GET] /project/list List projects * [GET] /project/get/{id} Retrieve a project @@ -65,11 +67,11 @@ * [POST] /project/code-as Create new project from existing project * * Version 2 supported endpoints - * CRUD.. - * + * ----------------------------------------------------------------------------------------------------- * CREATE * [POST] /v2/project/ Creates a new project and returns it in the response body - * [POST] /v2/project/code Create a new project based on details on the request 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 @@ -77,10 +79,10 @@ * * UPDATE * [PUT] /v2/project/{id} Updates s specific project. Project details are in the request body - * [PUT] /v2/project/code/{id} Updates the code block of a specific project * * DELETE * [DELETE] /v2/project/{id} Destroys the project specified by {id} + * ----------------------------------------------------------------------------------------------------- */ @Path("/v2/project") @@ -144,8 +146,9 @@ public Response get() { } + /** - * Create a new project + * Create a new project. Access is limited to authenticated users. * * @param projectName * is a required string parameter containing the project name @@ -209,6 +212,22 @@ public Response get() { * "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") @@ -244,7 +263,12 @@ public Response createProject( sharedProject = true; } - LOG.info("Saving the project"); + // 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); @@ -254,8 +278,6 @@ public Response createProject( } JsonObject result = projectConverter.toJson(savedProject,false); - LOG.info("Returning JSON: {}", result); - result.addProperty("success", true); return Response.ok(result.toString()).build(); @@ -266,7 +288,6 @@ public Response createProject( } catch (IllegalArgumentException aex) { LOG.warn("Illegal argument exception detected. {}", aex.getMessage()); - return Response.status(Response.Status.NOT_ACCEPTABLE).build(); } catch (AuthorizationException ae) { @@ -283,7 +304,155 @@ public Response createProject( + /** + * 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/ 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(); + } + } @@ -772,5 +941,6 @@ private String buildConvertedResponse(ProjectRecord project) { return result.toString(); } - } + + 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..de536eca 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,263 @@ 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); + + - List getSharedProjectsByUser(TableSort tablesSort, TableOrder order, Integer limit, Integer offset, Long idUser); + ProjectRecord createProject( + String name, + String description, + String descriptionHtml, + String projectCode, + boolean privateProject, + boolean sharedProject, + ProjectType type, + String board, + String settings); + + + // 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); - boolean deleteProject(Long idProject); + ProjectRecord saveProject( + Long idProject, + String name, + String description, + String descriptionHtml, + String projectCode, + boolean privateProject, + boolean sharedProject, + ProjectType type, + String board, + String settings); + + /** + * Save the code blocks for a specified project + * + * @param idProject + * @param code + * + * @return + */ ProjectRecord saveProjectCode(Long idProject, String code); - public ProjectRecord saveProjectCodeAs(Long idProject, String code, String newName, String newBoard); + /** + * Create a new project owned by the current user that is a copy of the original project + * + * @param idProject + * @param code + * @param newName + * @param newBoard + * + * @return + */ + ProjectRecord saveProjectCodeAs(Long idProject, String code, String newName, String newBoard); + + + /** + * Create a copy of the existing project into the current user's library + * + * @param idProject + * + * @return + */ + ProjectRecord cloneProject(Long idProject); + + + //------------- + // Delete + //------------- + + /** + * Destroy the specified project only if the project is owned by the current user + * + * @param idProject + * @return + */ + boolean deleteProject(Long idProject); } diff --git a/src/main/java/com/parallax/server/blocklyprop/services/impl/ProjectServiceImpl.java b/src/main/java/com/parallax/server/blocklyprop/services/impl/ProjectServiceImpl.java index ad3ada24..abeefbee 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,55 @@ 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. */ @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 +77,9 @@ public void setProjectSharingService(ProjectSharingService projectSharingService /** * Create a new project record - * + *

+ * This method signature is deprecated. + * * @param name * @param description * @param descriptionHtml @@ -61,12 +89,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 +107,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 +253,7 @@ public ProjectRecord createProject( * @param board * @return */ + @Deprecated @Override public ProjectRecord saveProject( Long idProject, String name, String description, @@ -112,10 +273,34 @@ 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); + } + } + + @Override public ProjectRecord getProjectOwnedByThisUser(Long idProject) { + ProjectRecord projectRecord = projectDao.getProject(idProject); + if (projectRecord != null) { if (projectRecord.getIdUser().equals(BlocklyPropSecurityUtils.getCurrentUserId())) { return projectRecord; @@ -208,7 +393,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); } @@ -267,15 +458,25 @@ public ProjectRecord saveProjectCode(Long idProject, String code) { /** + * Create a new project, specifying a new project name and board type, based on an existing project * - * @param idProject - * @param code - * @param newName - * @param newBoard - * @return + * @param idProject the primary key ID of the source project + * + * @param code is the code blocks to add to the new project + * + * @param newName is the nae that will be assigned to the new project + * + * @param newBoard is the new board type that will be assigned to the new project + * + * @return a ProjectRecord representing the newly created project */ @Override - public ProjectRecord saveProjectCodeAs(Long idProject, String code, String newName, String newBoard) { + public ProjectRecord saveProjectCodeAs( + Long idProject, + String code, + String newName, + String newBoard) { + return projectDao.saveProjectCodeAs(idProject, code, newName, newBoard); } From b65209ac9f2aa923da225f0cfa7a3443a058d554 Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Thu, 7 Mar 2019 10:45:21 -0800 Subject: [PATCH 14/19] Jooq generated updates for adding project settings to the application. --- .../blocklyprop/db/generated/tables/Motd.java | 2 +- .../db/generated/tables/Project.java | 7 ++- .../db/generated/tables/pojos/Motd.java | 2 +- .../db/generated/tables/pojos/Project.java | 17 ++++- .../generated/tables/records/MotdRecord.java | 2 +- .../tables/records/ProjectRecord.java | 63 +++++++++++++++---- 6 files changed, 76 insertions(+), 17 deletions(-) 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); } } From 00853c31885e814faf17919236c9e43b2ea59edd Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Thu, 7 Mar 2019 10:47:07 -0800 Subject: [PATCH 15/19] Add support for version 2 Project REST endpoints. --- .../server/blocklyprop/config/RestModule.java | 5 +++ .../blocklyprop/config/SetupConfig.java | 37 ++++++++----------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/parallax/server/blocklyprop/config/RestModule.java b/src/main/java/com/parallax/server/blocklyprop/config/RestModule.java index 04e11b84..999523c3 100644 --- a/src/main/java/com/parallax/server/blocklyprop/config/RestModule.java +++ b/src/main/java/com/parallax/server/blocklyprop/config/RestModule.java @@ -25,10 +25,13 @@ import com.parallax.server.blocklyprop.rest.RestMotd; import com.parallax.server.blocklyprop.rest.RestProfile; import com.parallax.server.blocklyprop.rest.RestProject; +import com.parallax.server.blocklyprop.rest.RestV2Project; import com.parallax.server.blocklyprop.rest.RestSharedProject; import com.parallax.server.blocklyprop.rest.RestUser; + import com.sun.jersey.guice.JerseyServletModule; import com.sun.jersey.guice.spi.container.servlet.GuiceContainer; + import javax.ws.rs.ext.MessageBodyReader; import javax.ws.rs.ext.MessageBodyWriter; import org.codehaus.jackson.jaxrs.JacksonJsonProvider; @@ -45,6 +48,8 @@ protected void configureServlets() { bind(RestCompile.class); bind(RestUser.class); bind(RestProject.class); + bind(RestV2Project.class); + bind(RestSharedProject.class); bind(RestProfile.class); bind(RestMotd.class); diff --git a/src/main/java/com/parallax/server/blocklyprop/config/SetupConfig.java b/src/main/java/com/parallax/server/blocklyprop/config/SetupConfig.java index 49aae174..fd830cdb 100644 --- a/src/main/java/com/parallax/server/blocklyprop/config/SetupConfig.java +++ b/src/main/java/com/parallax/server/blocklyprop/config/SetupConfig.java @@ -26,11 +26,12 @@ import com.google.inject.Injector; import com.google.inject.servlet.GuiceServletContextListener; -import com.parallax.server.blocklyprop.SessionData; import com.parallax.server.blocklyprop.jsp.Properties; import com.parallax.server.blocklyprop.monitoring.Monitor; + import java.sql.Driver; import java.sql.DriverManager; +import java.sql.SQLException; import java.util.Enumeration; import javax.servlet.ServletContextEvent; import org.apache.commons.configuration.Configuration; @@ -40,22 +41,25 @@ import org.slf4j.LoggerFactory; + /** * * @author Michel */ public class SetupConfig extends GuiceServletContextListener { - /** - * Application-specific configuration options - */ + // Application logging connector + private final Logger LOG = LoggerFactory.getLogger(SetupConfig.class); + + // Application-specific configuration options private Configuration configuration; - + + /** - * Application logging connector + * Create a Guice injector object + * + * @return a Guice injector object */ - private final Logger LOG = LoggerFactory.getLogger(SetupConfig.class); - @Override protected Injector getInjector() { readConfiguration(); @@ -65,9 +69,9 @@ protected Injector getInjector() { @Override protected void configure() { + LOG.info("Binding Configuration class"); bind(Configuration.class).toInstance(configuration); - bind(SessionData.class); bind(Properties.class).asEagerSingleton(); bind(Monitor.class).asEagerSingleton(); @@ -81,7 +85,6 @@ protected void configure() { install(new ServletsModule()); install(new RestModule()); } - }); } @@ -119,26 +122,16 @@ public void contextDestroyed(ServletContextEvent servletContextEvent) { // This manually deregisters JDBC driver, which prevents Tomcat 7 from // complaining about memory leaks into this class -/* while (drivers.hasMoreElements()) { Driver driver = drivers.nextElement(); try { DriverManager.deregisterDriver(driver); - LOG.info("deregistering jdbc driver: {}",driver); + LOG.info("Deregister the jdbc driver: {}",driver); } catch (SQLException sqlE) { - LOG.error("Error deregistering driver %s", driver); + LOG.error("Unable to deregister the jdbc driver {}", driver.toString()); LOG.error("{}", sqlE.getSQLState()); } - - } - - // Shut down the loggers. Assume SLF4J is bound to logback-classic - // in the current environment - LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); - if (loggerContext != null) { - loggerContext.stop(); } -*/ } } From 4c1730a949576add7840dcf0161b16876467d82a Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Thu, 7 Mar 2019 12:15:25 -0800 Subject: [PATCH 16/19] Add code to support Project Update REST endpoint. --- .../server/blocklyprop/db/dao/ProjectDao.java | 12 ++ .../db/dao/impl/ProjectDaoImpl.java | 39 ++++++ .../blocklyprop/rest/RestV2Project.java | 112 ++++++++++++++++-- .../blocklyprop/services/ProjectService.java | 3 + .../services/impl/ProjectServiceImpl.java | 22 +++- 5 files changed, 180 insertions(+), 8 deletions(-) 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 a6661a10..14875aea 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 @@ -8,6 +8,7 @@ import com.parallax.server.blocklyprop.TableOrder; import com.parallax.server.blocklyprop.TableSort; import com.parallax.server.blocklyprop.db.enums.ProjectType; +import com.parallax.server.blocklyprop.db.generated.tables.Project; import com.parallax.server.blocklyprop.db.generated.tables.records.ProjectRecord; import java.util.List; @@ -106,6 +107,17 @@ ProjectRecord updateProject( 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, String code); 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 e31fe08b..d33fc33b 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 @@ -447,6 +447,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 * diff --git a/src/main/java/com/parallax/server/blocklyprop/rest/RestV2Project.java b/src/main/java/com/parallax/server/blocklyprop/rest/RestV2Project.java index 6cb098cb..3ee1d771 100644 --- a/src/main/java/com/parallax/server/blocklyprop/rest/RestV2Project.java +++ b/src/main/java/com/parallax/server/blocklyprop/rest/RestV2Project.java @@ -69,13 +69,13 @@ * 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 + * [POST] /v2/project/ [X] Creates a new project and returns it in the response body + * [POST] /v2/project/{id} [X] 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 + * [GET] /v2/project/ [X] Returns a list of projects; parameters in request body + * [GET] /v2/project/{id} [X] Returns the specific project if authorized * * UPDATE * [PUT] /v2/project/{id} Updates s specific project. Project details are in the request body @@ -302,8 +302,6 @@ public Response createProject( - - /** * 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 @@ -645,7 +643,7 @@ public Response get( @Produces("application/json") public Response get(@PathParam("id") @ParameterDetail("Project identifier") Long idProject) { - LOG.info("REST:/rest/project/get/ Get request received for project '{}'", idProject); + LOG.info("REST:/rest/v2/project/get/ Get request received for project '{}'", idProject); try { ProjectRecord project = projectService.getProject(idProject); @@ -678,7 +676,70 @@ public Response get(@PathParam("id") @ParameterDetail("Project identifier") Long + @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, 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"); + result.addProperty("message", "Update project"); + return Response.ok(result.toString()).build(); + + } @@ -941,6 +1002,43 @@ private String buildConvertedResponse(ProjectRecord project) { return result.toString(); } + + + private ProjectRecord updateProjectRecordFields( + ProjectRecord project, String projectName, String description, String descriptionHtml, + String code, ProjectType type, String board, String settings) { + + if (projectName != null) { + project.setName(projectName); + } + + if (description != null) { + project.setDescription(description); + } + + if (descriptionHtml != null) { + project.setDescriptionHtml(descriptionHtml); + } + + if (code != null) { + project.setCode(code); + } + + if (type != null) { + project.setType(type); + } + + if (board != null) { + project.setBoard(board); + } + + if (settings != null) { + project.setSettings(settings); + } + + return project; + + } } diff --git a/src/main/java/com/parallax/server/blocklyprop/services/ProjectService.java b/src/main/java/com/parallax/server/blocklyprop/services/ProjectService.java index de536eca..e7695a20 100644 --- a/src/main/java/com/parallax/server/blocklyprop/services/ProjectService.java +++ b/src/main/java/com/parallax/server/blocklyprop/services/ProjectService.java @@ -242,6 +242,9 @@ ProjectRecord saveProject( String settings); + // Update an existing project + ProjectRecord saveProject(ProjectRecord project); + /** * Save the code blocks for a specified project * 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 abeefbee..57c5d816 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,7 +20,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.shiro.authz.UnauthorizedException; -import org.apache.commons.lang.StringUtils; +// import org.apache.commons.lang.StringUtils; /** * Implementation of project services layer @@ -38,6 +38,12 @@ * * 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 @@ -296,6 +302,20 @@ public ProjectRecord saveProject( } + // 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) { From 1d73749b9d81892ec20b62312b47e1fda5c29a2c Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Thu, 7 Mar 2019 15:19:32 -0800 Subject: [PATCH 17/19] Add inline documentation. --- .../blocklyprop/db/dao/impl/ProjectDaoImpl.java | 2 +- .../blocklyprop/services/impl/ProjectServiceImpl.java | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) 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 d33fc33b..15b5ad65 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 @@ -711,7 +711,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) { 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 57c5d816..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 @@ -455,11 +455,11 @@ public ProjectRecord cloneProject(Long idProject) { @Override public boolean deleteProject(Long idProject) { - LOG.info("Deleting project {}", idProject); - // Remove the project shared key if it exists. + LOG.info("Deleting project {} shared link", idProject); projectSharingService.deleteSharedProject(idProject); + LOG.info("Deleting project {}", idProject); return projectDao.deleteProject(idProject); } @@ -467,9 +467,10 @@ public boolean deleteProject(Long idProject) { /** * Update the code block in the specified project * - * @param idProject - * @param code - * @return + * @param idProject is the primary key ID of the source project + * @param code is the code blocks to add to the new project + * + * @return a ProjectRecord representing the updated project */ @Override public ProjectRecord saveProjectCode(Long idProject, String code) { From 4d235e154bc362efd7c84277b9d74fa53aeff5d5 Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Thu, 7 Mar 2019 15:20:29 -0800 Subject: [PATCH 18/19] Implement project REST DELETE /{id} endpoint. --- .../blocklyprop/rest/RestV2Project.java | 313 ++++++------------ 1 file changed, 107 insertions(+), 206 deletions(-) diff --git a/src/main/java/com/parallax/server/blocklyprop/rest/RestV2Project.java b/src/main/java/com/parallax/server/blocklyprop/rest/RestV2Project.java index 3ee1d771..e85aca9f 100644 --- a/src/main/java/com/parallax/server/blocklyprop/rest/RestV2Project.java +++ b/src/main/java/com/parallax/server/blocklyprop/rest/RestV2Project.java @@ -44,7 +44,6 @@ import javax.ws.rs.*; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import java.util.Arrays; import java.util.List; import org.jetbrains.annotations.NotNull; @@ -69,13 +68,13 @@ * Version 2 supported endpoints * ----------------------------------------------------------------------------------------------------- * CREATE - * [POST] /v2/project/ [X] Creates a new project and returns it in the response body - * [POST] /v2/project/{id} [X] Creates a new project using the contents of the provided + * [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/ [X] Returns a list of projects; parameters in request body - * [GET] /v2/project/{id} [X] Returns the specific project if authorized + * [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 @@ -106,6 +105,7 @@ public class RestV2Project { private static final int REQUEST_LIMIT = 100; + /** * Connect to the project service object * @@ -118,6 +118,7 @@ public void setProjectService(ProjectService projectService) { } + /** * Connect to the project converter object * @@ -131,6 +132,7 @@ public void setProjectConverter(ProjectConverter projectConverter) { + // GET /v2/project/ping /** * Test endpoint to verify that the class is reachable * @@ -147,6 +149,7 @@ public Response get() { + // POST /v2/project/ /** * Create a new project. Access is limited to authenticated users. * @@ -302,6 +305,7 @@ public Response createProject( + // 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 @@ -342,7 +346,7 @@ public Response createProject( * * VERB URI Notes: * ------- ---------------------- ---------------------------------------------- - * [POST] /v2/project/ Create a new project from the data provided. + * [POST] /v2/project/{id} Create a new project from the data provided. * The service returns a Json string containing * the new project details. * @@ -454,7 +458,7 @@ public Response createProjectCopy( - + // GET /v2/project/ /** * Return a list of projects owned by the currently authenticated user. * @@ -598,6 +602,7 @@ public Response get( } + // GET /v2/project/{id} /** * Retreive a project based on the supplied project ID * @@ -676,6 +681,41 @@ public Response get(@PathParam("id") @ParameterDetail("Project identifier") Long + // 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") @@ -691,7 +731,7 @@ public Response update( @FormParam("board") String board, @FormParam("settings") String settings) { - LOG.info("REST:/rest/v2/project/{} PUT request received for project", idProject, idProject); + LOG.info("REST:/rest/v2/project/{} PUT request received for project", idProject); ProjectRecord project; @@ -715,6 +755,7 @@ public Response update( 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 @@ -731,230 +772,72 @@ public Response update( } // Return the saved record - - JsonObject result = projectConverter.toJson(project,false); result.addProperty("result", "success"); - result.addProperty("message", "Update project"); return Response.ok(result.toString()).build(); - } - - - - - - - - - - - - - - - - - + // DELETE /v2/project/{id} /** - * Update the code in an existing project. - * - * This assumes that the project already exists. - * - * @param idProject The project key ID - * @param code the project blocks code string + * Delete a project only if the project is owned by the currently logged in user. * - * @return - * Returns a Json string containing the project details if the update was successful - * or an error message upon failure - */ - @POST - @Path("/code") - @Detail("Save project code") - @Name("UpdateProjectCode") - @Produces("application/json") - public Response saveProjectCode( - @FormParam("id") @ParameterDetail("Project identifier") @M() Long idProject, - @FormParam("code") @ParameterDetail("Project code") @M() String code) { - - LOG.info("REST:/rest/project/code/ POST request received for project '{}'", idProject); - - try { - - /* WARNING: - * ================================================================================= - * This call can create a new project record under specific circumstances and does - * not appear to provide any notification that this has occurred. - * ================================================================================= - */ - ProjectRecord savedProject = projectService.saveProjectCode(idProject, code); - - LOG.debug("Code for project {} has been saved", idProject); -/* - JsonObject result = projectConverter.toJson(savedProject,false); - result.addProperty("success", true); - return Response.ok(result.toString()).build(); -*/ - return Response.ok(buildConvertedResponse(savedProject)).build(); - } catch (AuthorizationException ae) { - LOG.warn("Project code not saved. Not Authorized"); - 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(); - } - } - - - + * @param idProject is the identifier of the project to use as a + * source for the new project - /** - * 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 + * @return a Json formatted string containing "success" otherwise returns an + * error code indicating the type of failure encountered. */ - @POST - @Path("/code-as") - @Detail("Save project code") - @Name("Save project code") + @DELETE + @Path("/{id}") + @Detail("Delete a project by the project id") + @Name("Delete Project by id") @Produces("application/json") - public Response saveProjectCodeAs( - @FormParam("id") Long idProject, - @FormParam("code") String code, - @FormParam("name") String newName, - @FormParam("board") String newBoard) { - - LOG.info("REST:/rest/project/code-as/ POST request received for project '{}'", idProject); - - try { - LOG.info("Saving project '{}', '{}' as a new project", idProject, newName); - - ProjectRecord savedProject = projectService.saveProjectCodeAs( - idProject, - 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(); -*/ - 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(); - } - } - - - - - - /*-------------------------------------------------------------------------+ - * VERB URI Notes: | - * ------- ---------------------- -------------------------------------- | - * [POST] /v2/project/ Creates a new project. The project | - * details, including the new project ID, | - * are returned to the caller. | - *-------------------------------------------------------------------------*/ - - - - - - - - /* - * Update the details of an existing project V1 implementation - * - * @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 - */ + public Response delete( + @PathParam("id") @ParameterDetail("Project identifier") Long idProject) { -/* - @POST -// @Path("/") - @Detail("Save project") - @Name("Save project") - @Produces("application/json") - public Response saveProject( - @FormParam("id") Long idProject, - @FormParam("name") String name, - @FormParam("description") String description, - @FormParam("description-html") String descriptionHtml, - @FormParam("sharing") String projectSharing, - @FormParam("type") ProjectType type, - @FormParam("board") String board) { + LOG.info("REST:/rest/v2/project/{} DELETE request received for project", idProject); - LOG.info("REST:/rest/project/ POST request received for project '{}'", idProject); + ProjectRecord project; + // Get the specified project try { - boolean privateProject = false; - boolean sharedProject = false; + project = projectService.getProject(idProject); - if ("private".equalsIgnoreCase(projectSharing)) { - privateProject = true; - } else if ("shared".equalsIgnoreCase(projectSharing)) { - sharedProject = true; + if (project == null) { + LOG.info("Project {} was not found", idProject); + return Response.status(Response.Status.NOT_FOUND).build(); } - ProjectRecord savedProject = projectService.saveProject( - idProject, - name, - description, - descriptionHtml, - privateProject, - sharedProject, - type, - board); - LOG.debug("Project {} has been saved.", idProject); + // 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(); + } - JsonObject result = projectConverter.toJson(savedProject,false); - LOG.debug("Returning JSON: {}", result); + LOG.info("Project {} is ready to be deleted."); - result.addProperty("success", true); + // 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 (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()); + 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 @@ -992,7 +875,7 @@ private String returnProjectsJson(@NotNull List projects, int pro * * @return a Json string representing the project contents and the operation results message */ - private String buildConvertedResponse(ProjectRecord project) { + private String buildConvertedResponse(@NotNull ProjectRecord project) { /* Convert the project record to a Json object */ JsonObject result = projectConverter.toJson(project,false); @@ -1004,8 +887,27 @@ private String buildConvertedResponse(ProjectRecord project) { } + /** + * 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( - ProjectRecord project, String projectName, String description, String descriptionHtml, + @NotNull ProjectRecord project, + String projectName, String description, String descriptionHtml, String code, ProjectType type, String board, String settings) { if (projectName != null) { @@ -1037,7 +939,6 @@ private ProjectRecord updateProjectRecordFields( } return project; - } } From ed0b7f8245e1e9bdc2a4b37e0ce8c638e6f2b64c Mon Sep 17 00:00:00 2001 From: Jim Ewald Date: Thu, 7 Mar 2019 20:40:53 -0800 Subject: [PATCH 19/19] Add URL path for version 2 project REST API interface. --- src/main/resources/shiro.ini | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/resources/shiro.ini b/src/main/resources/shiro.ini index 23a16291..05c06763 100644 --- a/src/main/resources/shiro.ini +++ b/src/main/resources/shiro.ini @@ -157,7 +157,17 @@ shiro.loginUrl = /login.jsp # REST api and api documentation /apidoc = anon /rest/shared/** = anon, ssl -/rest/** = authc, ssl + +# Version 2 of the Blockly API +# /rest/v2/** = authc, ssl +/rest/v2/** = anon, ssl + +# Version 1 of the Blockly API +#/rest/** = authc, ssl +/rest/** = anon, ssl + + + # Authorized projects /createproject = authc, noSessionCreation, ssl