From 8eac2405fff282c7b37a43fc6c53a91421083f7a Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Sat, 1 Jun 2024 20:03:49 +0200 Subject: [PATCH] CAMEL-20825: camel-rest - Contract first for api-doc should include the spec --- .../catalog/models/restConfiguration.json | 2 +- .../camel/catalog/schemas/camel-spring.xsd | 1 + components/camel-openapi-java/pom.xml | 4 ++ .../camel/openapi/RestOpenApiReader.java | 48 ++++++++++++++++++- .../apache/camel/spi/RestConfiguration.java | 3 +- .../camel/model/rest/restConfiguration.json | 2 +- .../model/rest/RestHostNameResolver.java | 3 +- .../modules/ROOT/pages/rest-dsl-openapi.adoc | 33 ++++++++++++- .../deserializers/ModelDeserializers.java | 2 +- .../resources/schema/camelYamlDsl.json | 2 +- 10 files changed, 91 insertions(+), 9 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/restConfiguration.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/restConfiguration.json index 357b2846746ad..740a4708e864d 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/restConfiguration.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/restConfiguration.json @@ -25,7 +25,7 @@ "apiContextPath": { "index": 10, "kind": "attribute", "displayName": "Api Context Path", "group": "consumer", "label": "consumer", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Sets a leading context-path the REST API will be using. This can be used when using components such as camel-servlet where the deployed web application is deployed using a context-path." }, "apiContextRouteId": { "index": 11, "kind": "attribute", "displayName": "Api Context Route Id", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Sets the route id to use for the route that services the REST API. The route will by default use an auto assigned route id." }, "apiVendorExtension": { "index": 12, "kind": "attribute", "displayName": "Api Vendor Extension", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether vendor extension is enabled in the Rest APIs. If enabled then Camel will include additional information as vendor extension (eg keys starting with x-) such as route ids, class names etc. Not all 3rd party API gateways and tools supports vendor-extensions when importing your API docs." }, - "hostNameResolver": { "index": 13, "kind": "attribute", "displayName": "Host Name Resolver", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "enum", "javaType": "org.apache.camel.model.rest.RestHostNameResolver", "enum": [ "allLocalIp", "localHostName", "localIp" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "allLocalIp", "description": "If no hostname has been explicit configured, then this resolver is used to compute the hostname the REST service will be using." }, + "hostNameResolver": { "index": 13, "kind": "attribute", "displayName": "Host Name Resolver", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "enum", "javaType": "org.apache.camel.model.rest.RestHostNameResolver", "enum": [ "allLocalIp", "localHostName", "localIp", "none" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "allLocalIp", "description": "If no hostname has been explicit configured, then this resolver is used to compute the hostname the REST service will be using." }, "bindingMode": { "index": 14, "kind": "attribute", "displayName": "Binding Mode", "group": "common", "required": false, "type": "enum", "javaType": "org.apache.camel.model.rest.RestBindingMode", "enum": [ "off", "auto", "json", "xml", "json_xml" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "off", "description": "Sets the binding mode to use. The default value is off" }, "bindingPackageScan": { "index": 15, "kind": "attribute", "displayName": "Binding Package Scan", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Package name to use as base (offset) for classpath scanning of POJO classes are located when using binding mode is enabled for JSon or XML. Multiple package names can be separated by comma." }, "skipBindingOnErrorCode": { "index": 16, "kind": "attribute", "displayName": "Skip Binding On Error Code", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether to skip binding on output if there is a custom HTTP error code header. This allows to build custom error messages that do not bind to json \/ xml etc, as success messages otherwise will do." }, diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd index 61209fbf610f8..e92a71aeca2a6 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd @@ -18201,6 +18201,7 @@ An optional certificate alias to use. This is useful when the keystore has multi + diff --git a/components/camel-openapi-java/pom.xml b/components/camel-openapi-java/pom.xml index 98fb640c7e5fd..dc34c7720c907 100644 --- a/components/camel-openapi-java/pom.xml +++ b/components/camel-openapi-java/pom.xml @@ -45,6 +45,10 @@ org.apache.camel camel-core-engine + + org.apache.camel + camel-platform-http + org.apache.camel camel-xml-io diff --git a/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestOpenApiReader.java b/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestOpenApiReader.java index 7140fcf4cb599..818871634625f 100644 --- a/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestOpenApiReader.java +++ b/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestOpenApiReader.java @@ -21,6 +21,7 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodType; import java.lang.reflect.Method; +import java.net.UnknownHostException; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @@ -61,10 +62,13 @@ import io.swagger.v3.oas.models.security.Scopes; import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; import io.swagger.v3.oas.models.tags.Tag; import io.swagger.v3.parser.OpenAPIV3Parser; import io.swagger.v3.parser.core.models.SwaggerParseResult; import org.apache.camel.CamelContext; +import org.apache.camel.component.platform.http.PlatformHttpComponent; +import org.apache.camel.component.platform.http.spi.PlatformHttpEngine; import org.apache.camel.model.rest.ApiKeyDefinition; import org.apache.camel.model.rest.BasicAuthDefinition; import org.apache.camel.model.rest.BearerTokenDefinition; @@ -76,6 +80,7 @@ import org.apache.camel.model.rest.ResponseHeaderDefinition; import org.apache.camel.model.rest.ResponseMessageDefinition; import org.apache.camel.model.rest.RestDefinition; +import org.apache.camel.model.rest.RestHostNameResolver; import org.apache.camel.model.rest.RestPropertyDefinition; import org.apache.camel.model.rest.RestSecuritiesDefinition; import org.apache.camel.model.rest.RestSecurityDefinition; @@ -84,10 +89,13 @@ import org.apache.camel.spi.ClassResolver; import org.apache.camel.spi.NodeIdFactory; import org.apache.camel.spi.Resource; +import org.apache.camel.spi.RestConfiguration; import org.apache.camel.support.CamelContextHelper; import org.apache.camel.support.ObjectHelper; import org.apache.camel.support.PluginHelper; +import org.apache.camel.support.RestComponentHelper; import org.apache.camel.util.FileUtil; +import org.apache.camel.util.HostUtils; import org.apache.camel.util.IOHelper; import org.apache.commons.lang3.ClassUtils; import org.slf4j.Logger; @@ -135,11 +143,12 @@ private static List getValue(CamelContext camelContext, List lis * @param classResolver class resolver to use @return the openApi model * @throws ClassNotFoundException is thrown if error loading class * @throws IOException is thrown if error loading openapi specification + * @throws UnknownHostException is thrown if error resolving local hostname */ public OpenAPI read( CamelContext camelContext, List rests, BeanConfig config, String camelContextId, ClassResolver classResolver) - throws ClassNotFoundException, IOException { + throws ClassNotFoundException, IOException, UnknownHostException { // contract first, then load the specification as-is and use as response for (RestDefinition rest : rests) { @@ -151,7 +160,42 @@ public OpenAPI read( IOHelper.close(is); OpenAPIV3Parser parser = new OpenAPIV3Parser(); SwaggerParseResult out = parser.readContents(data); - return out.getOpenAPI(); + OpenAPI answer = out.getOpenAPI(); + + String host = null; + RestConfiguration restConfig = camelContext.getRestConfiguration(); + if (restConfig.getHostNameResolver() != RestConfiguration.RestHostNameResolver.none) { + host = camelContext.getRestConfiguration().getApiHost(); + if (host == null || host.isEmpty()) { + String scheme = "http://"; + host = RestComponentHelper.resolveRestHostName(host, restConfig); + PlatformHttpComponent http = camelContext.getComponent("platform-http", PlatformHttpComponent.class); + if (http != null) { + int port = http.getEngine().getServerPort(); + if (port > 0) { + host = host + ":" + port; + if (port == 443) { + scheme = "https://"; + } + } + } + host = scheme + host; + } + } + if (host != null) { + String basePath = RestOpenApiSupport.getBasePathFromOasDocument(answer); + if (basePath == null) { + basePath = "/"; + } + if (!basePath.startsWith("/")) { + basePath = "/" + basePath; + } + Server server = new Server(); + server.setUrl(host + basePath); + answer.setServers(null); + answer.addServersItem(server); + } + return answer; } } } diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/RestConfiguration.java b/core/camel-api/src/main/java/org/apache/camel/spi/RestConfiguration.java index dc46c6987b314..0ecd481b5a951 100644 --- a/core/camel-api/src/main/java/org/apache/camel/spi/RestConfiguration.java +++ b/core/camel-api/src/main/java/org/apache/camel/spi/RestConfiguration.java @@ -45,7 +45,8 @@ public enum RestBindingMode { public enum RestHostNameResolver { allLocalIp, localIp, - localHostName + localHostName, + none; } private String component; diff --git a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/rest/restConfiguration.json b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/rest/restConfiguration.json index 357b2846746ad..740a4708e864d 100644 --- a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/rest/restConfiguration.json +++ b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/rest/restConfiguration.json @@ -25,7 +25,7 @@ "apiContextPath": { "index": 10, "kind": "attribute", "displayName": "Api Context Path", "group": "consumer", "label": "consumer", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Sets a leading context-path the REST API will be using. This can be used when using components such as camel-servlet where the deployed web application is deployed using a context-path." }, "apiContextRouteId": { "index": 11, "kind": "attribute", "displayName": "Api Context Route Id", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Sets the route id to use for the route that services the REST API. The route will by default use an auto assigned route id." }, "apiVendorExtension": { "index": 12, "kind": "attribute", "displayName": "Api Vendor Extension", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether vendor extension is enabled in the Rest APIs. If enabled then Camel will include additional information as vendor extension (eg keys starting with x-) such as route ids, class names etc. Not all 3rd party API gateways and tools supports vendor-extensions when importing your API docs." }, - "hostNameResolver": { "index": 13, "kind": "attribute", "displayName": "Host Name Resolver", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "enum", "javaType": "org.apache.camel.model.rest.RestHostNameResolver", "enum": [ "allLocalIp", "localHostName", "localIp" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "allLocalIp", "description": "If no hostname has been explicit configured, then this resolver is used to compute the hostname the REST service will be using." }, + "hostNameResolver": { "index": 13, "kind": "attribute", "displayName": "Host Name Resolver", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "enum", "javaType": "org.apache.camel.model.rest.RestHostNameResolver", "enum": [ "allLocalIp", "localHostName", "localIp", "none" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "allLocalIp", "description": "If no hostname has been explicit configured, then this resolver is used to compute the hostname the REST service will be using." }, "bindingMode": { "index": 14, "kind": "attribute", "displayName": "Binding Mode", "group": "common", "required": false, "type": "enum", "javaType": "org.apache.camel.model.rest.RestBindingMode", "enum": [ "off", "auto", "json", "xml", "json_xml" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "off", "description": "Sets the binding mode to use. The default value is off" }, "bindingPackageScan": { "index": 15, "kind": "attribute", "displayName": "Binding Package Scan", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Package name to use as base (offset) for classpath scanning of POJO classes are located when using binding mode is enabled for JSon or XML. Multiple package names can be separated by comma." }, "skipBindingOnErrorCode": { "index": 16, "kind": "attribute", "displayName": "Skip Binding On Error Code", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether to skip binding on output if there is a custom HTTP error code header. This allows to build custom error messages that do not bind to json \/ xml etc, as success messages otherwise will do." }, diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/rest/RestHostNameResolver.java b/core/camel-core-model/src/main/java/org/apache/camel/model/rest/RestHostNameResolver.java index 5e6e9249f213e..f9f0c08a4eafd 100644 --- a/core/camel-core-model/src/main/java/org/apache/camel/model/rest/RestHostNameResolver.java +++ b/core/camel-core-model/src/main/java/org/apache/camel/model/rest/RestHostNameResolver.java @@ -31,6 +31,7 @@ public enum RestHostNameResolver { allLocalIp, localIp, - localHostName + localHostName, + none } diff --git a/docs/user-manual/modules/ROOT/pages/rest-dsl-openapi.adoc b/docs/user-manual/modules/ROOT/pages/rest-dsl-openapi.adoc index 62ae96d5cfa03..af23014f4b50a 100644 --- a/docs/user-manual/modules/ROOT/pages/rest-dsl-openapi.adoc +++ b/docs/user-manual/modules/ROOT/pages/rest-dsl-openapi.adoc @@ -289,11 +289,42 @@ Here Camel will detect the `schema` part: } ---- -And compute the class name as `Pet` and attempt to disover this class from classpath scanning specified via the `bindingPackageScan` option. +And compute the class name as `Pet` and attempt to discover this class from classpath scanning specified via the `bindingPackageScan` option. You can source code generate Java POJO classes from an OpenAPI specification via tooling such as the `swagger-codegen-maven-plugin` Maven plugin. For more details see this https://github.com/apache/camel-spring-boot-examples/tree/main/openapi-contract-first[Spring Boot example]. +=== Expose API specification + +The OpenAPI specification is by default not exposed on the HTTP endpoint. You can make this happen by setting the rest-configuration as follows: + +[source,yaml] +---- +- restConfiguration: + apiContextPath: /api-doc +---- + +Then the specification is accessible on `/api-doc` on the embedded HTTP server, so typically that would be `http://localhost:8080/api-doc`. + +In the returned API specification the `server` section has been modified to return the IP of the current server. This can be controlled via: + + +[source,yaml] +---- +- restConfiguration: + apiContextPath: /api-doc + hostNameResolver: localIp +---- + +And you can turn this off by setting the value to `none` so the server part is taken verbatim from the specification file. + +[source,yaml] +---- +- restConfiguration: + apiContextPath: /api-doc + hostNameResolver: none +---- + == Examples You can find a few examples such as: diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/generated/java/org/apache/camel/dsl/yaml/deserializers/ModelDeserializers.java b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/generated/java/org/apache/camel/dsl/yaml/deserializers/ModelDeserializers.java index 2478efad9cb82..e3157f6b076ed 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/generated/java/org/apache/camel/dsl/yaml/deserializers/ModelDeserializers.java +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/generated/java/org/apache/camel/dsl/yaml/deserializers/ModelDeserializers.java @@ -13913,7 +13913,7 @@ protected boolean setProperty(RestBindingDefinition target, String propertyKey, @YamlProperty(name = "enableNoContentResponse", type = "boolean", description = "Whether to return HTTP 204 with an empty body when a response contains an empty JSON object or XML root object. The default value is false.", displayName = "Enable No Content Response"), @YamlProperty(name = "endpointProperty", type = "array:org.apache.camel.model.rest.RestPropertyDefinition", description = "Allows to configure as many additional properties for the rest endpoint in use.", displayName = "Endpoint Property"), @YamlProperty(name = "host", type = "string", description = "The hostname to use for exposing the REST service.", displayName = "Host"), - @YamlProperty(name = "hostNameResolver", type = "enum:allLocalIp,localHostName,localIp", defaultValue = "allLocalIp", description = "If no hostname has been explicit configured, then this resolver is used to compute the hostname the REST service will be using.", displayName = "Host Name Resolver"), + @YamlProperty(name = "hostNameResolver", type = "enum:allLocalIp,localHostName,localIp,none", defaultValue = "allLocalIp", description = "If no hostname has been explicit configured, then this resolver is used to compute the hostname the REST service will be using.", displayName = "Host Name Resolver"), @YamlProperty(name = "inlineRoutes", type = "boolean", description = "Inline routes in rest-dsl which are linked using direct endpoints. Each service in Rest DSL is an individual route, meaning that you would have at least two routes per service (rest-dsl, and the route linked from rest-dsl). By inlining (default) allows Camel to optimize and inline this as a single route, however this requires to use direct endpoints, which must be unique per service. If a route is not using direct endpoint then the rest-dsl is not inlined, and will become an individual route. This option is default true.", displayName = "Inline Routes"), @YamlProperty(name = "jsonDataFormat", type = "string", description = "Name of specific json data format to use. By default jackson will be used. Important: This option is only for setting a custom name of the data format, not to refer to an existing data format instance.", displayName = "Json Data Format"), @YamlProperty(name = "port", type = "string", description = "The port number to use for exposing the REST service. Notice if you use servlet component then the port number configured here does not apply, as the port number in use is the actual port number the servlet component is using. eg if using Apache Tomcat its the tomcat http port, if using Apache Karaf its the HTTP service in Karaf that uses port 8181 by default etc. Though in those situations setting the port number here, allows tooling and JMX to know the port number, so its recommended to set the port number to the number that the servlet engine uses.", displayName = "Port"), diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json index 749086b3a9713..b5f23143d5402 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json @@ -15393,7 +15393,7 @@ "title" : "Host Name Resolver", "description" : "If no hostname has been explicit configured, then this resolver is used to compute the hostname the REST service will be using.", "default" : "allLocalIp", - "enum" : [ "allLocalIp", "localHostName", "localIp" ] + "enum" : [ "allLocalIp", "localHostName", "localIp", "none" ] }, "inlineRoutes" : { "type" : "boolean",