diff --git a/aspect-model-editor-core/src/main/java/org/eclipse/esmf/ame/exceptions/GenerationException.java b/aspect-model-editor-core/src/main/java/org/eclipse/esmf/ame/exceptions/GenerationException.java new file mode 100644 index 00000000..43e85482 --- /dev/null +++ b/aspect-model-editor-core/src/main/java/org/eclipse/esmf/ame/exceptions/GenerationException.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 Robert Bosch Manufacturing Solutions GmbH + * + * See the AUTHORS file(s) distributed with this work for + * additional information regarding authorship. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package org.eclipse.esmf.ame.exceptions; + +import java.io.Serial; + +import lombok.Getter; + +@Getter +public class GenerationException extends RuntimeException { + @Serial + private static final long serialVersionUID = 1L; + + /** + * Constructs a GenerationException with message and cause. + * + * @param message the message of the exception + */ + public GenerationException( final String message ) { + super( message ); + } +} diff --git a/aspect-model-editor-runtime/postman/ame.postman_collection.json b/aspect-model-editor-runtime/postman/ame.postman_collection.json index ad894697..509ab512 100644 --- a/aspect-model-editor-runtime/postman/ame.postman_collection.json +++ b/aspect-model-editor-runtime/postman/ame.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "a66814d4-a0ab-4527-b872-6b66b8c20734", + "_postman_id": "26a275be-9ff5-4002-a3c8-9cdd4bcf4f29", "name": "AME.POSTMAN.RESOURCES", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -789,6 +789,154 @@ }, "response": [] }, + { + "name": "GenerateJsonOpenApiSpecWithResourcePath", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "pm.test(\"Response body is valid\", function () {\r", + " const jsonData = pm.response.json();\r", + " pm.expect(jsonData.openapi).to.equal(\"3.0.3\");\r", + " pm.expect(jsonData.info.title).to.equal(\"movement\");\r", + " pm.expect(jsonData.info.version).to.equal(\"v1.0.0\");\r", + " pm.expect(jsonData.servers[0].url).to.equal(\"http://www.test.com/api/v1.0.0\");\r", + " pm.expect(jsonData.paths).to.have.property('/resource/{resourceId}');\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "@prefix samm: .\r\n@prefix samm-c: .\r\n@prefix samm-e: .\r\n@prefix unit: .\r\n@prefix rdf: .\r\n@prefix rdfs: .\r\n@prefix xsd: .\r\n@prefix : .\r\n\r\n:Movement a samm:Aspect ;\r\n samm:preferredName \"movement\"@en ;\r\n samm:description \"Aspect for movement information\"@en ;\r\n samm:properties ( :isMoving :position :speed :speedLimitWarning ) ;\r\n samm:operations ( ) ;\r\n samm:events ( ) .\r\n\r\n:isMoving a samm:Property ;\r\n samm:preferredName \"is moving\"@en ;\r\n samm:description \"Flag indicating whether the asset is currently moving\"@en ;\r\n samm:characteristic samm-c:Boolean .\r\n\r\n:position a samm:Property ;\r\n samm:preferredName \"position\"@en ;\r\n samm:description \"Indicates a position\"@en ;\r\n samm:characteristic :SpatialPositionCharacteristic .\r\n\r\n:speed a samm:Property ;\r\n samm:preferredName \"speed\"@en ;\r\n samm:description \"speed of vehicle\"@en ;\r\n samm:characteristic :Speed .\r\n\r\n:speedLimitWarning a samm:Property ;\r\n samm:preferredName \"speed limit warning\"@en ;\r\n samm:description \"Indicates if the speed limit is adhered to.\"@en ;\r\n samm:characteristic :TrafficLight .\r\n\r\n:SpatialPositionCharacteristic a samm-c:SingleEntity ;\r\n samm:preferredName \"spatial position characteristic\"@en ;\r\n samm:description \"Represents a single position in space with optional z coordinate.\"@en ;\r\n samm:dataType :SpatialPosition .\r\n\r\n:Speed a samm-c:Measurement ;\r\n samm:preferredName \"speed\"@en ;\r\n samm:description \"Scalar representation of speed of an object in kilometers per hour.\"@en ;\r\n samm:dataType xsd:float ;\r\n samm-c:unit unit:kilometrePerHour .\r\n\r\n:TrafficLight a samm-c:Enumeration ;\r\n samm:preferredName \"warning level\"@en ;\r\n samm:description \"Represents if speed of position change is within specification (green), within tolerance (yellow), or outside specification (red).\"@en ;\r\n samm:dataType xsd:string ;\r\n samm-c:values ( \"green\" \"yellow\" \"red\" ) .\r\n\r\n:SpatialPosition a samm:Entity ;\r\n samm:preferredName \"spatial position\"@en ;\r\n samm:description \"Represents latitude, longitude and altitude information in the WGS84 geodetic reference datum\"@en ;\r\n samm:see ;\r\n samm:properties ( :latitude :longitude [ samm:property :altitude; samm:optional true ] ) .\r\n\r\n:latitude a samm:Property ;\r\n samm:preferredName \"latitude\"@en ;\r\n samm:description \"latitude coordinate in space (WGS84)\"@en ;\r\n samm:see ;\r\n samm:characteristic :Coordinate ;\r\n samm:exampleValue \"9.1781\"^^xsd:decimal .\r\n\r\n:longitude a samm:Property ;\r\n samm:preferredName \"longitude\"@en ;\r\n samm:description \"longitude coordinate in space (WGS84)\"@en ;\r\n samm:see ;\r\n samm:characteristic :Coordinate ;\r\n samm:exampleValue \"48.80835\"^^xsd:decimal .\r\n\r\n:altitude a samm:Property ;\r\n samm:preferredName \"altitude\"@en ;\r\n samm:description \"Elevation above sea level zero\"@en ;\r\n samm:see ;\r\n samm:characteristic :MetresAboveMeanSeaLevel ;\r\n samm:exampleValue \"153\"^^xsd:float .\r\n\r\n:Coordinate a samm-c:Measurement ;\r\n samm:preferredName \"coordinate\"@en ;\r\n samm:description \"Representing the geographical coordinate\"@en ;\r\n samm:dataType xsd:decimal ;\r\n samm-c:unit unit:degreeUnitOfAngle .\r\n\r\n:MetresAboveMeanSeaLevel a samm-c:Measurement ;\r\n samm:preferredName \"metres above mean sea level\"@en ;\r\n samm:description \"Signifies the vertical distance in reference to a historic mean sea level as a vertical datum\"@en ;\r\n samm:see ;\r\n samm:dataType xsd:float ;\r\n samm-c:unit unit:metre .\r\n" + }, + "url": { + "raw": "http://localhost:{{port}}/ame/api/generate/open-api-spec?output=json&baseUrl=http://www.test.com&includeQueryApi=true&useSemanticVersion=true&pagingOption=NO_PAGING&resourcePath=/resource/%7BresourceId%7D&jsonProperties=%7B\"resourceId\":%7B\"name\":\"resourceId\",\"in\":\"path\",\"description\":\"An example resource Id.\",\"required\":true,\"schema\":%7B\"type\":\"string\"%7D%7D%7D", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "ame", + "api", + "generate", + "open-api-spec" + ], + "query": [ + { + "key": "output", + "value": "json" + }, + { + "key": "baseUrl", + "value": "http://www.test.com" + }, + { + "key": "includeQueryApi", + "value": "true" + }, + { + "key": "useSemanticVersion", + "value": "true" + }, + { + "key": "pagingOption", + "value": "NO_PAGING" + }, + { + "key": "resourcePath", + "value": "/resource/%7BresourceId%7D" + }, + { + "key": "jsonProperties", + "value": "%7B\"resourceId\":%7B\"name\":\"resourceId\",\"in\":\"path\",\"description\":\"An example resource Id.\",\"required\":true,\"schema\":%7B\"type\":\"string\"%7D%7D%7D" + } + ] + } + }, + "response": [] + }, + { + "name": "GenerateJsonOpenApiSpecWithWrongResourcePath", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 400\", function () {\r", + " pm.response.to.have.status(400);\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "@prefix samm: .\r\n@prefix samm-c: .\r\n@prefix samm-e: .\r\n@prefix unit: .\r\n@prefix rdf: .\r\n@prefix rdfs: .\r\n@prefix xsd: .\r\n@prefix : .\r\n\r\n:Movement a samm:Aspect ;\r\n samm:preferredName \"movement\"@en ;\r\n samm:description \"Aspect for movement information\"@en ;\r\n samm:properties ( :isMoving :position :speed :speedLimitWarning ) ;\r\n samm:operations ( ) ;\r\n samm:events ( ) .\r\n\r\n:isMoving a samm:Property ;\r\n samm:preferredName \"is moving\"@en ;\r\n samm:description \"Flag indicating whether the asset is currently moving\"@en ;\r\n samm:characteristic samm-c:Boolean .\r\n\r\n:position a samm:Property ;\r\n samm:preferredName \"position\"@en ;\r\n samm:description \"Indicates a position\"@en ;\r\n samm:characteristic :SpatialPositionCharacteristic .\r\n\r\n:speed a samm:Property ;\r\n samm:preferredName \"speed\"@en ;\r\n samm:description \"speed of vehicle\"@en ;\r\n samm:characteristic :Speed .\r\n\r\n:speedLimitWarning a samm:Property ;\r\n samm:preferredName \"speed limit warning\"@en ;\r\n samm:description \"Indicates if the speed limit is adhered to.\"@en ;\r\n samm:characteristic :TrafficLight .\r\n\r\n:SpatialPositionCharacteristic a samm-c:SingleEntity ;\r\n samm:preferredName \"spatial position characteristic\"@en ;\r\n samm:description \"Represents a single position in space with optional z coordinate.\"@en ;\r\n samm:dataType :SpatialPosition .\r\n\r\n:Speed a samm-c:Measurement ;\r\n samm:preferredName \"speed\"@en ;\r\n samm:description \"Scalar representation of speed of an object in kilometers per hour.\"@en ;\r\n samm:dataType xsd:float ;\r\n samm-c:unit unit:kilometrePerHour .\r\n\r\n:TrafficLight a samm-c:Enumeration ;\r\n samm:preferredName \"warning level\"@en ;\r\n samm:description \"Represents if speed of position change is within specification (green), within tolerance (yellow), or outside specification (red).\"@en ;\r\n samm:dataType xsd:string ;\r\n samm-c:values ( \"green\" \"yellow\" \"red\" ) .\r\n\r\n:SpatialPosition a samm:Entity ;\r\n samm:preferredName \"spatial position\"@en ;\r\n samm:description \"Represents latitude, longitude and altitude information in the WGS84 geodetic reference datum\"@en ;\r\n samm:see ;\r\n samm:properties ( :latitude :longitude [ samm:property :altitude; samm:optional true ] ) .\r\n\r\n:latitude a samm:Property ;\r\n samm:preferredName \"latitude\"@en ;\r\n samm:description \"latitude coordinate in space (WGS84)\"@en ;\r\n samm:see ;\r\n samm:characteristic :Coordinate ;\r\n samm:exampleValue \"9.1781\"^^xsd:decimal .\r\n\r\n:longitude a samm:Property ;\r\n samm:preferredName \"longitude\"@en ;\r\n samm:description \"longitude coordinate in space (WGS84)\"@en ;\r\n samm:see ;\r\n samm:characteristic :Coordinate ;\r\n samm:exampleValue \"48.80835\"^^xsd:decimal .\r\n\r\n:altitude a samm:Property ;\r\n samm:preferredName \"altitude\"@en ;\r\n samm:description \"Elevation above sea level zero\"@en ;\r\n samm:see ;\r\n samm:characteristic :MetresAboveMeanSeaLevel ;\r\n samm:exampleValue \"153\"^^xsd:float .\r\n\r\n:Coordinate a samm-c:Measurement ;\r\n samm:preferredName \"coordinate\"@en ;\r\n samm:description \"Representing the geographical coordinate\"@en ;\r\n samm:dataType xsd:decimal ;\r\n samm-c:unit unit:degreeUnitOfAngle .\r\n\r\n:MetresAboveMeanSeaLevel a samm-c:Measurement ;\r\n samm:preferredName \"metres above mean sea level\"@en ;\r\n samm:description \"Signifies the vertical distance in reference to a historic mean sea level as a vertical datum\"@en ;\r\n samm:see ;\r\n samm:dataType xsd:float ;\r\n samm-c:unit unit:metre .\r\n" + }, + "url": { + "raw": "http://localhost:{{port}}/ame/api/generate/open-api-spec?output=json&baseUrl=http://www.test.com&includeQueryApi=true&useSemanticVersion=true&pagingOption=NO_PAGING&resourcePath=/resource/%7BresourceId%7D&jsonProperties=%7B\"wrongId\":%7B\"name\":\"wrongId\",\"in\":\"path\",\"description\":\"An example resource Id.\",\"required\":true,\"schema\":%7B\"type\":\"string\"%7D%7D%7D", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "ame", + "api", + "generate", + "open-api-spec" + ], + "query": [ + { + "key": "output", + "value": "json" + }, + { + "key": "baseUrl", + "value": "http://www.test.com" + }, + { + "key": "includeQueryApi", + "value": "true" + }, + { + "key": "useSemanticVersion", + "value": "true" + }, + { + "key": "pagingOption", + "value": "NO_PAGING" + }, + { + "key": "resourcePath", + "value": "/resource/%7BresourceId%7D" + }, + { + "key": "jsonProperties", + "value": "%7B\"wrongId\":%7B\"name\":\"wrongId\",\"in\":\"path\",\"description\":\"An example resource Id.\",\"required\":true,\"schema\":%7B\"type\":\"string\"%7D%7D%7D" + } + ] + } + }, + "response": [] + }, { "name": "GenerateYamlOpenApiSpec", "event": [ @@ -858,6 +1006,154 @@ }, "response": [] }, + { + "name": "GenerateYamlOpenApiSpecWithResourcePath", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "pm.test(\"Response body is valid\", function () {\r", + " const jsonData = pm.response.text();\r", + " pm.expect(jsonData).to.include(\"openapi: 3.0.3\");\r", + " pm.expect(jsonData).to.include(\"title: movement\");\r", + " pm.expect(jsonData).to.include(\"version: v1\");\r", + " pm.expect(jsonData).to.include(\"url: http://www.test.com/api/v1\");\r", + " pm.expect(jsonData).to.include('/resource/{resourceId}');\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "@prefix samm: .\r\n@prefix samm-c: .\r\n@prefix samm-e: .\r\n@prefix unit: .\r\n@prefix rdf: .\r\n@prefix rdfs: .\r\n@prefix xsd: .\r\n@prefix : .\r\n\r\n:Movement a samm:Aspect ;\r\n samm:preferredName \"movement\"@en ;\r\n samm:description \"Aspect for movement information\"@en ;\r\n samm:properties ( :isMoving :position :speed :speedLimitWarning ) ;\r\n samm:operations ( ) ;\r\n samm:events ( ) .\r\n\r\n:isMoving a samm:Property ;\r\n samm:preferredName \"is moving\"@en ;\r\n samm:description \"Flag indicating whether the asset is currently moving\"@en ;\r\n samm:characteristic samm-c:Boolean .\r\n\r\n:position a samm:Property ;\r\n samm:preferredName \"position\"@en ;\r\n samm:description \"Indicates a position\"@en ;\r\n samm:characteristic :SpatialPositionCharacteristic .\r\n\r\n:speed a samm:Property ;\r\n samm:preferredName \"speed\"@en ;\r\n samm:description \"speed of vehicle\"@en ;\r\n samm:characteristic :Speed .\r\n\r\n:speedLimitWarning a samm:Property ;\r\n samm:preferredName \"speed limit warning\"@en ;\r\n samm:description \"Indicates if the speed limit is adhered to.\"@en ;\r\n samm:characteristic :TrafficLight .\r\n\r\n:SpatialPositionCharacteristic a samm-c:SingleEntity ;\r\n samm:preferredName \"spatial position characteristic\"@en ;\r\n samm:description \"Represents a single position in space with optional z coordinate.\"@en ;\r\n samm:dataType :SpatialPosition .\r\n\r\n:Speed a samm-c:Measurement ;\r\n samm:preferredName \"speed\"@en ;\r\n samm:description \"Scalar representation of speed of an object in kilometers per hour.\"@en ;\r\n samm:dataType xsd:float ;\r\n samm-c:unit unit:kilometrePerHour .\r\n\r\n:TrafficLight a samm-c:Enumeration ;\r\n samm:preferredName \"warning level\"@en ;\r\n samm:description \"Represents if speed of position change is within specification (green), within tolerance (yellow), or outside specification (red).\"@en ;\r\n samm:dataType xsd:string ;\r\n samm-c:values ( \"green\" \"yellow\" \"red\" ) .\r\n\r\n:SpatialPosition a samm:Entity ;\r\n samm:preferredName \"spatial position\"@en ;\r\n samm:description \"Represents latitude, longitude and altitude information in the WGS84 geodetic reference datum\"@en ;\r\n samm:see ;\r\n samm:properties ( :latitude :longitude [ samm:property :altitude; samm:optional true ] ) .\r\n\r\n:latitude a samm:Property ;\r\n samm:preferredName \"latitude\"@en ;\r\n samm:description \"latitude coordinate in space (WGS84)\"@en ;\r\n samm:see ;\r\n samm:characteristic :Coordinate ;\r\n samm:exampleValue \"9.1781\"^^xsd:decimal .\r\n\r\n:longitude a samm:Property ;\r\n samm:preferredName \"longitude\"@en ;\r\n samm:description \"longitude coordinate in space (WGS84)\"@en ;\r\n samm:see ;\r\n samm:characteristic :Coordinate ;\r\n samm:exampleValue \"48.80835\"^^xsd:decimal .\r\n\r\n:altitude a samm:Property ;\r\n samm:preferredName \"altitude\"@en ;\r\n samm:description \"Elevation above sea level zero\"@en ;\r\n samm:see ;\r\n samm:characteristic :MetresAboveMeanSeaLevel ;\r\n samm:exampleValue \"153\"^^xsd:float .\r\n\r\n:Coordinate a samm-c:Measurement ;\r\n samm:preferredName \"coordinate\"@en ;\r\n samm:description \"Representing the geographical coordinate\"@en ;\r\n samm:dataType xsd:decimal ;\r\n samm-c:unit unit:degreeUnitOfAngle .\r\n\r\n:MetresAboveMeanSeaLevel a samm-c:Measurement ;\r\n samm:preferredName \"metres above mean sea level\"@en ;\r\n samm:description \"Signifies the vertical distance in reference to a historic mean sea level as a vertical datum\"@en ;\r\n samm:see ;\r\n samm:dataType xsd:float ;\r\n samm-c:unit unit:metre .\r\n" + }, + "url": { + "raw": "http://localhost:{{port}}/ame/api/generate/open-api-spec?output=yaml&baseUrl=http://www.test.com&includeQueryApi=false&useSemanticVersion=false&pagingOption=OFFSET_BASED_PAGING&resourcePath=/resource/%7BresourceId%7D&ymlProperties=resourceId%3A%0A%20%20name%3A%20resourceId%0A%20%20in%3A%20path%0A%20%20description%3A%20An%20example%20resource%20Id.%0A%20%20required%3A%20true%0A%20%20schema%3A%0A%20%20%20%20type%3A%20string", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "ame", + "api", + "generate", + "open-api-spec" + ], + "query": [ + { + "key": "output", + "value": "yaml" + }, + { + "key": "baseUrl", + "value": "http://www.test.com" + }, + { + "key": "includeQueryApi", + "value": "false" + }, + { + "key": "useSemanticVersion", + "value": "false" + }, + { + "key": "pagingOption", + "value": "OFFSET_BASED_PAGING" + }, + { + "key": "resourcePath", + "value": "/resource/%7BresourceId%7D" + }, + { + "key": "ymlProperties", + "value": "resourceId%3A%0A%20%20name%3A%20resourceId%0A%20%20in%3A%20path%0A%20%20description%3A%20An%20example%20resource%20Id.%0A%20%20required%3A%20true%0A%20%20schema%3A%0A%20%20%20%20type%3A%20string" + } + ] + } + }, + "response": [] + }, + { + "name": "GenerateYamlOpenApiSpecWithWrongResourcePath", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 400\", function () {\r", + " pm.response.to.have.status(400);\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "@prefix samm: .\r\n@prefix samm-c: .\r\n@prefix samm-e: .\r\n@prefix unit: .\r\n@prefix rdf: .\r\n@prefix rdfs: .\r\n@prefix xsd: .\r\n@prefix : .\r\n\r\n:Movement a samm:Aspect ;\r\n samm:preferredName \"movement\"@en ;\r\n samm:description \"Aspect for movement information\"@en ;\r\n samm:properties ( :isMoving :position :speed :speedLimitWarning ) ;\r\n samm:operations ( ) ;\r\n samm:events ( ) .\r\n\r\n:isMoving a samm:Property ;\r\n samm:preferredName \"is moving\"@en ;\r\n samm:description \"Flag indicating whether the asset is currently moving\"@en ;\r\n samm:characteristic samm-c:Boolean .\r\n\r\n:position a samm:Property ;\r\n samm:preferredName \"position\"@en ;\r\n samm:description \"Indicates a position\"@en ;\r\n samm:characteristic :SpatialPositionCharacteristic .\r\n\r\n:speed a samm:Property ;\r\n samm:preferredName \"speed\"@en ;\r\n samm:description \"speed of vehicle\"@en ;\r\n samm:characteristic :Speed .\r\n\r\n:speedLimitWarning a samm:Property ;\r\n samm:preferredName \"speed limit warning\"@en ;\r\n samm:description \"Indicates if the speed limit is adhered to.\"@en ;\r\n samm:characteristic :TrafficLight .\r\n\r\n:SpatialPositionCharacteristic a samm-c:SingleEntity ;\r\n samm:preferredName \"spatial position characteristic\"@en ;\r\n samm:description \"Represents a single position in space with optional z coordinate.\"@en ;\r\n samm:dataType :SpatialPosition .\r\n\r\n:Speed a samm-c:Measurement ;\r\n samm:preferredName \"speed\"@en ;\r\n samm:description \"Scalar representation of speed of an object in kilometers per hour.\"@en ;\r\n samm:dataType xsd:float ;\r\n samm-c:unit unit:kilometrePerHour .\r\n\r\n:TrafficLight a samm-c:Enumeration ;\r\n samm:preferredName \"warning level\"@en ;\r\n samm:description \"Represents if speed of position change is within specification (green), within tolerance (yellow), or outside specification (red).\"@en ;\r\n samm:dataType xsd:string ;\r\n samm-c:values ( \"green\" \"yellow\" \"red\" ) .\r\n\r\n:SpatialPosition a samm:Entity ;\r\n samm:preferredName \"spatial position\"@en ;\r\n samm:description \"Represents latitude, longitude and altitude information in the WGS84 geodetic reference datum\"@en ;\r\n samm:see ;\r\n samm:properties ( :latitude :longitude [ samm:property :altitude; samm:optional true ] ) .\r\n\r\n:latitude a samm:Property ;\r\n samm:preferredName \"latitude\"@en ;\r\n samm:description \"latitude coordinate in space (WGS84)\"@en ;\r\n samm:see ;\r\n samm:characteristic :Coordinate ;\r\n samm:exampleValue \"9.1781\"^^xsd:decimal .\r\n\r\n:longitude a samm:Property ;\r\n samm:preferredName \"longitude\"@en ;\r\n samm:description \"longitude coordinate in space (WGS84)\"@en ;\r\n samm:see ;\r\n samm:characteristic :Coordinate ;\r\n samm:exampleValue \"48.80835\"^^xsd:decimal .\r\n\r\n:altitude a samm:Property ;\r\n samm:preferredName \"altitude\"@en ;\r\n samm:description \"Elevation above sea level zero\"@en ;\r\n samm:see ;\r\n samm:characteristic :MetresAboveMeanSeaLevel ;\r\n samm:exampleValue \"153\"^^xsd:float .\r\n\r\n:Coordinate a samm-c:Measurement ;\r\n samm:preferredName \"coordinate\"@en ;\r\n samm:description \"Representing the geographical coordinate\"@en ;\r\n samm:dataType xsd:decimal ;\r\n samm-c:unit unit:degreeUnitOfAngle .\r\n\r\n:MetresAboveMeanSeaLevel a samm-c:Measurement ;\r\n samm:preferredName \"metres above mean sea level\"@en ;\r\n samm:description \"Signifies the vertical distance in reference to a historic mean sea level as a vertical datum\"@en ;\r\n samm:see ;\r\n samm:dataType xsd:float ;\r\n samm-c:unit unit:metre .\r\n" + }, + "url": { + "raw": "http://localhost:{{port}}/ame/api/generate/open-api-spec?output=yaml&baseUrl=http://www.test.com&includeQueryApi=false&useSemanticVersion=false&pagingOption=OFFSET_BASED_PAGING&resourcePath=/resource/%7BresourceId%7D&ymlProperties=wrongId%3A%0A%20%20name%3A%20wrongId%0A%20%20in%3A%20path%0A%20%20description%3A%20An%20example%20resource%20Id.%0A%20%20required%3A%20true%0A%20%20schema%3A%0A%20%20%20%20type%3A%20string", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "ame", + "api", + "generate", + "open-api-spec" + ], + "query": [ + { + "key": "output", + "value": "yaml" + }, + { + "key": "baseUrl", + "value": "http://www.test.com" + }, + { + "key": "includeQueryApi", + "value": "false" + }, + { + "key": "useSemanticVersion", + "value": "false" + }, + { + "key": "pagingOption", + "value": "OFFSET_BASED_PAGING" + }, + { + "key": "resourcePath", + "value": "/resource/%7BresourceId%7D" + }, + { + "key": "ymlProperties", + "value": "wrongId%3A%0A%20%20name%3A%20wrongId%0A%20%20in%3A%20path%0A%20%20description%3A%20An%20example%20resource%20Id.%0A%20%20required%3A%20true%0A%20%20schema%3A%0A%20%20%20%20type%3A%20string" + } + ] + } + }, + "response": [] + }, { "name": "GenerateJsonOpenApiSpecFromInvalidAspectModel", "event": [ diff --git a/aspect-model-editor-service/src/main/java/org/eclipse/esmf/ame/services/GenerateService.java b/aspect-model-editor-service/src/main/java/org/eclipse/esmf/ame/services/GenerateService.java index ed57856a..9f85cabe 100644 --- a/aspect-model-editor-service/src/main/java/org/eclipse/esmf/ame/services/GenerateService.java +++ b/aspect-model-editor-service/src/main/java/org/eclipse/esmf/ame/services/GenerateService.java @@ -20,6 +20,7 @@ import java.util.Optional; import org.apache.commons.lang3.LocaleUtils; +import org.eclipse.esmf.ame.exceptions.GenerationException; import org.eclipse.esmf.ame.exceptions.InvalidAspectModelException; import org.eclipse.esmf.ame.resolver.strategy.FileSystemStrategy; import org.eclipse.esmf.ame.resolver.strategy.utils.ResolverUtils; @@ -50,6 +51,7 @@ public class GenerateService { private static final String COULD_NOT_LOAD_ASPECT = "Could not load Aspect"; private static final String COULD_NOT_LOAD_ASPECT_MODEL = "Could not load Aspect model, please make sure the model is valid."; + public static final String WRONG_RESOURCE_PATH_ID = "The resource path ID and properties ID do not match. Please verify and correct them."; public GenerateService() { DataType.setupTypeMapping(); @@ -96,33 +98,33 @@ public String sampleJSONPayload( final String aspectModel ) { } } - public String generateAASXFile( String aspectModel ) { + public String generateAASXFile( final String aspectModel ) { final AspectModelAasGenerator generator = new AspectModelAasGenerator(); final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - AspectContext aspectContext = generateAspectContext( aspectModel ); + final AspectContext aspectContext = generateAspectContext( aspectModel ); generator.generate( AasFileFormat.AASX, aspectContext.aspect(), name -> outputStream ); return outputStream.toString( StandardCharsets.UTF_8 ); } - public String generateAasXmlFile( String aspectModel ) { + public String generateAasXmlFile( final String aspectModel ) { final AspectModelAasGenerator generator = new AspectModelAasGenerator(); final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - AspectContext aspectContext = generateAspectContext( aspectModel ); + final AspectContext aspectContext = generateAspectContext( aspectModel ); generator.generate( AasFileFormat.XML, aspectContext.aspect(), name -> outputStream ); return outputStream.toString( StandardCharsets.UTF_8 ); } - public String generateAasJsonFile( String aspectModel ) { + public String generateAasJsonFile( final String aspectModel ) { final AspectModelAasGenerator generator = new AspectModelAasGenerator(); final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - AspectContext aspectContext = generateAspectContext( aspectModel ); + final AspectContext aspectContext = generateAspectContext( aspectModel ); generator.generate( AasFileFormat.JSON, aspectContext.aspect(), name -> outputStream ); @@ -140,13 +142,19 @@ private AspectContext generateAspectContext( final String aspectModel ) { } public String generateYamlOpenApiSpec( final String language, final String aspectModel, final String baseUrl, - final boolean includeQueryApi, final boolean useSemanticVersion, final Optional pagingOption ) { + final boolean includeQueryApi, final boolean useSemanticVersion, final Optional pagingOption, + final Optional resourcePath, final Optional yamlProperties ) { try { - final AspectModelOpenApiGenerator generator = new AspectModelOpenApiGenerator(); - - return generator.applyForYaml( ResolverUtils.resolveAspectFromModel( aspectModel ), - useSemanticVersion, baseUrl, Optional.empty(), Optional.empty(), includeQueryApi, pagingOption, + final String ymlOutput = new AspectModelOpenApiGenerator().applyForYaml( + ResolverUtils.resolveAspectFromModel( aspectModel ), + useSemanticVersion, baseUrl, resourcePath, yamlProperties, includeQueryApi, pagingOption, Locale.forLanguageTag( language ) ); + + if ( ymlOutput.equals( "--- {}\n" ) ) { + throw new GenerationException( WRONG_RESOURCE_PATH_ID ); + } + + return ymlOutput; } catch ( final IOException e ) { LOG.error( "YAML OpenAPI specification could not be generated." ); throw new InvalidAspectModelException( "Error generating YAML OpenAPI specification", e ); @@ -154,20 +162,25 @@ public String generateYamlOpenApiSpec( final String language, final String aspec } public String generateJsonOpenApiSpec( final String language, final String aspectModel, final String baseUrl, - final boolean includeQueryApi, final boolean useSemanticVersion, final Optional pagingOption ) { + final boolean includeQueryApi, final boolean useSemanticVersion, final Optional pagingOption, + final Optional resourcePath, final Optional jsonProperties ) { try { - final AspectModelOpenApiGenerator generator = new AspectModelOpenApiGenerator(); - - final JsonNode json = generator.applyForJson( + final JsonNode json = new AspectModelOpenApiGenerator().applyForJson( ResolverUtils.resolveAspectFromModel( aspectModel ), useSemanticVersion, baseUrl, - Optional.empty(), Optional.empty(), includeQueryApi, pagingOption, LocaleUtils.toLocale( language ) ); + resourcePath, jsonProperties, includeQueryApi, pagingOption, LocaleUtils.toLocale( language ) ); final ByteArrayOutputStream out = new ByteArrayOutputStream(); final ObjectMapper objectMapper = new ObjectMapper(); objectMapper.writerWithDefaultPrettyPrinter().writeValue( out, json ); - return out.toString(); + final String jsonOutput = out.toString(); + + if ( jsonOutput.equals( "{ }" ) ) { + throw new GenerationException( WRONG_RESOURCE_PATH_ID ); + } + + return jsonOutput; } catch ( final IOException e ) { LOG.error( "JSON OpenAPI specification could not be generated." ); throw new InvalidAspectModelException( "Error generating JSON OpenAPI specification", e ); diff --git a/aspect-model-editor-service/src/test/java/org/eclipse/esmf/ame/services/GenerateServiceTest.java b/aspect-model-editor-service/src/test/java/org/eclipse/esmf/ame/services/GenerateServiceTest.java index d43f05aa..5345b046 100644 --- a/aspect-model-editor-service/src/test/java/org/eclipse/esmf/ame/services/GenerateServiceTest.java +++ b/aspect-model-editor-service/src/test/java/org/eclipse/esmf/ame/services/GenerateServiceTest.java @@ -22,6 +22,7 @@ import java.util.Optional; import org.eclipse.esmf.ame.config.TestConfig; +import org.eclipse.esmf.ame.exceptions.GenerationException; import org.eclipse.esmf.aspectmodel.generator.openapi.PagingOption; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -32,6 +33,9 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit.jupiter.SpringExtension; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + @ExtendWith( SpringExtension.class ) @SpringBootTest( classes = GenerateService.class ) @DirtiesContext( classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD ) @@ -68,26 +72,84 @@ void testAspectModelJsonSchema() throws IOException { } @Test - void testAspectModelJsonOpenApiSpec() throws IOException { + void testAspectModelJsonOpenApiSpecWithoutResourcePath() throws IOException { + final Path storagePath = Path.of( eclipseTestPath.toString(), model ); + final String testModel = Files.readString( storagePath, StandardCharsets.UTF_8 ); + + final String payload = generateService.generateJsonOpenApiSpec( "en", testModel, "https://test.com", false, false, + Optional.of( PagingOption.TIME_BASED_PAGING ), Optional.empty(), Optional.empty() ); + + assertTrue( payload.contains( "\"openapi\" : \"3.0.3\"" ) ); + assertTrue( payload.contains( "\"version\" : \"v1\"" ) ); + assertTrue( payload.contains( "\"title\" : \"AspectModelForService\"" ) ); + assertTrue( payload.contains( "\"url\" : \"https://test.com/api/v1\"" ) ); + } + + @Test + void testAspectModelJsonOpenApiSpecWithResourcePath() throws IOException { final Path storagePath = Path.of( eclipseTestPath.toString(), model ); final String testModel = Files.readString( storagePath, StandardCharsets.UTF_8 ); + final ObjectMapper objectMapper = new ObjectMapper(); + final Optional jsonProperties = Optional.of( objectMapper.readTree( """ + { + "resourceId": { + "name": "resourceId", + "in": "path", + "description": "An example resource Id.", + "required": true, + "schema": { + "type": "string" + } + } + } + """ ) ); + final String payload = generateService.generateJsonOpenApiSpec( "en", testModel, "https://test.com", false, false, - Optional.of( PagingOption.TIME_BASED_PAGING ) ); + Optional.of( PagingOption.TIME_BASED_PAGING ), Optional.of( "/resource/{resourceId}" ), jsonProperties ); assertTrue( payload.contains( "\"openapi\" : \"3.0.3\"" ) ); assertTrue( payload.contains( "\"version\" : \"v1\"" ) ); assertTrue( payload.contains( "\"title\" : \"AspectModelForService\"" ) ); assertTrue( payload.contains( "\"url\" : \"https://test.com/api/v1\"" ) ); + assertTrue( payload.contains( "\"/resource/{resourceId}\"" ) ); + assertTrue( payload.contains( "\"name\" : \"resourceId\"" ) ); + assertTrue( payload.contains( "\"in\" : \"path\"" ) ); + } + + @Test + void testAspectModelJsonOpenApiSpecWithWrongResourcePathProperties() throws IOException { + final Path storagePath = Path.of( eclipseTestPath.toString(), model ); + final String testModel = Files.readString( storagePath, StandardCharsets.UTF_8 ); + + final ObjectMapper objectMapper = new ObjectMapper(); + final Optional jsonProperties = Optional.of( objectMapper.readTree( """ + { + "wrongId": { + "name": "wrongId", + "in": "path", + "description": "An example resource Id.", + "required": true, + "schema": { + "type": "string" + } + } + } + """ ) ); + + assertThrows( GenerationException.class, () -> { + generateService.generateJsonOpenApiSpec( "en", testModel, "https://test.com", false, false, + Optional.of( PagingOption.TIME_BASED_PAGING ), Optional.of( "/resource/{resourceId}" ), jsonProperties ); + } ); } @Test - void testAspectModelYamlOpenApiSpec() throws IOException { + void testAspectModelYamlOpenApiSpecWithoutResourcePath() throws IOException { final Path storagePath = Path.of( eclipseTestPath.toString(), model ); final String testModel = Files.readString( storagePath, StandardCharsets.UTF_8 ); final String payload = generateService.generateYamlOpenApiSpec( "en", testModel, "https://test.com", false, false, - Optional.of( PagingOption.TIME_BASED_PAGING ) ); + Optional.of( PagingOption.TIME_BASED_PAGING ), Optional.empty(), Optional.empty() ); assertTrue( payload.contains( "openapi: 3.0.3" ) ); assertTrue( payload.contains( "title: AspectModel" ) ); @@ -95,6 +157,54 @@ void testAspectModelYamlOpenApiSpec() throws IOException { assertTrue( payload.contains( "url: https://test.com/api/v1" ) ); } + @Test + void testAspectModelYamlOpenApiSpecWithResourcePath() throws IOException { + final Path storagePath = Path.of( eclipseTestPath.toString(), model ); + final String testModel = Files.readString( storagePath, StandardCharsets.UTF_8 ); + + final Optional yamlProperties = Optional.of( """ + resourceId: + name: resourceId + in: path + description: An example resource Id. + required: true + schema: + type: string + """ ); + + final String payload = generateService.generateYamlOpenApiSpec( "en", testModel, "https://test.com", false, false, + Optional.of( PagingOption.TIME_BASED_PAGING ), Optional.of( "/resource/{resourceId}" ), yamlProperties ); + + assertTrue( payload.contains( "openapi: 3.0.3" ) ); + assertTrue( payload.contains( "title: AspectModel" ) ); + assertTrue( payload.contains( "version: v1" ) ); + assertTrue( payload.contains( "url: https://test.com/api/v1" ) ); + assertTrue( payload.contains( "/resource/{resourceId}" ) ); + assertTrue( payload.contains( "name: resourceId" ) ); + assertTrue( payload.contains( "in: path" ) ); + } + + @Test + void testAspectModelYamlOpenApiSpecWithWrongResourcePathProperties() throws IOException { + final Path storagePath = Path.of( eclipseTestPath.toString(), model ); + final String testModel = Files.readString( storagePath, StandardCharsets.UTF_8 ); + + final Optional yamlProperties = Optional.of( """ + wrongId: + name: wrongId + in: path + description: An example resource Id. + required: true + schema: + type: string + """ ); + + assertThrows( GenerationException.class, () -> { + generateService.generateYamlOpenApiSpec( "en", testModel, "https://test.com", false, false, + Optional.of( PagingOption.TIME_BASED_PAGING ), Optional.of( "/resource/{resourceId}" ), yamlProperties ); + } ); + } + @Test void testAspectModelAASX() throws IOException { final Path storagePath = Path.of( eclipseTestPath.toString(), model ); diff --git a/aspect-model-editor-web-core/src/main/java/org/eclipse/esmf/ame/exceptions/ResponseExceptionHandler.java b/aspect-model-editor-web-core/src/main/java/org/eclipse/esmf/ame/exceptions/ResponseExceptionHandler.java index 08b745e7..96283b8b 100644 --- a/aspect-model-editor-web-core/src/main/java/org/eclipse/esmf/ame/exceptions/ResponseExceptionHandler.java +++ b/aspect-model-editor-web-core/src/main/java/org/eclipse/esmf/ame/exceptions/ResponseExceptionHandler.java @@ -165,6 +165,19 @@ public ResponseEntity handleInvalidAspectModelException( final We return error( HttpStatus.CONFLICT, request, e, e.getMessage() ); } + /** + * Method for handling exception to type {@link GenerationException} + * + * @param request the Http request + * @param e the exception which occurred + * @return the custom {@link ErrorResponse} as {@link ResponseEntity} for the exception + */ + @ExceptionHandler( GenerationException.class ) + public ResponseEntity handleInvalidAspectModelException( final WebRequest request, + final GenerationException e ) { + return error( HttpStatus.BAD_REQUEST, request, e, e.getMessage() ); + } + /** * Method for handling exception to type {@link FileHandlingException} * @@ -215,37 +228,40 @@ private static String getLogRequestMessage( final String requestURL, final Throw httpStatus.value(), requestURL ); } - private ResponseEntity handleExceptionInternal(Exception ex, @Nullable ErrorResponse body, HttpHeaders headers, HttpStatusCode statusCode, WebRequest request) { + private ResponseEntity handleExceptionInternal( final Exception ex, + @Nullable final ErrorResponse body, + final HttpHeaders headers, final HttpStatusCode statusCode, final WebRequest request ) { - if (isResponseCommitted(request)) { - logger.warn("Response already committed. Ignoring: " + ex); + if ( isResponseCommitted( request ) ) { + logger.warn( "Response already committed. Ignoring: " + ex ); return null; } - if (statusCode.equals(HttpStatus.INTERNAL_SERVER_ERROR)) { - request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST); + if ( statusCode.equals( HttpStatus.INTERNAL_SERVER_ERROR ) ) { + request.setAttribute( WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST ); } - if (body == null && ex instanceof org.springframework.web.ErrorResponse) { - return handleErrorResponse(ex, headers, statusCode, request); + if ( body == null && ex instanceof org.springframework.web.ErrorResponse ) { + return handleErrorResponse( ex, headers, statusCode, request ); } - return new ResponseEntity<>(body, headers, statusCode); + return new ResponseEntity<>( body, headers, statusCode ); } - private boolean isResponseCommitted(WebRequest request) { - if (request instanceof ServletWebRequest) { - HttpServletResponse response = ((ServletWebRequest) request).getResponse(); + private boolean isResponseCommitted( final WebRequest request ) { + if ( request instanceof ServletWebRequest ) { + final HttpServletResponse response = ((ServletWebRequest) request).getResponse(); return response != null && response.isCommitted(); } return false; } - private ResponseEntity handleErrorResponse(Exception ex, HttpHeaders headers, HttpStatusCode statusCode, WebRequest request) { - ResponseEntity entity = super.handleExceptionInternal(ex, null, headers, statusCode, request); - Object responseBody = Objects.requireNonNull( entity ).getBody(); - ErrorResponse errorResponse = (responseBody instanceof ErrorResponse) ? (ErrorResponse) responseBody : null; + private ResponseEntity handleErrorResponse( final Exception ex, final HttpHeaders headers, + final HttpStatusCode statusCode, final WebRequest request ) { + final ResponseEntity entity = super.handleExceptionInternal( ex, null, headers, statusCode, request ); + final Object responseBody = Objects.requireNonNull( entity ).getBody(); + final ErrorResponse errorResponse = (responseBody instanceof ErrorResponse) ? (ErrorResponse) responseBody : null; - return new ResponseEntity<>(errorResponse, entity.getHeaders(), entity.getStatusCode()); + return new ResponseEntity<>( errorResponse, entity.getHeaders(), entity.getStatusCode() ); } } diff --git a/aspect-model-editor-web/src/main/java/org/eclipse/esmf/ame/api/GenerateController.java b/aspect-model-editor-web/src/main/java/org/eclipse/esmf/ame/api/GenerateController.java index 586e5003..6f8466d2 100644 --- a/aspect-model-editor-web/src/main/java/org/eclipse/esmf/ame/api/GenerateController.java +++ b/aspect-model-editor-web/src/main/java/org/eclipse/esmf/ame/api/GenerateController.java @@ -25,6 +25,9 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + /** * Controller class that supports the generation of the aspect model into other formats. */ @@ -47,8 +50,7 @@ public GenerateController( final GenerateService generateService ) { @PostMapping( "documentation" ) public ResponseEntity generateHtml( @RequestBody final String aspectModel, @RequestParam( name = "language" ) final String language ) throws IOException { - return ResponseEntity.ok( - generateService.generateHtmlDocument( aspectModel, language ) ); + return ResponseEntity.ok( generateService.generateHtmlDocument( aspectModel, language ) ); } /** @@ -131,14 +133,36 @@ public ResponseEntity openApiSpec( @RequestBody final String aspectModel @RequestParam( name = "includeQueryApi", defaultValue = "false" ) final boolean includeQueryApi, @RequestParam( name = "useSemanticVersion", defaultValue = "false" ) final boolean useSemanticVersion, @RequestParam( name = "pagingOption", defaultValue = "TIME_BASED_PAGING" ) - final Optional pagingOption ) { + final Optional pagingOption, + @RequestParam( name = "resourcePath", defaultValue = "" ) final String resourcePath, + @RequestParam( name = "ymlProperties", defaultValue = "" ) final String ymlProperties, + @RequestParam( name = "jsonProperties", defaultValue = "" ) final String jsonProperties ) + throws JsonProcessingException { + + final Optional properties = + !resourcePath.isEmpty() && (!ymlProperties.isEmpty() || !jsonProperties.isEmpty()) + ? Optional.of( !ymlProperties.isEmpty() ? ymlProperties : jsonProperties ) + : Optional.empty(); - final String openApiOutput = output.equals( "json" ) ? - generateService.generateJsonOpenApiSpec( language, aspectModel, baseUrl, includeQueryApi, - useSemanticVersion, pagingOption ) : - generateService.generateYamlOpenApiSpec( language, aspectModel, baseUrl, includeQueryApi, - useSemanticVersion, pagingOption ); + final String openApiOutput = generateOpenApiSpec( language, aspectModel, baseUrl, includeQueryApi, + useSemanticVersion, pagingOption, resourcePath, properties, output ); return ResponseEntity.ok( openApiOutput ); } + + private String generateOpenApiSpec( final String language, final String aspectModel, final String baseUrl, + final boolean includeQueryApi, final boolean useSemanticVersion, final Optional pagingOption, + final String resourcePath, final Optional properties, final String output ) + throws JsonProcessingException { + + if ( output.equals( "json" ) ) { + final ObjectMapper objectMapper = new ObjectMapper(); + return generateService.generateJsonOpenApiSpec( language, aspectModel, baseUrl, includeQueryApi, + useSemanticVersion, pagingOption, Optional.of( resourcePath ), + Optional.of( objectMapper.readTree( properties.orElse( "{}" ) ) ) ); + } + + return generateService.generateYamlOpenApiSpec( language, aspectModel, baseUrl, includeQueryApi, + useSemanticVersion, pagingOption, Optional.of( resourcePath ), properties ); + } }