From ddd7df3a51606eeb490df720b9ae310228a8abf4 Mon Sep 17 00:00:00 2001 From: Lars Johansson <lars.johansson@ess.eu> Date: Thu, 16 Jun 2022 11:12:02 +0200 Subject: [PATCH] Add endpoints for names and structures to handle operations with upload and download of Excel files Add endpoints for names for operations create, read, update, delete. Add endpoints for structures for operations create, read, update, delete and approve, cancel, reject. --- pom.xml | 5 + .../openepics/names/rest/api/v1/INames.java | 393 ++++++++++-- .../names/rest/api/v1/IStructures.java | 607 +++++++++++++++--- .../names/rest/beans/element/NameElement.java | 12 +- .../beans/element/NameElementCommand.java | 12 +- .../rest/beans/element/StructureElement.java | 2 +- .../element/StructureElementCommand.java | 4 +- .../rest/controller/NamesController.java | 125 +++- .../rest/controller/StructuresController.java | 203 ++++++ .../org/openepics/names/util/ExcelUtil.java | 536 ++++++++++++++++ .../org/openepics/names/util/TextUtil.java | 2 + .../resources/application-docker.properties | 5 +- src/main/resources/application.properties | 5 +- .../static/templates/NameElementCommand.xlsx | Bin 0 -> 7168 bytes .../templates/StructureElementCommand.xlsx | Bin 0 -> 7237 bytes 15 files changed, 1756 insertions(+), 155 deletions(-) create mode 100644 src/main/java/org/openepics/names/util/ExcelUtil.java create mode 100644 src/main/resources/static/templates/NameElementCommand.xlsx create mode 100644 src/main/resources/static/templates/StructureElementCommand.xlsx diff --git a/pom.xml b/pom.xml index b8ceac8b..2c64c231 100644 --- a/pom.xml +++ b/pom.xml @@ -65,6 +65,11 @@ <artifactId>findbugs-annotations</artifactId> <version>3.0.1</version> </dependency> + <dependency> + <groupId>org.apache.poi</groupId> + <artifactId>poi-ooxml</artifactId> + <version>5.2.2</version> + </dependency> <dependency> <groupId>org.springdoc</groupId> diff --git a/src/main/java/org/openepics/names/rest/api/v1/INames.java b/src/main/java/org/openepics/names/rest/api/v1/INames.java index 45f9112c..70f80fc7 100644 --- a/src/main/java/org/openepics/names/rest/api/v1/INames.java +++ b/src/main/java/org/openepics/names/rest/api/v1/INames.java @@ -27,6 +27,8 @@ import org.openepics.names.rest.beans.response.Response; import org.openepics.names.rest.beans.response.ResponseBoolean; import org.openepics.names.rest.beans.response.ResponseBooleanList; import org.openepics.names.rest.beans.response.ResponsePageNameElements; +import org.openepics.names.util.ExcelUtil; +import org.springframework.core.io.Resource; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -36,6 +38,7 @@ import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; @@ -61,22 +64,23 @@ public interface INames { /* FieldName - UUID, NAME, NAMEEQUIVALENCE, SYSTEMSTRUCTURE, DEVICESTRUCTURE, DESCRIPTION - NameElement - uuid (UUID) - systemgroup (UUID) - system (UUID) - subsystem (UUID) - devicetype (UUID) - systemstructure (String)(mnemonic path) - devicestructure (String)(mnemonic path) - index (String) - name (String) - description (String) - status (Status) - latest (Boolean) - deleted (Boolean) - when (Date) - who (String) - comment (String) + NameElementCommand - subset of NameElement attributes + NameElement + uuid (UUID) + parentsystemstructure (UUID) + parentdevicestructure (UUID) + index (String) + description (String) + comment (String) + ---------------------------------- + systemstructure (String) + devicestructure (String) + name (String) + status (Status) + latest (Boolean) + deleted (Boolean) + when (Date) + who (String) authentication/authorization 3 levels - no, user, administrator @@ -85,29 +89,34 @@ public interface INames { ( administrator ) Methods - create POST /names - createNames(List<NameElementCommand>) + create POST /names - createNames (List<NameElementCommand>) + create POST /names/upload - createNames (MultipartFile) ---------------------------------------------------------------------------------------------------- - read GET /names - readNames(Boolean, FieldName[], String[], FieldName, Boolean, Integer, Integer) - read GET /names/{name} - readNames(String, FieldName, Boolean, Integer, Integer) - read GET /names/systemstructure/{mnemonicpath} - readNamesSystemStructure(String, FieldName, Boolean, Integer, Integer) - read GET /names/devicestructure/{mnemonicpath} - readNamesDeviceStructure(String, FieldName, Boolean, Integer, Integer) - read GET /names/history/{uuid} - readNamesHistory(String, FieldName, Boolean, Integer, Integer) + read GET /names - readNames (Boolean, FieldName[], String[], FieldName, Boolean, Integer, Integer) + read GET /names/download - readNamesDownload (Boolean, FieldName[], String[], FieldName, Boolean, Integer, Integer) + read GET /names/{name} - readNames (String, FieldName, Boolean, Integer, Integer) + read GET /names/systemstructure/{mnemonicpath} - readNamesSystemStructure (String, FieldName, Boolean, Integer, Integer) + read GET /names/devicestructure/{mnemonicpath} - readNamesDeviceStructure (String, FieldName, Boolean, Integer, Integer) + read GET /names/history/{uuid} - readNamesHistory (String, FieldName, Boolean, Integer, Integer) ---------------------------------------------------------------------------------------------------- - read GET /names/equivalence/{name} - equivalenceName(String) - read GET /names/exists/{name} - existsName(String) - read GET /names/islegacy/{name} - isLegacyName(String) - read GET /names/isvalidtocreate/{name} - isValidToCreateName(String) + read GET /names/equivalence/{name} - equivalenceName (String) + read GET /names/exists/{name} - existsName (String) + read GET /names/islegacy/{name} - isLegacyName (String) + read GET /names/isvalidtocreate/{name} - isValidToCreateName (String) ---------------------------------------------------------------------------------------------------- - read GET /names/validatecreate - validateNamesCreate(List<NameElementCommand>) - read GET /names/validateupdate - validateNamesUpdate(List<NameElementCommand>) - read GET /names/validatedelete - validateNamesDelete(List<NameElementCommand>) + read GET /names/validatecreate - validateNamesCreate (List<NameElementCommand>) + read GET /names/validateupdate - validateNamesUpdate (List<NameElementCommand>) + read GET /names/validatedelete - validateNamesDelete (List<NameElementCommand>) ---------------------------------------------------------------------------------------------------- - update PUT /names - updateNames(List<NameElementCommand>) + update PUT /names - updateNames (List<NameElementCommand>) + update PUT /names/upload - updateNames (MultipartFile) ---------------------------------------------------------------------------------------------------- - delete DELETE /names - deleteNames(List<NameElementCommand>) + delete DELETE /names - deleteNames (List<NameElementCommand>) + delete DELETE /names/upload - deleteNames (MultipartFile) Note read GET /names/{name} - both name and uuid (name - exact and search, uuid exact) + /upload and /download endpoints handle Excel files */ public static final String DEFAULT_PAGE = "0"; @@ -115,7 +124,7 @@ public interface INames { /** * Create names by list of name elements. - * Return list of created name elements. + * Return list of name elements for created names. * * @param nameElements list of name elements * @return list of created name elements @@ -124,20 +133,24 @@ public interface INames { summary = "Create names by array of name elements", description = """ Create names by array of name elements. - Return array of created name elements. + Return array of name elements for created names. Required attributes: - - parentsystemstructure + - parentsystemstructure (uuid) - description - comment + Optional attributes: + - parentdevicestructure (uuid) + - index + Uuid is created by Naming. """ ) @ApiResponses(value = { @ApiResponse( responseCode = "201", - description = "OK. Return array of created name elements.", + description = "OK. Return array of name elements for created names.", content = @Content( mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = NameElement.class)))), @@ -176,6 +189,69 @@ public interface INames { requiredProperties = {"parentsystemstructure","parentdevicestructure","index","description","comment"})))) @RequestBody List<NameElementCommand> nameElements); + /** + * Create names by upload Excel file. + * Return Excel file with name elements for created names. + * + * @param file file + * @return Excel file + */ + @Operation( + summary = "Create names by upload Excel file", + description = """ + Create names by upload Excel file. + Return Excel file with name elements for created names. + + Expected columns as in Excel template for names. + + Required attributes: + - parentsystemstructure (uuid) + - description + - comment + + Optional attributes: + - parentdevicestructure (uuid) + - index + + Uuid is created by Naming. + """ + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "201", + description = "OK. Return Excel file with name elements for created names.", + content = @Content(mediaType = ExcelUtil.MIME_TYPE_EXCEL)), + @ApiResponse( + responseCode = "400", + description = "Bad request. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "422", + description = "Unprocessable entity. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "500", + description = "Internal server error. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))) + }) + @PostMapping( + value = "/upload", + produces = {"application/vnd.ms-excel"}, + consumes = {"multipart/form-data"}) + public ResponseEntity<Resource> createNames( + @Parameter( + in = ParameterIn.DEFAULT, + description = "Excel file with name elements", + required = true, + content = @Content(mediaType = ExcelUtil.MIME_TYPE_OPENXML_SPREADSHEET)) + @RequestParam("file") MultipartFile file); + // ---------------------------------------------------------------------------------------------------- /** @@ -236,6 +312,63 @@ public interface INames { @Parameter(in = ParameterIn.QUERY, description = "page starting from 0, offset") @RequestParam(required = false, defaultValue = DEFAULT_PAGE) Integer page, @Parameter(in = ParameterIn.QUERY, description = "page size, limit") @RequestParam(required = false, defaultValue = DEFAULT_PAGE_SIZE) Integer pageSize); + /** + * Find valid names (search). + * Return Excel file with paged list of name elements. + * + * @param deleted if deleted-only names are to be included + * (false for non-deleted-only names, true for deleted-only names, not used for both cases) + * @param queryFields search fields + * @param queryValues search values corresponding to search fields + * @param orderBy order by field + * @param isAsc sort order, ascending or descending + * @param page page starting from 0, offset + * @param pageSize page size, limit + * @return Excel file + */ + @Operation( + summary = "Find valid names (search) and download Excel file", + description = """ + Find valid names (search). + Return Excel file with paged list of name elements. + """ + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "OK. Return Excel file with paged list of name elements.", + content = @Content(mediaType = ExcelUtil.MIME_TYPE_EXCEL)), + @ApiResponse( + responseCode = "400", + description = "Bad request. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "422", + description = "Unprocessable entity. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "500", + description = "Internal server error. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))) + }) + @GetMapping( + value = "/download", + produces = {"application/vnd.ms-excel"}) + public ResponseEntity<Resource> readNamesDownload( + @Parameter(in = ParameterIn.QUERY, description = "if deleted names are to be included or not, omitted for both deleted and not deleted names") @RequestParam(required = false) Boolean deleted, + @Parameter(in = ParameterIn.QUERY, description = "search fields") @RequestParam(required = false) FieldName[] queryFields, + @Parameter(in = ParameterIn.QUERY, description = "search values corresponding to search fields") @RequestParam(required = false) String[] queryValues, + @Parameter(in = ParameterIn.QUERY, description = "order by field") @RequestParam(required = false) FieldName orderBy, + @Parameter(in = ParameterIn.QUERY, description = "sort order, ascending or descending") @RequestParam(required = false) Boolean isAsc, + @Parameter(in = ParameterIn.QUERY, description = "page starting from 0, offset") @RequestParam(required = false, defaultValue = DEFAULT_PAGE) Integer page, + @Parameter(in = ParameterIn.QUERY, description = "page size, limit") @RequestParam(required = false, defaultValue = DEFAULT_PAGE_SIZE) Integer pageSize); + /** * Find valid names by name or uuid (search). * Return paged list of name elements. @@ -663,7 +796,7 @@ public interface INames { // ---------------------------------------------------------------------------------------------------- /** - * Return if name elements are valid to create. + * Return if names are valid to create by list of name elements. * If names are valid to create, successful create of names can be expected. * * <p> @@ -672,25 +805,31 @@ public interface INames { * </p> * * @param nameElements list of name elements - * @return if list of name elements is valid to create + * @return if list of names is valid to create */ @Hidden @Operation( - summary = "Return if name elements are valid to create", + summary = "Return if names are valid to create by list of name elements", description = """ - Return if name elements are valid to create. + Return if names are valid to create by list of name elements. If names are valid to create, successful create of names can be expected. Required attributes: - - parentsystemstructure + - parentsystemstructure (uuid) - description - comment + + Optional attributes: + - parentdevicestructure (uuid) + - index + + Uuid is created by Naming. """ ) @ApiResponses(value = { @ApiResponse( responseCode = "200", - description = "OK. Response is true if all name elements validated ok, false otherwise. " + description = "OK. Response is true if all name elements validated ok, false otherwise." + "Responses contain array with result for each name element. Message and details are available if no response is available.", content = @Content( mediaType = "application/json", @@ -732,7 +871,7 @@ public interface INames { @RequestBody List<NameElementCommand> nameElements); /** - * Return if name elements are valid to update. + * Return if names are valid to update by list of name elements. * If names are valid to update, successful update of names can be expected. * * <p> @@ -745,22 +884,26 @@ public interface INames { */ @Hidden @Operation( - summary = "Return if name elements are valid to update", + summary = "Return if names are valid to update by list of name elements", description = """ - Return if name elements are valid to update. + Return if names are valid to update by list of name elements. If names are valid to update, successful update of names can be expected. Required attributes: - uuid - - parentsystemstructure + - parentsystemstructure (uuid) - description - comment + + Optional attributes: + - parentdevicestructure (uuid) + - index """ ) @ApiResponses(value = { @ApiResponse( responseCode = "200", - description = "OK. Response is true if all name elements validated ok, false otherwise. " + description = "OK. Response is true if all name elements validated ok, false otherwise." + "Responses contain array with result for each name element. Message and details are available if no response is available.", content = @Content( mediaType = "application/json", @@ -802,8 +945,8 @@ public interface INames { @RequestBody List<NameElementCommand> nameElements); /** - * Return if name elements are valid to delete. - * If names are valid to update, successful delete of names can be expected. + * Return if names are valid to delete by list of name elements. + * If names are valid to delete, successful delete of names can be expected. * * <p> * Response is true if all name elements validated ok, false otherwise, responses contain array @@ -815,20 +958,22 @@ public interface INames { */ @Hidden @Operation( - summary = "Return if name elements are valid to delete", + summary = "Return if names are valid to delete by list of name elements", description = """ - Return if name elements are valid to delete. - If names are valid to update, successful delete of names can be expected. + Return if names are valid to delete by list of name elements. + If names are valid to delete, successful delete of names can be expected. Required attributes: - uuid - comment + + Other attributes are not considered. """ ) @ApiResponses(value = { @ApiResponse( responseCode = "200", - description = "OK. Response is true if all name elements validated ok, false otherwise. " + description = "OK. Response is true if all name elements validated ok, false otherwise." + "Responses contain array with result for each name element. Message and details are available if no response is available.", content = @Content( mediaType = "application/json", @@ -873,30 +1018,32 @@ public interface INames { /** * Update names by list of name elements. - * Returns list of updated name elements. + * Return list of name elements for updated names. * * @param nameElements list of name elements - * @return list of updated name elements + * @return list of name elements for updated names */ @Operation( summary = "Update names by array of name elements", description = """ Update names by array of name elements. - Return array of updated name elements. + Return array of name elements for updated names. Required attributes: - uuid - - parentsystemstructure + - parentsystemstructure (uuid) - description - comment - Other attributes are optional. + Optional attributes: + - parentdevicestructure (uuid) + - index """ ) @ApiResponses(value = { @ApiResponse( responseCode = "200", - description = "OK. Return array of updated name elements.", + description = "OK. Return array of name elements for updated names.", content = @Content( mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = NameElement.class)))), @@ -935,20 +1082,82 @@ public interface INames { requiredProperties = {"uuid","parentsystemstructure","description","comment"})))) @RequestBody List<NameElementCommand> nameElements); + /** + * Update names by upload Excel file. + * Return Excel file with name elements for updated names. + * + * @param file file + * @return Excel file + */ + @Operation( + summary = "Update names by upload Excel file", + description = """ + Update names by upload Excel file. + Return Excel file with name elements for updated names. + + Expected columns as in Excel template for names. + + Required attributes: + - uuid + - parentsystemstructure (uuid) + - description + - comment + + Optional attributes: + - parentdevicestructure (uuid) + - index + """ + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "OK. Return Excel file with name elements for updated names.", + content = @Content(mediaType = ExcelUtil.MIME_TYPE_EXCEL)), + @ApiResponse( + responseCode = "400", + description = "Bad request. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "422", + description = "Unprocessable entity. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "500", + description = "Internal server error. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))) + }) + @PutMapping( + value = "/upload", + produces = {"application/vnd.ms-excel"}, + consumes = {"multipart/form-data"}) + public ResponseEntity<Resource> updateNames( + @Parameter( + in = ParameterIn.DEFAULT, + description = "Excel file with name elements", + required = true, + content = @Content(mediaType = ExcelUtil.MIME_TYPE_OPENXML_SPREADSHEET)) + @RequestParam("file") MultipartFile file); + // ---------------------------------------------------------------------------------------------------- /** * Delete names by list of name elements. - * Returns list of deleted name elements. + * Return list of name elements for deleted names. * - * @param uuid uuid - * @return list of deleted name elements + * @param nameElements list of name elements + * @return list of name elements for deleted names */ @Operation( summary = "Delete names by array of name elements", description = """ Delete names by array of name elements. - Return array of deleted name elements. + Return array of name elements for deleted names. Required attributes: - uuid @@ -960,7 +1169,7 @@ public interface INames { @ApiResponses(value = { @ApiResponse( responseCode = "200", - description = "OK. Return array of deleted name elements.", + description = "OK. Return array of name elements for deleted names.", content = @Content( mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = NameElement.class)))), @@ -999,4 +1208,62 @@ public interface INames { requiredProperties = {"uuid","comment"})))) @RequestBody List<NameElementCommand> nameElements); + /** + * Delete names by upload Excel file. + * Return Excel file with name elements for deleted names. + * + * @param file file + * @return Excel file + */ + @Operation( + summary = "Delete names by upload Excel file", + description = """ + Delete names by upload Excel file. + Return Excel file with name elements for deleted names. + + Expected columns as in Excel template for names. + + Required attributes: + - uuid + - comment + + Other attributes are not considered. + """ + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "OK. Return Excel file with name elements for deleted names.", + content = @Content(mediaType = ExcelUtil.MIME_TYPE_EXCEL)), + @ApiResponse( + responseCode = "400", + description = "Bad request. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "422", + description = "Unprocessable entity. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "500", + description = "Internal server error. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))) + }) + @DeleteMapping( + value = "/upload", + produces = {"application/vnd.ms-excel"}, + consumes = {"multipart/form-data"}) + public ResponseEntity<Resource> deleteNames( + @Parameter( + in = ParameterIn.DEFAULT, + description = "Excel file with name elements", + required = true, + content = @Content(mediaType = ExcelUtil.MIME_TYPE_OPENXML_SPREADSHEET)) + @RequestParam("file") MultipartFile file); + } diff --git a/src/main/java/org/openepics/names/rest/api/v1/IStructures.java b/src/main/java/org/openepics/names/rest/api/v1/IStructures.java index 17cba1bb..45d962ad 100644 --- a/src/main/java/org/openepics/names/rest/api/v1/IStructures.java +++ b/src/main/java/org/openepics/names/rest/api/v1/IStructures.java @@ -29,6 +29,8 @@ import org.openepics.names.rest.beans.response.Response; import org.openepics.names.rest.beans.response.ResponseBoolean; import org.openepics.names.rest.beans.response.ResponseBooleanList; import org.openepics.names.rest.beans.response.ResponsePageStructureElements; +import org.openepics.names.util.ExcelUtil; +import org.springframework.core.io.Resource; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -39,6 +41,7 @@ import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; @@ -66,20 +69,23 @@ public interface IStructures { Status - APPROVED, ARCHIVED, CANCELLED, PENDING, REJECTED FieldStructure - UUID, PARENT, NAME, MNEMONIC, MNEMONICEQUIVALENCE, MNEMONICPATH, DESCRIPTION - StructureElement - type (Type) + StructureElementCommand - subset of StructureElement attributes + StructureElement uuid (UUID) + type (Type) parent (UUID) name (String) mnemonic (String) + description (String) + comment (String) + ------------------------- mnemonicpath (String) level (Integer) - description (String) status (Status) latest (Boolean) deleted (Boolean) when (Date) who (String) - comment (String) authentication/authorization 3 levels - no, user, administrator @@ -91,8 +97,10 @@ public interface IStructures { Methods create POST /structures - createStructures (List<StructureElementCommand>) + create POST /structures/upload - createStructures (MultipartFile) ---------------------------------------------------------------------------------------------------- read GET /structures/{type} - readStructures (Type, Status[], Boolean, FieldStructure[], String[], FieldStructure, Boolean, Integer, Integer) + read GET /structures/{type}/download - readStructuresDownload (Type, Status[], Boolean, FieldStructure[], String[], FieldStructure, Boolean, Integer, Integer) read GET /structures/children/{type}/{uuid} - readStructuresChildren (Type, String, FieldStructure, Boolean, Integer, Integer) read GET /structures/mnemonic/{mnemonic} - readStructuresMnemonic (String, FieldStructure, Boolean, Integer, Integer, FieldStructure, Boolean, Integer, Integer) read GET /structures/mnemonicpath/{mnemonicpath} - readStructuresMnemonicpath (String, FieldStructure, Boolean, Integer, Integer, FieldStructure, Boolean, Integer, Integer) @@ -110,12 +118,20 @@ public interface IStructures { read GET /structures/validatereject - validateStructuresReject (List<StructureElementCommand>) ---------------------------------------------------------------------------------------------------- update PUT /structures - updateStructures (List<StructureElementCommand>) + update PUT /structures/upload - updateStructures (MultipartFile) ---------------------------------------------------------------------------------------------------- delete DELETE /structures - deleteStructures (List<StructureElementCommand>) + delete DELETE /structures/upload - deleteStructures (MultipartFile) ---------------------------------------------------------------------------------------------------- update PATCH /structures/approve - approveStructures (List<StructureElementCommand>) + update PATCH /structures/approve/upload - approveStructures (MultipartFile) update PATCH /structures/cancel - cancelStructures (List<StructureElementCommand>) + update PATCH /structures/cancel/upload - cancelStructures (MultipartFile) update PATCH /structures/reject - rejectStructures (List<StructureElementCommand>) + update PATCH /structures/reject/upload - rejectStructures (MultipartFile) + + Note + /upload and /download endpoints handle Excel files */ public static final String DEFAULT_PAGE = "0"; @@ -123,20 +139,7 @@ public interface IStructures { /** * Create (propose) structures by list of structure elements. - * Return list of created structure elements (proposals). - * - * <p> - * StructureElement attributes required: - * <ul> - * <li>type</li> - * <li>parent (System, Subsystem, DeviceGroup, DeviceType)</li> - * <li>name</li> - * <li>mnemonic (System, Subsystem, Discipline, DeviceType) - * (may be set for SystemGroup, not allowed for DeviceGroup)</li> - * <li>description</li> - * <li>comment</li> - * </ul> - * </p> + * Return list of structure elements for created structures (proposals). * * @param structureElements list of structure elements * @return list of structure elements for created structures (proposals) @@ -145,11 +148,11 @@ public interface IStructures { summary = "Create (propose) structures by array of structure elements", description = """ Create (propose) structures by array of structure elements. - Return array of created structure elements (proposals). + Return array of structure elements for created structures (proposals). Required attributes: - type - - parent (System, Subsystem, DeviceGroup, DeviceType) + - parent (System, Subsystem, DeviceGroup, DeviceType)(uuid) - name - mnemonic (System, Subsystem, Discipline, DeviceType, may be set for SystemGroup, not allowed for DeviceGroup) - description @@ -206,6 +209,74 @@ public interface IStructures { requiredProperties = {"type","parent","name","mnemonic","description","comment"})))) @RequestBody List<StructureElementCommand> structureElements); + /** + * Create (propose) structures by upload Excel file. + * Return Excel file with structure elements for created structures (proposals). + * + * @param file file + * @return Excel file + */ + @Operation( + summary = "Create (propose) structures by upload Excel file", + description = """ + Create (propose) structures by upload Excel file. + Return Excel file with structure elements for created structures (proposals). + + Expected columns as in Excel template for structures. + + Required attributes: + - type + - parent (System, Subsystem, DeviceGroup, DeviceType)(uuid) + - name + - mnemonic (System, Subsystem, Discipline, DeviceType, may be set for SystemGroup, not allowed for DeviceGroup) + - description + - comment + + Uuid is created by Naming. + """ + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "201", + description = "OK. Return Excel file with structure elements for created structures (proposals).", + content = @Content(mediaType = ExcelUtil.MIME_TYPE_EXCEL)), + @ApiResponse( + responseCode = "400", + description = "Bad request. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "409", + description = "Conflict. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "422", + description = "Unprocessable entity. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "500", + description = "Internal server error. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))) + }) + @PostMapping( + value = "/upload", + produces = {"application/vnd.ms-excel"}, + consumes = {"multipart/form-data"}) + public ResponseEntity<Resource> createStructures( + @Parameter( + in = ParameterIn.DEFAULT, + description = "Excel file with structure elements", + required = true, + content = @Content(mediaType = ExcelUtil.MIME_TYPE_OPENXML_SPREADSHEET)) + @RequestParam("file") MultipartFile file); + // ---------------------------------------------------------------------------------------------------- /** @@ -271,6 +342,67 @@ public interface IStructures { @Parameter(in = ParameterIn.QUERY, description = "page starting from 0, offset") @RequestParam(required = false, defaultValue = DEFAULT_PAGE) Integer page, @Parameter(in = ParameterIn.QUERY, description = "page size, limit") @RequestParam(required = false, defaultValue = DEFAULT_PAGE_SIZE) Integer pageSize); + /** + * Find valid structures (search). + * Return Excel file with paged list of structure elements. + * + * @param type type of structure to search in + * @param statuses statuses of structures to search for + * @param deleted if deleted-only structures are to be included + * (false for non-deleted-only structures, true for deleted-only structures, not used for both cases) + * @param queryFields search fields + * @param queryValues search values corresponding to search fields + * @param orderBy order by field + * @param isAsc sort order, ascending or descending + * @param page page starting from 0, offset + * @param pageSize page size, limit + * @return paged list of structure elements + */ + @Operation( + summary = "Find valid structures (search) and download Excel file", + description = """ + Find valid structures (search). + Return Excel file with paged list of structure elements. + """ + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "OK. Return Excel file with paged list of structure elements.", + content = @Content(mediaType = ExcelUtil.MIME_TYPE_EXCEL)), + @ApiResponse( + responseCode = "400", + description = "Bad request. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "422", + description = "Unprocessable entity. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "500", + description = "Internal server error. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))) + }) + @GetMapping( + value = "/{type}/download", + produces = {"application/vnd.ms-excel"}) + public ResponseEntity<Resource> readStructuresDownload( + @Parameter(in = ParameterIn.PATH, description = "type of structure to search in") @PathVariable("type") Type type, + @Parameter(in = ParameterIn.QUERY, description = "statuses of structures to search for") @RequestParam(required = false) Status[] statuses, + @Parameter(in = ParameterIn.QUERY, description = "if deleted structures are to be included or not, omitted for both deleted and not deleted structures") @RequestParam(required = false) Boolean deleted, + @Parameter(in = ParameterIn.QUERY, description = "search fields") @RequestParam(required = false) FieldStructure[] queryFields, + @Parameter(in = ParameterIn.QUERY, description = "search values corresponding to search fields") @RequestParam(required = false) String[] queryValues, + @Parameter(in = ParameterIn.QUERY, description = "order by field") @RequestParam(required = false) FieldStructure orderBy, + @Parameter(in = ParameterIn.QUERY, description = "sort order, ascending or descending") @RequestParam(required = false) Boolean isAsc, + @Parameter(in = ParameterIn.QUERY, description = "page starting from 0, offset") @RequestParam(required = false, defaultValue = DEFAULT_PAGE) Integer page, + @Parameter(in = ParameterIn.QUERY, description = "page size, limit") @RequestParam(required = false, defaultValue = DEFAULT_PAGE_SIZE) Integer pageSize); + /** * Find valid children structures by type and parent uuid (exact match). * Return paged list of structure elements. @@ -661,8 +793,8 @@ public interface IStructures { // ---------------------------------------------------------------------------------------------------- /** - * Return if structure elements are valid to create (propose). - * If structure elements are valid to create, successful create of structures can be expected. + * Return if structures are valid to create (propose) by list of structure elements. + * If structures are valid to create, successful create of structures can be expected. * * <p> * Returned object has four fields (message, details, response, responses). @@ -675,18 +807,18 @@ public interface IStructures { * </p> * * @param structureElements list of structure elements - * @return if list of structure elements is valid to create (propose) + * @return if list of structures is valid to create (propose) */ @Hidden @Operation( - summary = "Return if structure elements are valid to create (propose)", + summary = "Return if structures are valid to create (propose) by list of structure elements", description = """ - Return if structure elements are valid to create (propose). - If structure elements are valid to create, successful create of structures can be expected. + Return if structures are valid to create (propose) by list of structure elements. + If structures are valid to create, successful create of structures can be expected. Required attributes: - type - - parent (System, Subsystem, DeviceGroup, DeviceType) + - parent (System, Subsystem, DeviceGroup, DeviceType)(uuid) - name - mnemonic (System, Subsystem, Discipline, DeviceType, may be set for SystemGroup, not allowed for DeviceGroup) - description @@ -744,8 +876,8 @@ public interface IStructures { @RequestBody List<StructureElementCommand> structureElements); /** - * Return if structure elements are valid to update (propose). - * If structure elements are valid to update, successful update of structures can be expected. + * Return if structures are valid to update (propose) by list of structure elements. + * If structures are valid to update, successful update of structures can be expected. * * <p> * Returned object has four fields (message, details, response, responses). @@ -758,19 +890,19 @@ public interface IStructures { * </p> * * @param structureElements list of structure elements - * @return if list of structure elements is valid to update (propose) + * @return if list of structures is valid to update (propose) */ @Hidden @Operation( - summary = "Return if structure elements are valid to update (propose)", + summary = "Return if structures are valid to update (propose) by list of structure elements", description = """ - Return if structure elements are valid to update (propose). - If structure elements are valid to update, successful update of structures can be expected. + Return if structures are valid to update (propose) by list of structure elements. + If structures are valid to update, successful update of structures can be expected. Required attributes: - uuid - type - - parent (System, Subsystem, DeviceGroup, DeviceType) + - parent (System, Subsystem, DeviceGroup, DeviceType)(uuid) - name - mnemonic (System, Subsystem, Discipline, DeviceType, may be set for SystemGroup, not allowed for DeviceGroup) - description @@ -828,8 +960,8 @@ public interface IStructures { @RequestBody List<StructureElementCommand> structureElements); /** - * Return if structure elements are valid to delete (propose). - * If structure elements are valid to delete, successful delete of structures can be expected. + * Return if structures are valid to delete (propose) by list of structure elements. + * If structures are valid to delete, successful delete of structures can be expected. * * <p> * Returned object has four fields (message, details, response, responses). @@ -842,14 +974,14 @@ public interface IStructures { * </p> * * @param structureElements list of structure elements - * @return if list of structure elements is valid to delete (propose) + * @return if list of structures is valid to delete (propose) */ @Hidden @Operation( - summary = "Return if structure elements are valid to delete (propose)", + summary = "Return if structures are valid to delete (propose) by list of structure elements", description = """ - Return if structure elements are valid to delete (propose). - If structure elements are valid to delete, successful delete of structures can be expected. + Return if structures are valid to delete (propose) by list of structure elements. + If structures are valid to delete, successful delete of structures can be expected. Required attributes: - uuid @@ -908,8 +1040,8 @@ public interface IStructures { @RequestBody List<StructureElementCommand> structureElements); /** - * Return if structure elements are valid to approve. - * If structure elements are valid to approve, successful approve of structures can be expected. + * Return if structures are valid to approve by list of structure elements. + * If structures are valid to approve, successful approve of structures can be expected. * * <p> * Returned object has four fields (message, details, response, responses). @@ -922,19 +1054,19 @@ public interface IStructures { * </p> * * @param structureElements list of structure elements - * @return if list of structure elements is valid to approve + * @return if list of structures is valid to approve */ @Hidden @Operation( - summary = "Return if structure elements are valid to approve", + summary = "Return if structures are valid to approve by list of structure elements", description = """ - Return if structure elements are valid to approve. - If structure elements are valid to approve, successful approve of structures can be expected. + Return if structures are valid to approve by list of structure elements. + If structures are valid to approve, successful approve of structures can be expected. Required attributes: - uuid - type - - parent (System, Subsystem, DeviceGroup, DeviceType) + - parent (System, Subsystem, DeviceGroup, DeviceType)(uuid) - name - mnemonic (System, Subsystem, Discipline, DeviceType, may be set for SystemGroup, not allowed for DeviceGroup) - description @@ -992,8 +1124,8 @@ public interface IStructures { @RequestBody List<StructureElementCommand> structureElements); /** - * Return if structure elements are valid to cancel. - * If structure elements are valid to cancel, successful cancel of structures can be expected. + * Return if structures are valid to cancel by list of structure elements. + * If structures are valid to cancel, successful cancel of structures can be expected. * * <p> * Returned object has four fields (message, details, response, responses). @@ -1006,19 +1138,19 @@ public interface IStructures { * </p> * * @param structureElements list of structure elements - * @return if list of structure elements is valid to cancel + * @return if list of structures is valid to cancel */ @Hidden @Operation( - summary = "Return if structure elements are valid to cancel", + summary = "Return if structures are valid to cancel by list of structure elements", description = """ - Return if structure elements are valid to cancel. - If structure elements are valid to cancel, successful cancel of structures can be expected. + Return if structures are valid to cancel by list of structure elements. + If structures are valid to cancel, successful cancel of structures can be expected. Required attributes: - uuid - type - - parent (System, Subsystem, DeviceGroup, DeviceType) + - parent (System, Subsystem, DeviceGroup, DeviceType)(uuid) - name - mnemonic (System, Subsystem, Discipline, DeviceType, may be set for SystemGroup, not allowed for DeviceGroup) - description @@ -1076,8 +1208,8 @@ public interface IStructures { @RequestBody List<StructureElementCommand> structureElements); /** - * Return if structure elements are valid to reject. - * If structure elements are valid to reject, successful reject of structures can be expected. + * Return if structures are valid to reject by list of structure elements. + * If structures are valid to reject, successful reject of structures can be expected. * * <p> * Returned object has four fields (message, details, response, responses). @@ -1090,19 +1222,19 @@ public interface IStructures { * </p> * * @param structureElements list of structure elements - * @return if list of structure elements is valid to reject + * @return if list of structures is valid to reject */ @Hidden @Operation( - summary = "Return if structure elements are valid to reject", + summary = "Return if structures are valid to reject by list of structure elements", description = """ - Return if structure elements are valid to reject. - If structure elements are valid to reject, successful reject of structures can be expected. + Return if structures are valid to reject by list of structure elements. + If structures are valid to reject, successful reject of structures can be expected. Required attributes: - uuid - type - - parent (System, Subsystem, DeviceGroup, DeviceType) + - parent (System, Subsystem, DeviceGroup, DeviceType)(uuid) - name - mnemonic (System, Subsystem, Discipline, DeviceType, may be set for SystemGroup, not allowed for DeviceGroup) - description @@ -1163,7 +1295,7 @@ public interface IStructures { /** * Update (propose) structures by list of structure elements. - * Return list of updated structure elements (proposals). + * Return list of structure elements for updated structures (proposals). * * @param structureElements list of structure elements * @return list of structure elements for updated structures (proposals) @@ -1172,12 +1304,12 @@ public interface IStructures { summary = "Update (propose) structures by array of structure elements", description = """ Update (propose) structures by array of structure elements. - Return array of updated structure elements (proposals). + Return array of structure elements for updated structures (proposals). Required attributes: - uuid - type - - parent (System, Subsystem, DeviceGroup, DeviceType) + - parent (System, Subsystem, DeviceGroup, DeviceType)(uuid) - name - mnemonic (System, Subsystem, Discipline, DeviceType, may be set for SystemGroup, not allowed for DeviceGroup) - description @@ -1232,11 +1364,78 @@ public interface IStructures { requiredProperties = {"uuid","type","parent","name","mnemonic","description","comment"})))) @RequestBody List<StructureElementCommand> structureElements); + /** + * Update (propose) structures by upload Excel file. + * Return Excel file with structure elements for updated structures (proposals). + * + * @param file file + * @return Excel file + */ + @Operation( + summary = "Update (propose) structures by upload Excel file", + description = """ + Update (propose) structures by upload Excel file. + Return Excel file with structure elements for updated structures (proposals). + + Expected columns as in Excel template for structures. + + Required attributes: + - uuid + - type + - parent (System, Subsystem, DeviceGroup, DeviceType)(uuid) + - name + - mnemonic (System, Subsystem, Discipline, DeviceType, may be set for SystemGroup, not allowed for DeviceGroup) + - description + - comment + """ + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "OK. Return Excel file with structure elements for updated structures (proposals).", + content = @Content(mediaType = ExcelUtil.MIME_TYPE_EXCEL)), + @ApiResponse( + responseCode = "400", + description = "Bad request. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "409", + description = "Conflict. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "422", + description = "Unprocessable entity. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "500", + description = "Internal server error. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))) + }) + @PutMapping( + value = "/upload", + produces = {"application/vnd.ms-excel"}, + consumes = {"multipart/form-data"}) + public ResponseEntity<Resource> updateStructures( + @Parameter( + in = ParameterIn.DEFAULT, + description = "Excel file with structure elements", + required = true, + content = @Content(mediaType = ExcelUtil.MIME_TYPE_OPENXML_SPREADSHEET)) + @RequestParam("file") MultipartFile file); + // ---------------------------------------------------------------------------------------------------- /** * Delete (propose) structures by list of structure elements. - * Return list of deleted structure elements (proposals). + * Return list of structure elements for deleted structures (proposals). * * @param structureElements list of structure elements * @return list of structure elements for deleted structures (proposals) @@ -1245,7 +1444,7 @@ public interface IStructures { summary = "Delete (propose) structures by array of structure elements", description = """ Delete (propose) structures by array of structure elements. - Return array of deleted structure elements (proposals). + Return array of structure elements for deleted structures (proposals). Required attributes: - uuid @@ -1303,12 +1502,75 @@ public interface IStructures { requiredProperties = {"uuid","type","comment"})))) @RequestBody List<StructureElementCommand> structureElements); + /** + * Delete (propose) structures by upload Excel file. + * Return Excel file with structure elements for deleted structures (proposals). + * + * @param file file + * @return Excel file + */ + @Operation( + summary = "Delete (propose) structures by upload Excel file", + description = """ + Delete (propose) structures by upload Excel file. + Return Excel file with structure elements for deleted structures (proposals). + + Expected columns as in Excel template for structures. + Required attributes: + - uuid + - type + - comment + + Other attributes are not considered. + """ + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "OK. Return Excel file with structure elements for deleted structures (proposals).", + content = @Content(mediaType = ExcelUtil.MIME_TYPE_EXCEL)), + @ApiResponse( + responseCode = "400", + description = "Bad request. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "409", + description = "Conflict. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "422", + description = "Unprocessable entity. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "500", + description = "Internal server error. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))) + }) + @DeleteMapping( + value = "/upload", + produces = {"application/vnd.ms-excel"}, + consumes = {"multipart/form-data"}) + public ResponseEntity<Resource> deleteStructures( + @Parameter( + in = ParameterIn.DEFAULT, + description = "Excel file with structure elements", + required = true, + content = @Content(mediaType = ExcelUtil.MIME_TYPE_OPENXML_SPREADSHEET)) + @RequestParam("file") MultipartFile file); // ---------------------------------------------------------------------------------------------------- /** * Approve structures (proposals) by list of structure elements. - * Return list of approved structure elements. + * Return list of structure elements for approved structures. * * @param structureElements list of structure elements * @return list of structure elements for approved structures @@ -1317,12 +1579,12 @@ public interface IStructures { summary = "Approve structures (proposals) by array of structure elements", description = """ Approve structures (proposals) by array of structure elements. - Return array of approved structure elements. + Return array of structure elements for approved structures. Required attributes: - uuid - type - - parent (System, Subsystem, DeviceGroup, DeviceType) + - parent (System, Subsystem, DeviceGroup, DeviceType)(uuid) - name - mnemonic (System, Subsystem, Discipline, DeviceType, may be set for SystemGroup, not allowed for DeviceGroup) - description @@ -1378,6 +1640,73 @@ public interface IStructures { requiredProperties = {"uuid","type","parent","name","mnemonic","description","comment"})))) @RequestBody List<StructureElementCommand> structureElements); + /** + * Approve structures (proposals) by upload Excel file. + * Return Excel file with structure elements for approved structures. + * + * @param structureElements list of structure elements + * @return Excel file + */ + @Operation( + summary = "Approve structures (proposals) by upload Excel file", + description = """ + Approve structures (proposals) by upload Excel file. + Return Excel file with structure elements for for approved structures. + + Expected columns as in Excel template for structures. + + Required attributes: + - uuid + - type + - parent (System, Subsystem, DeviceGroup, DeviceType)(uuid) + - name + - mnemonic (System, Subsystem, Discipline, DeviceType, may be set for SystemGroup, not allowed for DeviceGroup) + - description + - comment + """ + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "OK. Return Excel file with structure elements for approved structures.", + content = @Content(mediaType = ExcelUtil.MIME_TYPE_EXCEL)), + @ApiResponse( + responseCode = "400", + description = "Bad request. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "409", + description = "Conflict. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "422", + description = "Unprocessable entity. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "500", + description = "Internal server error. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))) + }) + @PatchMapping( + value = "/approve/upload", + produces = {"application/vnd.ms-excel"}, + consumes = {"multipart/form-data"}) + public ResponseEntity<Resource> approveStructures( + @Parameter( + in = ParameterIn.DEFAULT, + description = "Excel file with structure elements", + required = true, + content = @Content(mediaType = ExcelUtil.MIME_TYPE_OPENXML_SPREADSHEET)) + @RequestParam("file") MultipartFile file); + /** * Cancel structures (proposals) by list of structure elements. * Return list of cancelled structure elements. @@ -1389,12 +1718,12 @@ public interface IStructures { summary = "Cancel structures (proposals) by array of structure elements", description = """ Cancel structures (proposals) by array of structure elements. - Return array of cancelled structure elements. + Return array of structure elements for cancelled structures. Required attributes: - uuid - type - - parent (System, Subsystem, DeviceGroup, DeviceType) + - parent (System, Subsystem, DeviceGroup, DeviceType)(uuid) - name - mnemonic (System, Subsystem, Discipline, DeviceType, may be set for SystemGroup, not allowed for DeviceGroup) - description @@ -1450,6 +1779,73 @@ public interface IStructures { requiredProperties = {"uuid","type","parent","name","mnemonic","description","comment"})))) @RequestBody List<StructureElementCommand> structureElements); + /** + * Cancel structures (proposals) by upload Excel file. + * Return Excel file with cancelled structure elements. + * + * @param structureElements list of structure elements + * @return Excel file + */ + @Operation( + summary = "Cancel structures (proposals) by upload Excel file", + description = """ + Cancel structures (proposals) by upload Excel file. + Return Excel file with structure elements for cancelled structures. + + Expected columns as in Excel template for structures. + + Required attributes: + - uuid + - type + - parent (System, Subsystem, DeviceGroup, DeviceType)(uuid) + - name + - mnemonic (System, Subsystem, Discipline, DeviceType, may be set for SystemGroup, not allowed for DeviceGroup) + - description + - comment + """ +) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "OK. Return Excel file with structure elements for cancelled structures.", + content = @Content(mediaType = ExcelUtil.MIME_TYPE_EXCEL)), + @ApiResponse( + responseCode = "400", + description = "Bad request. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "409", + description = "Conflict. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "422", + description = "Unprocessable entity. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "500", + description = "Internal server error. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))) + }) + @PatchMapping( + value = "/cancel/upload", + produces = {"application/vnd.ms-excel"}, + consumes = {"multipart/form-data"}) + public ResponseEntity<Resource> cancelStructures( + @Parameter( + in = ParameterIn.DEFAULT, + description = "Excel file with structure elements", + required = true, + content = @Content(mediaType = ExcelUtil.MIME_TYPE_OPENXML_SPREADSHEET)) + @RequestParam("file") MultipartFile file); + /** * Reject structures (proposals) by list of structure elements. * Return list of rejected structure elements. @@ -1461,12 +1857,12 @@ public interface IStructures { summary = "Reject structures (proposals) by array of structure elements", description = """ Reject structures (proposals) by array of structure elements. - Return array of rejected structure elements. + Return array of structure elements for rejected structures. Required attributes: - uuid - type - - parent (System, Subsystem, DeviceGroup, DeviceType) + - parent (System, Subsystem, DeviceGroup, DeviceType)(uuid) - name - mnemonic (System, Subsystem, Discipline, DeviceType, may be set for SystemGroup, not allowed for DeviceGroup) - description @@ -1522,4 +1918,71 @@ public interface IStructures { requiredProperties = {"uuid","type","parent","name","mnemonic","description","comment"})))) @RequestBody List<StructureElementCommand> structureElements); + /** + * Reject structures (proposals) by upload Excel file. + * Return Excel file with rejected structure elements. + * + * @param structureElements list of structure elements + * @return Excel file + */ + @Operation( + summary = "Reject structures (proposals) by upload Excel file", + description = """ + Reject structures (proposals) by upload Excel file. + Return Excel file with structure elements for rejected structures. + + Expected columns as in Excel template for structures. + + Required attributes: + - uuid + - type + - parent (System, Subsystem, DeviceGroup, DeviceType)(uuid) + - name + - mnemonic (System, Subsystem, Discipline, DeviceType, may be set for SystemGroup, not allowed for DeviceGroup) + - description + - comment + """ + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "OK. Return Excel file with structure elements for rejected structures.", + content = @Content(mediaType = ExcelUtil.MIME_TYPE_EXCEL)), + @ApiResponse( + responseCode = "400", + description = "Bad request. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "409", + description = "Conflict. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "422", + description = "Unprocessable entity. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "500", + description = "Internal server error. Reason and information such as message, details, field are available.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Response.class))) + }) + @PatchMapping( + value = "/reject/upload", + produces = {"application/vnd.ms-excel"}, + consumes = {"multipart/form-data"}) + public ResponseEntity<Resource> rejectStructures( + @Parameter( + in = ParameterIn.DEFAULT, + description = "Excel file with structure elements", + required = true, + content = @Content(mediaType = ExcelUtil.MIME_TYPE_OPENXML_SPREADSHEET)) + @RequestParam("file") MultipartFile file); + } diff --git a/src/main/java/org/openepics/names/rest/beans/element/NameElement.java b/src/main/java/org/openepics/names/rest/beans/element/NameElement.java index e3d1cf62..f0e1378d 100644 --- a/src/main/java/org/openepics/names/rest/beans/element/NameElement.java +++ b/src/main/java/org/openepics/names/rest/beans/element/NameElement.java @@ -45,17 +45,17 @@ public class NameElement extends NameElementCommand implements Serializable { private String systemstructure; @Schema(description = "Mnemonic path for for the device structure.") private String devicestructure; - @Schema(description = "Name (verbose) of the name.") + @Schema(description = "Name (verbose) of the name entry.") private String name; - @Schema(description = "Status of the name.") + @Schema(description = "Status of the name entry.") private Status status; - @Schema(description = "If the name is latest (with status APPROVED) in its line of entries.") + @Schema(description = "If the name entry is latest (with status APPROVED) in its line of entries.") private Boolean latest; - @Schema(description = "If the name is deleted.") + @Schema(description = "If the name entry is deleted.") private Boolean deleted; - @Schema(description = "Date and time when the name was created.") + @Schema(description = "Date and time when the name entry was created.") private Date when; - @Schema(description = "(User) Name of who created the name.") + @Schema(description = "Name (user) of who created the name entry.") private String who; /** diff --git a/src/main/java/org/openepics/names/rest/beans/element/NameElementCommand.java b/src/main/java/org/openepics/names/rest/beans/element/NameElementCommand.java index b1899d42..2f21e63d 100644 --- a/src/main/java/org/openepics/names/rest/beans/element/NameElementCommand.java +++ b/src/main/java/org/openepics/names/rest/beans/element/NameElementCommand.java @@ -38,17 +38,17 @@ public class NameElementCommand implements Serializable { */ private static final long serialVersionUID = -3166840982436164399L; - @Schema(description = "Identity of the name. Value is set server-side.") + @Schema(description = "Identity (uuid) of the name entry. Value is created server-side.") private UUID uuid; - @Schema(description = "Identity for the system structure parent.") + @Schema(description = "Identity (uuid) for the system structure parent.") private UUID parentsystemstructure; - @Schema(description = "Identity for the device structure parent.") + @Schema(description = "Identity (uuid) for the device structure parent (if the name entry refers to device structure).") private UUID parentdevicestructure; - @Schema(description = "Index (instance) of the name.") + @Schema(description = "Index (instance) of the name entry (if the name entry refers to device structure).") private String index; - @Schema(description = "Description of the name.") + @Schema(description = "Description of the name entry.") private String description; - @Schema(description = "Comment of the name command.") + @Schema(description = "Comment of the name entry command.") private String comment; /** diff --git a/src/main/java/org/openepics/names/rest/beans/element/StructureElement.java b/src/main/java/org/openepics/names/rest/beans/element/StructureElement.java index 68f76cc3..30d5f138 100644 --- a/src/main/java/org/openepics/names/rest/beans/element/StructureElement.java +++ b/src/main/java/org/openepics/names/rest/beans/element/StructureElement.java @@ -54,7 +54,7 @@ public class StructureElement extends StructureElementCommand implements Seriali private Boolean deleted; @Schema(description = "Date and time when the structure entry was created.") private Date when; - @Schema(description = "(User) Name of who created the structure entry.") + @Schema(description = "Name (user) of who created the structure entry.") private String who; /** diff --git a/src/main/java/org/openepics/names/rest/beans/element/StructureElementCommand.java b/src/main/java/org/openepics/names/rest/beans/element/StructureElementCommand.java index d64eaee0..13b7ad80 100644 --- a/src/main/java/org/openepics/names/rest/beans/element/StructureElementCommand.java +++ b/src/main/java/org/openepics/names/rest/beans/element/StructureElementCommand.java @@ -41,11 +41,11 @@ public class StructureElementCommand implements Serializable { */ private static final long serialVersionUID = 788979309267676232L; - @Schema(description = "Identity of the structure entry. Value is set server-side.") + @Schema(description = "Identity (uuid) of the structure entry. Value is created server-side.") private UUID uuid; @Schema(description = "Type of the structure entry.") private Type type; - @Schema(description = "Identity for the structure entry parent (if the structure entry has a parent).") + @Schema(description = "Identity (uuid) for the structure entry parent (if the structure entry has a parent).") private UUID parent; @Schema(description = "Name of the structure entry.") private String name; diff --git a/src/main/java/org/openepics/names/rest/controller/NamesController.java b/src/main/java/org/openepics/names/rest/controller/NamesController.java index 5ee006d6..5358b5d9 100644 --- a/src/main/java/org/openepics/names/rest/controller/NamesController.java +++ b/src/main/java/org/openepics/names/rest/controller/NamesController.java @@ -18,6 +18,7 @@ package org.openepics.names.rest.controller; +import java.io.IOException; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -32,13 +33,19 @@ import org.openepics.names.rest.beans.response.ResponseBooleanList; import org.openepics.names.rest.beans.response.ResponsePageNameElements; import org.openepics.names.service.NamesService; import org.openepics.names.service.exception.ServiceException; +import org.openepics.names.util.ExcelUtil; import org.openepics.names.util.LogUtil; import org.openepics.names.util.TextUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; import com.google.common.collect.Lists; @@ -85,6 +92,41 @@ public class NamesController implements INames { } } + @Override + public ResponseEntity<Resource> createNames(MultipartFile file) { + // validate authority + // naming user & admin + // validate + // do + + try { + if (ExcelUtil.hasExcelFormat(file)) { + List<NameElementCommand> nameElementCommands = ExcelUtil.excelToNameElementCommands(file.getInputStream()); + namesService.validateNamesCreate(nameElementCommands); + List<NameElement> nameElements = namesService.createNames(nameElementCommands); + InputStreamResource isr = new InputStreamResource(ExcelUtil.nameElementsToExcel(nameElements)); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=NameElement.xlsx") + .contentType(MediaType.parseMediaType(ExcelUtil.MIME_TYPE_EXCEL)) + .body(isr); + } else { + throw new ServiceException(TextUtil.FILE_COULD_NOT_BE_PARSED, null, null, null); + } + } catch (ServiceException e) { + LogUtil.logServiceException(LOGGER, Level.SEVERE, e); + LogUtil.logStackTraceElements(LOGGER, Level.SEVERE, e); + throw e; + } catch (IOException e) { + LOGGER.log(Level.SEVERE, e.getMessage()); + LogUtil.logStackTraceElements(LOGGER, Level.SEVERE, e); + throw new ServiceException(TextUtil.FILE_COULD_NOT_BE_PARSED, e.getMessage(), null, e); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, e.getMessage()); + LogUtil.logStackTraceElements(LOGGER, Level.SEVERE, e); + throw e; + } + } + // ---------------------------------------------------------------------------------------------------- @Override @@ -106,6 +148,17 @@ public class NamesController implements INames { } } + @Override + public ResponseEntity<Resource> readNamesDownload(Boolean deleted, FieldName[] queryFields, String[] queryValues, + FieldName orderBy, Boolean isAsc, Integer page, Integer pageSize) { + ResponsePageNameElements nameElements = readNames(deleted, queryFields, queryValues, orderBy, isAsc, page, pageSize); + InputStreamResource isr = new InputStreamResource(ExcelUtil.nameElementsToExcel(nameElements)); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=NameElement.xlsx") + .contentType(MediaType.parseMediaType(ExcelUtil.MIME_TYPE_EXCEL)) + .body(isr); + } + @Override public ResponsePageNameElements readNames( String name, @@ -351,6 +404,41 @@ public class NamesController implements INames { } } + @Override + public ResponseEntity<Resource> updateNames(MultipartFile file) { + // validate authority + // naming user & admin + // validate + // do + + try { + if (ExcelUtil.hasExcelFormat(file)) { + List<NameElementCommand> nameElementCommands = ExcelUtil.excelToNameElementCommands(file.getInputStream()); + namesService.validateNamesUpdate(nameElementCommands); + List<NameElement> nameElements = namesService.updateNames(nameElementCommands); + InputStreamResource isr = new InputStreamResource(ExcelUtil.nameElementsToExcel(nameElements)); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=NameElement.xlsx") + .contentType(MediaType.parseMediaType(ExcelUtil.MIME_TYPE_EXCEL)) + .body(isr); + } else { + throw new ServiceException(TextUtil.FILE_COULD_NOT_BE_PARSED, null, null, null); + } + } catch (ServiceException e) { + LogUtil.logServiceException(LOGGER, Level.SEVERE, e); + LogUtil.logStackTraceElements(LOGGER, Level.SEVERE, e); + throw e; + } catch (IOException e) { + LOGGER.log(Level.SEVERE, e.getMessage()); + LogUtil.logStackTraceElements(LOGGER, Level.SEVERE, e); + throw new ServiceException(TextUtil.FILE_COULD_NOT_BE_PARSED, e.getMessage(), null, e); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, e.getMessage()); + LogUtil.logStackTraceElements(LOGGER, Level.SEVERE, e); + throw e; + } + } + // ---------------------------------------------------------------------------------------------------- @Override @@ -374,4 +462,39 @@ public class NamesController implements INames { } } -} \ No newline at end of file + @Override + public ResponseEntity<Resource> deleteNames(MultipartFile file) { + // validate authority + // naming user & admin + // validate + // do + + try { + if (ExcelUtil.hasExcelFormat(file)) { + List<NameElementCommand> nameElementCommands = ExcelUtil.excelToNameElementCommands(file.getInputStream()); + namesService.validateNamesDelete(nameElementCommands); + List<NameElement> nameElements = namesService.deleteNames(nameElementCommands); + InputStreamResource isr = new InputStreamResource(ExcelUtil.nameElementsToExcel(nameElements)); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=NameElement.xlsx") + .contentType(MediaType.parseMediaType(ExcelUtil.MIME_TYPE_EXCEL)) + .body(isr); + } else { + throw new ServiceException(TextUtil.FILE_COULD_NOT_BE_PARSED, null, null, null); + } + } catch (ServiceException e) { + LogUtil.logServiceException(LOGGER, Level.SEVERE, e); + LogUtil.logStackTraceElements(LOGGER, Level.SEVERE, e); + throw e; + } catch (IOException e) { + LOGGER.log(Level.SEVERE, e.getMessage()); + LogUtil.logStackTraceElements(LOGGER, Level.SEVERE, e); + throw new ServiceException(TextUtil.FILE_COULD_NOT_BE_PARSED, e.getMessage(), null, e); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, e.getMessage()); + LogUtil.logStackTraceElements(LOGGER, Level.SEVERE, e); + throw e; + } + } + +} diff --git a/src/main/java/org/openepics/names/rest/controller/StructuresController.java b/src/main/java/org/openepics/names/rest/controller/StructuresController.java index 9b708cb5..047ec6ca 100644 --- a/src/main/java/org/openepics/names/rest/controller/StructuresController.java +++ b/src/main/java/org/openepics/names/rest/controller/StructuresController.java @@ -18,6 +18,7 @@ package org.openepics.names.rest.controller; +import java.io.IOException; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -34,13 +35,19 @@ import org.openepics.names.rest.beans.response.ResponseBooleanList; import org.openepics.names.rest.beans.response.ResponsePageStructureElements; import org.openepics.names.service.StructuresService; import org.openepics.names.service.exception.ServiceException; +import org.openepics.names.util.ExcelUtil; import org.openepics.names.util.LogUtil; import org.openepics.names.util.TextUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; import com.google.common.collect.Lists; @@ -94,6 +101,41 @@ public class StructuresController implements IStructures { } } + @Override + public ResponseEntity<Resource> createStructures(MultipartFile file) { + // validate authority + // naming user & admin + // validate + // do + + try { + if (ExcelUtil.hasExcelFormat(file)) { + List<StructureElementCommand> structureElementCommands = ExcelUtil.excelToStructureElementCommands(file.getInputStream()); + structuresService.validateStructuresCreate(structureElementCommands); + List<StructureElement> structureElements = structuresService.createStructures(structureElementCommands); + InputStreamResource isr = new InputStreamResource(ExcelUtil.structureElementsToExcel(structureElements)); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=StructureElement.xlsx") + .contentType(MediaType.parseMediaType(ExcelUtil.MIME_TYPE_EXCEL)) + .body(isr); + } else { + throw new ServiceException(TextUtil.FILE_COULD_NOT_BE_PARSED, null, null, null); + } + } catch (ServiceException e) { + LogUtil.logServiceException(LOGGER, Level.SEVERE, e); + LogUtil.logStackTraceElements(LOGGER, Level.SEVERE, e); + throw e; + } catch (IOException e) { + LOGGER.log(Level.SEVERE, e.getMessage()); + LogUtil.logStackTraceElements(LOGGER, Level.SEVERE, e); + throw new ServiceException(TextUtil.FILE_COULD_NOT_BE_PARSED, e.getMessage(), null, e); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, e.getMessage()); + LogUtil.logStackTraceElements(LOGGER, Level.SEVERE, e); + throw e; + } + } + // ---------------------------------------------------------------------------------------------------- @Override @@ -115,6 +157,18 @@ public class StructuresController implements IStructures { } } + @Override + public ResponseEntity<Resource> readStructuresDownload(Type type, Status[] statuses, Boolean deleted, + FieldStructure[] queryFields, String[] queryValues, FieldStructure orderBy, Boolean isAsc, Integer page, + Integer pageSize) { + ResponsePageStructureElements structureElements = readStructures(type, statuses, deleted, queryFields, queryValues, orderBy, isAsc, page, pageSize); + InputStreamResource isr = new InputStreamResource(ExcelUtil.structureElementsToExcel(structureElements)); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=StructureElement.xlsx") + .contentType(MediaType.parseMediaType(ExcelUtil.MIME_TYPE_EXCEL)) + .body(isr); + } + @Override public ResponsePageStructureElements readStructuresChildren( Type type, String uuid, @@ -434,6 +488,35 @@ public class StructuresController implements IStructures { } } + @Override + public ResponseEntity<Resource> updateStructures(MultipartFile file) { + try { + if (ExcelUtil.hasExcelFormat(file)) { + List<StructureElementCommand> structureElementCommands = ExcelUtil.excelToStructureElementCommands(file.getInputStream()); + structuresService.validateStructuresUpdate(structureElementCommands); + List<StructureElement> structureElements = structuresService.updateStructures(structureElementCommands); + InputStreamResource isr = new InputStreamResource(ExcelUtil.structureElementsToExcel(structureElements)); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=StructureElement.xlsx") + .contentType(MediaType.parseMediaType(ExcelUtil.MIME_TYPE_EXCEL)) + .body(isr); + } else { + throw new ServiceException(TextUtil.FILE_COULD_NOT_BE_PARSED, null, null, null); + } + } catch (ServiceException e) { + LogUtil.logServiceException(LOGGER, Level.SEVERE, e); + LogUtil.logStackTraceElements(LOGGER, Level.SEVERE, e); + throw e; + } catch (IOException e) { + LOGGER.log(Level.SEVERE, e.getMessage()); + LogUtil.logStackTraceElements(LOGGER, Level.SEVERE, e); + throw new ServiceException(TextUtil.FILE_COULD_NOT_BE_PARSED, e.getMessage(), null, e); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, e.getMessage()); + LogUtil.logStackTraceElements(LOGGER, Level.SEVERE, e); + throw e; + } + } // ---------------------------------------------------------------------------------------------------- @@ -453,6 +536,36 @@ public class StructuresController implements IStructures { } } + @Override + public ResponseEntity<Resource> deleteStructures(MultipartFile file) { + try { + if (ExcelUtil.hasExcelFormat(file)) { + List<StructureElementCommand> structureElementCommands = ExcelUtil.excelToStructureElementCommands(file.getInputStream()); + structuresService.validateStructuresDelete(structureElementCommands); + List<StructureElement> structureElements = structuresService.deleteStructures(structureElementCommands); + InputStreamResource isr = new InputStreamResource(ExcelUtil.structureElementsToExcel(structureElements)); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=StructureElement.xlsx") + .contentType(MediaType.parseMediaType(ExcelUtil.MIME_TYPE_EXCEL)) + .body(isr); + } else { + throw new ServiceException(TextUtil.FILE_COULD_NOT_BE_PARSED, null, null, null); + } + } catch (ServiceException e) { + LogUtil.logServiceException(LOGGER, Level.SEVERE, e); + LogUtil.logStackTraceElements(LOGGER, Level.SEVERE, e); + throw e; + } catch (IOException e) { + LOGGER.log(Level.SEVERE, e.getMessage()); + LogUtil.logStackTraceElements(LOGGER, Level.SEVERE, e); + throw new ServiceException(TextUtil.FILE_COULD_NOT_BE_PARSED, e.getMessage(), null, e); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, e.getMessage()); + LogUtil.logStackTraceElements(LOGGER, Level.SEVERE, e); + throw e; + } + } + // ---------------------------------------------------------------------------------------------------- @Override @@ -471,6 +584,36 @@ public class StructuresController implements IStructures { } } + @Override + public ResponseEntity<Resource> approveStructures(MultipartFile file) { + try { + if (ExcelUtil.hasExcelFormat(file)) { + List<StructureElementCommand> structureElementCommands = ExcelUtil.excelToStructureElementCommands(file.getInputStream()); + structuresService.validateStructuresApprove(structureElementCommands); + List<StructureElement> structureElements = structuresService.approveStructures(structureElementCommands); + InputStreamResource isr = new InputStreamResource(ExcelUtil.structureElementsToExcel(structureElements)); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=StructureElement.xlsx") + .contentType(MediaType.parseMediaType(ExcelUtil.MIME_TYPE_EXCEL)) + .body(isr); + } else { + throw new ServiceException(TextUtil.FILE_COULD_NOT_BE_PARSED, null, null, null); + } + } catch (ServiceException e) { + LogUtil.logServiceException(LOGGER, Level.SEVERE, e); + LogUtil.logStackTraceElements(LOGGER, Level.SEVERE, e); + throw e; + } catch (IOException e) { + LOGGER.log(Level.SEVERE, e.getMessage()); + LogUtil.logStackTraceElements(LOGGER, Level.SEVERE, e); + throw new ServiceException(TextUtil.FILE_COULD_NOT_BE_PARSED, e.getMessage(), null, e); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, e.getMessage()); + LogUtil.logStackTraceElements(LOGGER, Level.SEVERE, e); + throw e; + } + } + @Override public List<StructureElement> cancelStructures(List<StructureElementCommand> structureElements) { try { @@ -487,6 +630,36 @@ public class StructuresController implements IStructures { } } + @Override + public ResponseEntity<Resource> cancelStructures(MultipartFile file) { + try { + if (ExcelUtil.hasExcelFormat(file)) { + List<StructureElementCommand> structureElementCommands = ExcelUtil.excelToStructureElementCommands(file.getInputStream()); + structuresService.validateStructuresCancel(structureElementCommands); + List<StructureElement> structureElements = structuresService.cancelStructures(structureElementCommands); + InputStreamResource isr = new InputStreamResource(ExcelUtil.structureElementsToExcel(structureElements)); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=StructureElement.xlsx") + .contentType(MediaType.parseMediaType(ExcelUtil.MIME_TYPE_EXCEL)) + .body(isr); + } else { + throw new ServiceException(TextUtil.FILE_COULD_NOT_BE_PARSED, null, null, null); + } + } catch (ServiceException e) { + LogUtil.logServiceException(LOGGER, Level.SEVERE, e); + LogUtil.logStackTraceElements(LOGGER, Level.SEVERE, e); + throw e; + } catch (IOException e) { + LOGGER.log(Level.SEVERE, e.getMessage()); + LogUtil.logStackTraceElements(LOGGER, Level.SEVERE, e); + throw new ServiceException(TextUtil.FILE_COULD_NOT_BE_PARSED, e.getMessage(), null, e); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, e.getMessage()); + LogUtil.logStackTraceElements(LOGGER, Level.SEVERE, e); + throw e; + } + } + @Override public List<StructureElement> rejectStructures(List<StructureElementCommand> structureElements) { try { @@ -503,4 +676,34 @@ public class StructuresController implements IStructures { } } + @Override + public ResponseEntity<Resource> rejectStructures(MultipartFile file) { + try { + if (ExcelUtil.hasExcelFormat(file)) { + List<StructureElementCommand> structureElementCommands = ExcelUtil.excelToStructureElementCommands(file.getInputStream()); + structuresService.validateStructuresReject(structureElementCommands); + List<StructureElement> structureElements = structuresService.rejectStructures(structureElementCommands); + InputStreamResource isr = new InputStreamResource(ExcelUtil.structureElementsToExcel(structureElements)); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=StructureElement.xlsx") + .contentType(MediaType.parseMediaType(ExcelUtil.MIME_TYPE_EXCEL)) + .body(isr); + } else { + throw new ServiceException(TextUtil.FILE_COULD_NOT_BE_PARSED, null, null, null); + } + } catch (ServiceException e) { + LogUtil.logServiceException(LOGGER, Level.SEVERE, e); + LogUtil.logStackTraceElements(LOGGER, Level.SEVERE, e); + throw e; + } catch (IOException e) { + LOGGER.log(Level.SEVERE, e.getMessage()); + LogUtil.logStackTraceElements(LOGGER, Level.SEVERE, e); + throw new ServiceException(TextUtil.FILE_COULD_NOT_BE_PARSED, e.getMessage(), null, e); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, e.getMessage()); + LogUtil.logStackTraceElements(LOGGER, Level.SEVERE, e); + throw e; + } + } + } diff --git a/src/main/java/org/openepics/names/util/ExcelUtil.java b/src/main/java/org/openepics/names/util/ExcelUtil.java new file mode 100644 index 00000000..b98389c8 --- /dev/null +++ b/src/main/java/org/openepics/names/util/ExcelUtil.java @@ -0,0 +1,536 @@ +/* + * Copyright (C) 2022 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.text.DateFormat; +import java.text.MessageFormat; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.commons.lang3.StringUtils; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.ClientAnchor; +import org.apache.poi.ss.usermodel.Comment; +import org.apache.poi.ss.usermodel.CreationHelper; +import org.apache.poi.ss.usermodel.DataFormat; +import org.apache.poi.ss.usermodel.Drawing; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.RichTextString; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.openepics.names.rest.beans.Type; +import org.openepics.names.rest.beans.element.NameElement; +import org.openepics.names.rest.beans.element.NameElementCommand; +import org.openepics.names.rest.beans.element.StructureElement; +import org.openepics.names.rest.beans.element.StructureElementCommand; +import org.openepics.names.rest.beans.response.ResponsePageNameElements; +import org.openepics.names.rest.beans.response.ResponsePageStructureElements; +import org.openepics.names.service.exception.ServiceException; +import org.springframework.web.multipart.MultipartFile; + +import com.google.common.collect.Lists; + +/** + * Utility class to assist in handling of Excel files. + * + * @author Lars Johansson + */ +public class ExcelUtil { + + private static final Logger LOGGER = Logger.getLogger(ExcelUtil.class.getName()); + + public static final String MIME_TYPE_EXCEL = "application/vnd.ms-excel"; + public static final String MIME_TYPE_OPENXML_SPREADSHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + + // width (in units of 1/256th of a character width) + // column width 2.75 inch (to fit header text and uuid) + public static final int SHEET_COLUMN_WIDTH = 28 * 256 + 64; + + public static final int NAMEELEMENTCOMMAND_LENGTH = 6; + public static final String[][] NAMEELEMENT_HEADER_COMMENT = { + {"Uuid", "Identity (uuid) of the name entry. Value is created server-side."}, + {"Parentsystemstructure", "Identity (uuid) for the system structure parent."}, + {"Parentdevicestructure", "Identity (uuid) for the device structure parent (if the name entry refers to device structure)."}, + {"Index", "Index (instance) of the name entry (if the name entry refers to device structure)."}, + {"Description", "Description of the name entry."}, + {"Comment", "Comment of the name entry command."}, + // above NameElementCommand + {"Systemstructure", "Mnemonic path for for the system structure."}, + {"Devicestructure", "Mnemonic path for for the device structure."}, + {"Name", "Name (verbose) of the name entry."}, + {"Status", "Status of the name entry."}, + {"Latest", "If the name entry is latest (with status APPROVED) in its line of entries."}, + {"Deleted", "If the name entry is deleted."}, + {"When", "Date and time when the name entry was created."}, + {"Who", "Name (user) of who created the name entry."} }; + + public static final int STRUCTUREELEMENTCOMMAND_LENGTH = 7; + public static final String[][] STRUCTUREELEMENT_HEADER_COMMENT = { + {"Uuid", "Identity (uuid) of the structure entry. Value is created server-side."}, + {"Type", "Type of the structure entry."}, + {"Parent", "Identity (uuid) for the structure entry parent (if the structure entry has a parent)."}, + {"Name", "Name (verbose) of the structure entry."}, + {"Mnemonic", "Mnemonic of the structure entry."}, + {"Description", "Description of the structure entry."}, + {"Comment", "Comment of the structure entry command."}, + // above StructureElementCommand + {"Mnemonicpath", "Mnemonic path of the structure entry."}, + {"Level", "Level of the structure entry."}, + {"Status", "Status of the structure entry."}, + {"Latest", "If the structure entry is latest (with status APPROVED) in its line of entries."}, + {"Deleted", "If the structure entry is deleted."}, + {"When", "Date and time when the structure entry was created."}, + {"Who", "Name (user) of who created the structure entry."} }; + + private static final String SHEET = "Entries"; + + private static final String ENTRIES_EXCEL_TO_NAME_ELEMENT_COMMANDS = "excelToNameElementCommands, # entries: {0}"; + private static final String ENTRIES_EXCEL_TO_STRUCTURE_ELEMENT_COMMANDS = "excelToStructureElementCommands, # entries: {0}"; + private static final String ENTRIES_NAME_ELEMENTS_TO_EXCEL = "nameElementsToExcel, # entries: {0}"; + private static final String ENTRIES_STRUCTURE_ELEMENTS_TO_EXCEL = "structureElementsToExcel, # entries: {0}"; + + private static final String FAILED_TO_EXPORT_VALUES_TO_FILE = "Failed to export values to file"; + private static final String FILE_COULD_NOT_BE_PARSED_FOR_VALUE_AT_ROW_CELL = "File could not be parsed for value at row: {0} cell: {1}"; + private static final String INVALID_SHEET_EXPECTED_SHEET = "Invalid sheet. Expected sheet: {0}"; + private static final String INVALID_VALUE_EXPECTED_VALUE = "Invalid value: {0} Expected value: {1}"; + private static final String INVALID_VALUE_UNEXPECTED_VALUE = "Invalid value: {0} Unexpected value."; + + /** + * This class is not to be instantiated. + */ + private ExcelUtil() { + throw new IllegalStateException("Utility class"); + } + + /** + * Utility method to check if file has Excel format. + * + * @param file file + * @return boolean if file has Excel format + */ + public static boolean hasExcelFormat(MultipartFile file) { + return MIME_TYPE_OPENXML_SPREADSHEET.equals(file.getContentType()); + } + + /** + * Utility method to convert an Excel file to a list of name elements. + * + * @param is input stream + * @return list of name elements + */ + public static List<NameElementCommand> excelToNameElementCommands(InputStream is) { + // rules for conversion + // Excel as NameElementCommand accepted + // Excel as NameElement not accepted + // see validateHeaderNameElementCommand + + int rowIndex = 0; + int columnIndex = 0; + + try { + Workbook workbook = new XSSFWorkbook(is); + validateSheet(workbook); + + Sheet sheet = workbook.getSheet(SHEET); + Iterator<Row> rows = sheet.iterator(); + List<NameElementCommand> list = Lists.newArrayList(); + + // iterate over rows and columns (cells) + while (rows.hasNext()) { + Row row = rows.next(); + Iterator<Cell> cells = row.iterator(); + NameElementCommand nameElementCommand = new NameElementCommand(); + + while (cells.hasNext()) { + Cell cell = cells.next(); + rowIndex = cell.getRowIndex(); + columnIndex = cell.getColumnIndex(); + + // ensure header row with syntax + if (rowIndex == 0) { + validateHeaderNameElementCommand(cell); + continue; + } + + String value = cell.getStringCellValue(); + switch (columnIndex) { + case 0: + nameElementCommand.setUuid(!StringUtils.isEmpty(value) ? UUID.fromString(value) : null); + break; + case 1: + nameElementCommand.setParentsystemstructure(!StringUtils.isEmpty(value) ? UUID.fromString(value) : null); + break; + case 2: + nameElementCommand.setParentdevicestructure(!StringUtils.isEmpty(value) ? UUID.fromString(value) : null); + break; + case 3: + nameElementCommand.setIndex(value); + break; + case 4: + nameElementCommand.setDescription(value); + break; + case 5: + nameElementCommand.setComment(value); + break; + default: + throw new IllegalArgumentException( + MessageFormat.format(INVALID_VALUE_UNEXPECTED_VALUE, value)); + } + } + // not add header row + if (rowIndex > 0) { + list.add(nameElementCommand); + } + } + + workbook.close(); + LOGGER.log(Level.INFO, ENTRIES_EXCEL_TO_NAME_ELEMENT_COMMANDS, list.size()); + return list; + } catch (IllegalArgumentException | IOException e) { + throw new ServiceException( + MessageFormat.format(FILE_COULD_NOT_BE_PARSED_FOR_VALUE_AT_ROW_CELL, rowIndex, columnIndex), + e.getMessage(), null, e); + } + } + + /** + * Utility method to convert an Excel file to a list of structure elements. + * + * @param is input stream + * @return list of structure elements + */ + public static List<StructureElementCommand> excelToStructureElementCommands(InputStream is) { + // rules for conversion + // Excel as StructureElementCommand accepted + // Excel as StructureElement not accepted + // see validateHeaderStructureElementCommand + + int rowIndex = 0; + int columnIndex = 0; + + try { + Workbook workbook = new XSSFWorkbook(is); + validateSheet(workbook); + + Sheet sheet = workbook.getSheet(SHEET); + Iterator<Row> rows = sheet.iterator(); + List<StructureElementCommand> list = Lists.newArrayList(); + + // iterate over rows and columns (cells) + while (rows.hasNext()) { + Row row = rows.next(); + Iterator<Cell> cells = row.iterator(); + StructureElementCommand structureElementCommand = new StructureElementCommand(); + + while (cells.hasNext()) { + Cell cell = cells.next(); + rowIndex = cell.getRowIndex(); + columnIndex = cell.getColumnIndex(); + + // ensure header row with syntax + if (rowIndex == 0) { + validateHeaderStructureElementCommand(cell); + continue; + } + + String value = cell.getStringCellValue(); + switch (columnIndex) { + case 0: + structureElementCommand.setUuid(!StringUtils.isEmpty(value) ? UUID.fromString(value) : null); + break; + case 1: + structureElementCommand.setType(Type.valueOf(value)); + break; + case 2: + structureElementCommand.setParent(!StringUtils.isEmpty(value) ? UUID.fromString(value) : null); + break; + case 3: + structureElementCommand.setName(value); + break; + case 4: + structureElementCommand.setMnemonic(value); + break; + case 5: + structureElementCommand.setDescription(value); + break; + case 6: + structureElementCommand.setComment(value); + break; + default: + throw new IllegalArgumentException( + MessageFormat.format(INVALID_VALUE_UNEXPECTED_VALUE, value)); + } + + } + // not add header row + if (rowIndex > 0) { + list.add(structureElementCommand); + } + } + + workbook.close(); + LOGGER.log(Level.INFO, ENTRIES_EXCEL_TO_STRUCTURE_ELEMENT_COMMANDS, list.size()); + return list; + } catch (IllegalArgumentException | IOException e) { + throw new ServiceException( + MessageFormat.format(FILE_COULD_NOT_BE_PARSED_FOR_VALUE_AT_ROW_CELL, rowIndex, columnIndex), + e.getMessage(), null, e); + } + } + + /** + * Validate that workbook has sheet with expected name. + * + * @param workbook workbook + */ + private static void validateSheet(Workbook workbook) { + if (workbook.getSheet(SHEET) == null) { + throw new IllegalArgumentException(MessageFormat.format(INVALID_SHEET_EXPECTED_SHEET, SHEET)); + } + } + + /** + * Validate that cell is a proper header cell for a name element. + * + * @param cell cell + */ + private static void validateHeaderNameElementCommand(Cell cell) { + // validate that columns are NameElementCommand + // nothing less or more + // not have (cell.getColumnIndex() >= NAMEELEMENTCOMMAND_LENGTH) to allow NameElement + + if (cell.getColumnIndex() < NAMEELEMENTCOMMAND_LENGTH + && !NAMEELEMENT_HEADER_COMMENT[cell.getColumnIndex()][0].equals(cell.getStringCellValue())) { + throw new IllegalArgumentException( + MessageFormat.format( + INVALID_VALUE_EXPECTED_VALUE, + cell.getStringCellValue(), + NAMEELEMENT_HEADER_COMMENT[cell.getColumnIndex()][0])); + } else if (cell.getColumnIndex() >= NAMEELEMENTCOMMAND_LENGTH) { + throw new IllegalArgumentException( + MessageFormat.format(INVALID_VALUE_UNEXPECTED_VALUE, cell.getStringCellValue())); + } + } + + /** + * Validate that cell is a proper header cell for a structure element. + * + * @param cell cell + */ + private static void validateHeaderStructureElementCommand(Cell cell) { + // validate that columns are StructureElementCommand + // nothing less or more + // not have (cell.getColumnIndex() >= STRUCTUREELEMENTCOMMAND_LENGTH) to allow StructureElement + + if (cell.getColumnIndex() < STRUCTUREELEMENTCOMMAND_LENGTH + && !STRUCTUREELEMENT_HEADER_COMMENT[cell.getColumnIndex()][0].equals(cell.getStringCellValue())) { + throw new IllegalArgumentException( + MessageFormat.format( + INVALID_VALUE_EXPECTED_VALUE, + cell.getStringCellValue(), + STRUCTUREELEMENT_HEADER_COMMENT[cell.getColumnIndex()][0])); + } else if (cell.getColumnIndex() >= STRUCTUREELEMENTCOMMAND_LENGTH) { + throw new IllegalArgumentException( + MessageFormat.format(INVALID_VALUE_UNEXPECTED_VALUE, cell.getStringCellValue())); + } + } + + /** + * Utility method to convert a list of name elements to an Excel file. + * + * @param nameElements name elements + * @return Excel file + */ + public static ByteArrayInputStream nameElementsToExcel(ResponsePageNameElements nameElements) { + return nameElementsToExcel(nameElements.getList()); + } + + /** + * Utility method to convert a list of name elements to an Excel file. + * + * @param nameElements name elements + * @return Excel file + */ + public static ByteArrayInputStream nameElementsToExcel(List<NameElement> nameElements) { + LOGGER.log(Level.INFO, ENTRIES_NAME_ELEMENTS_TO_EXCEL, nameElements.size()); + try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream();) { + Sheet sheet = workbook.createSheet(SHEET); + + // prepare header + Drawing<?> drawing = sheet.createDrawingPatriarch(); + CreationHelper factory = workbook.getCreationHelper(); + ClientAnchor anchor = factory.createClientAnchor(); + DataFormat format = workbook.createDataFormat(); + short textFormat = format.getFormat("text"); + Font headerFont = workbook.createFont(); + headerFont.setBold(true); + + // header + Row headerRow = sheet.createRow(0); + for (int columnIndex = 0; columnIndex < NAMEELEMENT_HEADER_COMMENT.length; columnIndex++) { + Cell cell = headerRow.createCell(columnIndex); + + // value + cell.setCellValue(NAMEELEMENT_HEADER_COMMENT[columnIndex][0]); + + // width + sheet.setColumnWidth(columnIndex, SHEET_COLUMN_WIDTH); + + // style + CellStyle cellStyle = cell.getCellStyle(); + cellStyle.setDataFormat(textFormat); + cellStyle.setFont(headerFont); + cell.setCellStyle(cellStyle); + + // comment + anchor.setRow1(headerRow.getRowNum()); + anchor.setRow2(headerRow.getRowNum()+3); + anchor.setCol1(cell.getColumnIndex()); + anchor.setCol2(cell.getColumnIndex()+1); + Comment cellComment = drawing.createCellComment(anchor); + RichTextString richTextString = factory.createRichTextString(NAMEELEMENT_HEADER_COMMENT[columnIndex][1]); + cellComment.setString(richTextString); + cellComment.setAuthor(TextUtil.NAMING); + cell.setCellComment(cellComment); + } + + // data + int rowIndex = 1; + for (NameElement nameElement : nameElements) { + Row row = sheet.createRow(rowIndex++); + row.createCell(0).setCellValue(nameElement.getUuid() != null ? nameElement.getUuid().toString() : null); + row.createCell(1).setCellValue(nameElement.getParentsystemstructure() != null ? nameElement.getParentsystemstructure().toString() : null); + row.createCell(2).setCellValue(nameElement.getParentdevicestructure() != null ? nameElement.getParentdevicestructure().toString() : null); + row.createCell(3).setCellValue(nameElement.getIndex()); + row.createCell(4).setCellValue(nameElement.getDescription()); + row.createCell(5).setCellValue(nameElement.getComment()); + row.createCell(6).setCellValue(nameElement.getSystemstructure()); + row.createCell(7).setCellValue(nameElement.getDevicestructure()); + row.createCell(8).setCellValue(nameElement.getName()); + row.createCell(9).setCellValue(nameElement.getStatus() != null ? nameElement.getStatus().toString() : null); + row.createCell(10).setCellValue(nameElement.isLatest()); + row.createCell(11).setCellValue(nameElement.isDeleted()); + row.createCell(12).setCellValue(DateFormat.getDateTimeInstance().format(nameElement.getWhen())); + row.createCell(13).setCellValue(nameElement.getWho()); + } + workbook.write(out); + return new ByteArrayInputStream(out.toByteArray()); + + } catch (IOException e) { + throw new ServiceException(FAILED_TO_EXPORT_VALUES_TO_FILE, e.getMessage(), null, e); + } + } + + /** + * Utility method to convert a list of structure elements to an Excel file. + * + * @param structureElements structure elements + * @return Excel file + */ + public static ByteArrayInputStream structureElementsToExcel(ResponsePageStructureElements structureElements) { + return structureElementsToExcel(structureElements.getList()); + } + + /** + * Utility method to convert a list of structure elements to an Excel file. + * + * @param structureElements structure elements + * @return Excel file + */ + public static ByteArrayInputStream structureElementsToExcel(List<StructureElement> structureElements) { + LOGGER.log(Level.INFO, ENTRIES_STRUCTURE_ELEMENTS_TO_EXCEL, structureElements.size()); + try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream();) { + Sheet sheet = workbook.createSheet(SHEET); + + // prepare header + Drawing<?> drawing = sheet.createDrawingPatriarch(); + CreationHelper factory = workbook.getCreationHelper(); + ClientAnchor anchor = factory.createClientAnchor(); + DataFormat format = workbook.createDataFormat(); + short textFormat = format.getFormat("text"); + Font headerFont = workbook.createFont(); + headerFont.setBold(true); + + // header + Row headerRow = sheet.createRow(0); + for (int columnIndex = 0; columnIndex < STRUCTUREELEMENT_HEADER_COMMENT.length; columnIndex++) { + Cell cell = headerRow.createCell(columnIndex); + + // value + cell.setCellValue(STRUCTUREELEMENT_HEADER_COMMENT[columnIndex][0]); + + // width + sheet.setColumnWidth(columnIndex, SHEET_COLUMN_WIDTH); + + // style + CellStyle cellStyle = cell.getCellStyle(); + cellStyle.setDataFormat(textFormat); + cellStyle.setFont(headerFont); + cell.setCellStyle(cellStyle); + + // comment + anchor.setRow1(headerRow.getRowNum()); + anchor.setRow2(headerRow.getRowNum()+3); + anchor.setCol1(cell.getColumnIndex()); + anchor.setCol2(cell.getColumnIndex()+1); + Comment cellComment = drawing.createCellComment(anchor); + RichTextString richTextString = factory.createRichTextString(STRUCTUREELEMENT_HEADER_COMMENT[columnIndex][1]); + cellComment.setString(richTextString); + cellComment.setAuthor(TextUtil.NAMING); + cell.setCellComment(cellComment); + } + + // data + int rowIndex = 1; + for (StructureElement structureElement : structureElements) { + Row row = sheet.createRow(rowIndex++); + row.createCell(0).setCellValue(structureElement.getUuid() != null ? structureElement.getUuid().toString() : null); + row.createCell(1).setCellValue(structureElement.getType() != null ? structureElement.getType().toString() : null); + row.createCell(2).setCellValue(structureElement.getParent() != null ? structureElement.getParent().toString() : null); + row.createCell(3).setCellValue(structureElement.getName()); + row.createCell(4).setCellValue(structureElement.getMnemonic()); + row.createCell(5).setCellValue(structureElement.getDescription()); + row.createCell(6).setCellValue(structureElement.getComment()); + row.createCell(7).setCellValue(structureElement.getMnemonicpath()); + row.createCell(8).setCellValue(structureElement.getLevel()); + row.createCell(9).setCellValue(structureElement.getStatus() != null ? structureElement.getStatus().toString() : null); + row.createCell(10).setCellValue(structureElement.isLatest()); + row.createCell(11).setCellValue(structureElement.isDeleted()); + row.createCell(12).setCellValue(DateFormat.getDateTimeInstance().format(structureElement.getWhen())); + row.createCell(13).setCellValue(structureElement.getWho()); + } + workbook.write(out); + return new ByteArrayInputStream(out.toByteArray()); + + } catch (IOException e) { + throw new ServiceException(FAILED_TO_EXPORT_VALUES_TO_FILE, e.getMessage(), null, e); + } + } + +} diff --git a/src/main/java/org/openepics/names/util/TextUtil.java b/src/main/java/org/openepics/names/util/TextUtil.java index 3701117c..47fd1efc 100644 --- a/src/main/java/org/openepics/names/util/TextUtil.java +++ b/src/main/java/org/openepics/names/util/TextUtil.java @@ -120,6 +120,8 @@ public class TextUtil { public static final String INDEX_IS_NOT_VALID = INDEX + SPACE + IS_NOT_VALID; public static final String MNEMONIC_IS_NOT_VALID = MNEMONIC + SPACE + IS_NOT_VALID; + public static final String FILE_COULD_NOT_BE_PARSED = "File could not be parsed"; + /** * This class is not to be instantiated. */ diff --git a/src/main/resources/application-docker.properties b/src/main/resources/application-docker.properties index 102c01b5..272502d9 100644 --- a/src/main/resources/application-docker.properties +++ b/src/main/resources/application-docker.properties @@ -22,8 +22,9 @@ openapi.info.contact.url=https://jira.esss.lu.se/projects/NT/issues openapi.info.description=\ Perform Naming tasks Create Read Update Delete + Approve Cancel Reject \n\n \ Here are some useful links \n \ - - [CCDB ecosystem](https://confluence.esss.lu.se/display/SW/CCDB+ecosystem) \n \ - - [Introduction to Naming REST API](/pdfs/naming_rest_api_brief_introduction.pdf) + [Cheat Sheet](/pdfs/naming_rest_api_cheat_sheet.pdf) \n\n \ + - [Naming](https://confluence.esss.lu.se/display/SW/Naming+Tool%2C+Cable+DB%2C+CCDB%2C+IOC+Factory%2C+RBAC) in ICS Software toolchain \n \ + - [Introduction to Naming REST API](/pdfs/naming_rest_api_brief_introduction.pdf) + [Cheat Sheet](/pdfs/naming_rest_api_cheat_sheet.pdf) \n \ + - Excel templates for [names](/templates/NameElementCommand.xlsx) + [structures](/templates/StructureElementCommand.xlsx) \n\n \ Note \n \ - Observe which fields to use for operations client to server. \n \ - Obsolete values are not shown unless history is requested. \n \ diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 102c01b5..272502d9 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -22,8 +22,9 @@ openapi.info.contact.url=https://jira.esss.lu.se/projects/NT/issues openapi.info.description=\ Perform Naming tasks Create Read Update Delete + Approve Cancel Reject \n\n \ Here are some useful links \n \ - - [CCDB ecosystem](https://confluence.esss.lu.se/display/SW/CCDB+ecosystem) \n \ - - [Introduction to Naming REST API](/pdfs/naming_rest_api_brief_introduction.pdf) + [Cheat Sheet](/pdfs/naming_rest_api_cheat_sheet.pdf) \n\n \ + - [Naming](https://confluence.esss.lu.se/display/SW/Naming+Tool%2C+Cable+DB%2C+CCDB%2C+IOC+Factory%2C+RBAC) in ICS Software toolchain \n \ + - [Introduction to Naming REST API](/pdfs/naming_rest_api_brief_introduction.pdf) + [Cheat Sheet](/pdfs/naming_rest_api_cheat_sheet.pdf) \n \ + - Excel templates for [names](/templates/NameElementCommand.xlsx) + [structures](/templates/StructureElementCommand.xlsx) \n\n \ Note \n \ - Observe which fields to use for operations client to server. \n \ - Obsolete values are not shown unless history is requested. \n \ diff --git a/src/main/resources/static/templates/NameElementCommand.xlsx b/src/main/resources/static/templates/NameElementCommand.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..04db6c54a4d35f2b81fd79e2827ba481bb54b4d5 GIT binary patch literal 7168 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/static/templates/StructureElementCommand.xlsx b/src/main/resources/static/templates/StructureElementCommand.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..95ac1775957aced5957b45ddbb8e1191b4687112 GIT binary patch literal 7237 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 JPyI}#{{#FY2loH~ literal 0 HcmV?d00001 -- GitLab