blob: b7138eab3a7dcd6f1875aac806560fd25d4996e1 [file]
= REST OpenApi Component
:doctitle: REST OpenApi
:shortname: rest-openapi
:artifactid: camel-rest-openapi
:description: To call and expose REST services using OpenAPI specification as contract.
:since: 3.1
:supportlevel: Stable
:tabs-sync-option:
:component-header: Both producer and consumer are supported
*Since Camel {since}*
*{component-header}*
The REST OpenApi configures rest producers from
https://www.openapis.org/[OpenApi] (Open API) specification document and
delegates to a component implementing the _RestProducerFactory_
interface. Currently, known working components are:
* xref:http-component.adoc[http]
* xref:netty-http-component.adoc[netty-http]
* xref:undertow-component.adoc[undertow]
* xref:vertx-http-component.adoc[vertx-http]
IMPORTANT: Only OpenAPI spec version 3.x is supported. You cannot use the old Swagger 2.0 spec.
Maven users will need to add the following dependency to their
`pom.xml` for this component:
[source,xml]
----
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-rest-openapi</artifactId>
<version>x.x.x</version>
<!-- use the same version as your Camel core version -->
</dependency>
----
== URI format
----
rest-openapi:[specificationPath#]operationId
----
Where `operationId` is the ID of the operation in the OpenApi
specification, and `specificationPath` is the path to the
specification.
If the `specificationPath` is not specified it defaults to
`openapi.json`. The lookup mechanism uses Camels `ResourceHelper` to
load the resource, which means that you can use CLASSPATH resources
(`classpath:my-specification.json`), files
(`file:/some/path.json`), the web
(`\http://api.example.com/openapi.json`) or reference a bean
(`ref:nameOfBean`) or use a method of a bean
(`bean:nameOfBean.methodName`) to get the specification resource,
failing that OpenApi's own resource loading support.
This component does not act as an HTTP client.
It delegates that to another component mentioned above.
The lookup mechanism searches for a single component that
implements the `RestProducerFactory` interface and
uses that.
If the `_CLASSPATH_` contains more than one, then the property
`componentName` should be set to indicate which component to delegate
to.
Most of the configuration is taken from the OpenApi specification, but
the option exists to override those by specifying them on the component
or on the endpoint. Typically, you would need to override the
`host` or `basePath` if those differ from the specification.
[NOTE]
====
The `host` parameter should contain the absolute URI containing
scheme, hostname and port number, for instance:
`\https://api.example.com`
====
With `componentName` you specify what component is used to perform the
requests, this named component needs to be present in the Camel context
and implement the required _RestProducerFactory_ interface -- as do the
components listed at the top.
If you do not specify the _componentName_ at either component or
endpoint level, a suitable delegate is looked up: a component already
in the Camel context that implements `RestProducerFactory`, then the
registry, then the default components in this order: `http`,
`vertx-http`, `undertow`, `netty-http`. So with both camel-http and
camel-vertx-http on the classpath, camel-http is used unless you name
the other. The Camel CLI (`camel run`) adds camel-http for a
rest-openapi producer.
This component's endpoint URI is lenient which means that in addition
to message headers you can specify REST operation's parameters as
endpoint parameters, these will be constant for all subsequent
invocations, so it makes sense to use this feature only for parameters
that are indeed constant for all invocations -- for example API version
in path such as `/api/\{version}/users/\{id}`.
// component options: START
include::partial$component-configure-options.adoc[]
include::partial$component-endpoint-options.adoc[]
include::partial$component-endpoint-headers.adoc[]
// component options: END
== Usage
=== Contract-first REST DSL
To serve the operations of an OpenAPI contract, use the Rest DSL in _contract-first_ mode rather than a
`rest-openapi` consumer endpoint. Camel loads the contract at startup and routes every operation to the
route that starts from `direct:` and its _operation id_; see
xref:manual::rest-dsl-openapi.adoc[REST DSL with contract-first OpenAPI].
[tabs]
====
Java::
+
[source,java]
----
rest().openApi("petstore-v3.json");
from("direct:getPetById")
.setBody(simple("{\"id\": ${header.petId}, \"name\": \"Rex\"}"));
----
XML::
+
[source,xml]
----
<rest>
<openApi specification="petstore-v3.json"/>
</rest>
<route>
<from uri="direct:getPetById"/>
<setBody>
<simple>{"id": ${header.petId}, "name": "Rex"}</simple>
</setBody>
</route>
----
YAML::
+
[source,yaml]
----
- rest:
openApi:
specification: petstore-v3.json
- route:
from:
uri: direct:getPetById
steps:
- setBody:
simple: '{"id": ${header.petId}, "name": "Rex"}'
----
====
=== Calling an operation of an API
To call an API by its contract, send to a `rest-openapi` endpoint with the contract and the _operation id_. Camel
builds the HTTP request from the contract:
* a path or query parameter, such as `+{petId}+` in `+/pet/{petId}+`, is taken from the message header of the same name;
* the message body is the request body (a JSON string for a JSON API, with a `Content-Type` header);
* the response body becomes the message body, as text: unmarshal it to read its fields.
The server comes from the `servers` of the contract; set `host` to call another one (for example
`http://localhost:8080` while developing). The HTTP call itself is done by an HTTP component on the classpath, such
as `camel-http`; set `componentName` when there is more than one.
[tabs]
====
Java::
+
[source,java]
----
from("timer:pets?repeatCount=1")
.setHeader("petId", constant(10))
.to("rest-openapi:petstore-v3.json#getPetById")
.unmarshal().json()
.log("Pet ${body[name]}")
.setBody(constant("{\"id\": 11, \"name\": \"Rex\"}"))
.setHeader(Exchange.CONTENT_TYPE, constant("application/json"))
.to("rest-openapi:petstore-v3.json#addPet?host={{petstore.url}}")
.log("Added ${body}");
----
XML::
+
[source,xml]
----
<route>
<from uri="timer:pets?repeatCount=1"/>
<setHeader name="petId">
<constant>10</constant>
</setHeader>
<to uri="rest-openapi:petstore-v3.json#getPetById"/>
<unmarshal>
<json/>
</unmarshal>
<log message="Pet ${body[name]}"/>
<setBody>
<constant>{"id": 11, "name": "Rex"}</constant>
</setBody>
<setHeader name="Content-Type">
<constant>application/json</constant>
</setHeader>
<to uri="rest-openapi:petstore-v3.json#addPet?host={{petstore.url}}"/>
<log message="Added ${body}"/>
</route>
----
YAML::
+
[source,yaml]
----
- route:
from:
uri: timer:pets
parameters:
repeatCount: 1
steps:
- setHeader:
name: petId
expression:
constant:
expression: "10"
- to:
uri: rest-openapi
parameters:
specificationUri: petstore-v3.json
operationId: getPetById
- unmarshal:
json: {}
- log:
message: "Pet ${body[name]}"
- setBody:
expression:
constant:
expression: '{"id": 11, "name": "Rex"}'
- setHeader:
name: Content-Type
expression:
constant:
expression: application/json
- to:
uri: rest-openapi
parameters:
specificationUri: petstore-v3.json
operationId: addPet
host: "{{petstore.url}}"
- log:
message: "Added ${body}"
----
====
An answer with an error status (a `404` or a `409`) fails the exchange with an `HttpOperationFailedException`; handle
it with `onException` to read `${exception.statusCode}` and `${exception.responseBody}`.
=== Consumer OAuth Bearer token validation
When `rest-openapi` is used as a consumer, HTTP requests are served by the selected
`RestOpenApiConsumerFactory` delegate, such as the built-in `platform-http` delegate or a custom
delegate.
To validate incoming `Authorization: Bearer` tokens from Rest DSL contract-first routes, pass the
delegate endpoint option `oauthProfile` through the REST configuration endpoint properties.
._Java-only: configuring REST with OAuth profile_
[source,java]
----
restConfiguration()
.component("platform-http")
.endpointProperty("oauthProfile", "myprofile");
rest().openApi("petstore-v3.json");
----
For direct `rest-openapi` consumer routes, pass the delegate endpoint option directly on the
consumer endpoint URI. Consumer URIs identify the OpenAPI specification and do not include an
`#operationId` fragment.
[tabs]
====
Java::
+
[source,java]
----
from("rest-openapi:petstore-v3.json?consumerComponentName=platform-http&oauthProfile=myprofile")
.to("direct:businessLogic");
----
XML::
+
[source,xml]
----
<route>
<from uri="rest-openapi:petstore-v3.json?consumerComponentName=platform-http&amp;oauthProfile=myprofile"/>
<to uri="direct:businessLogic"/>
</route>
----
YAML::
+
[source,yaml]
----
- route:
from:
uri: rest-openapi:petstore-v3.json
parameters:
consumerComponentName: platform-http
oauthProfile: myprofile
steps:
- to:
uri: direct:businessLogic
----
====
The selected delegate component must support the `oauthProfile` endpoint option. The built-in
`platform-http` delegate supports this option. An `OAuthTokenValidationFactory` must be available, for example from
xref:components:others:oauth.adoc[camel-oauth] or from the runtime integration.
OpenAPI `securitySchemes` and operation security requirements are not converted into `oauthProfile`
configuration; select the OAuth profile explicitly with the REST endpoint property or direct endpoint
URI option.
The `oauthProfile` option is a first-class `rest-openapi` endpoint option that is forwarded to the
selected delegate, which is responsible for enforcing it. The route fails at startup when the
resolved `RestOpenApiConsumerFactory` does not declare that its consumers enforce `oauthProfile`,
so a misconfigured delegate cannot start without the expected protection. Custom factories that
enforce the option must override `RestOpenApiConsumerFactory.supportsOAuthProfile()` to return
`true`.
== Request validation
API requests can be validated against the configured OpenAPI specification before they are sent by setting the `requestValidationEnabled` option to `true`.
Validation is provided by the https://bitbucket.org/atlassian/swagger-request-validator/src/master/[swagger-request-validator].
The validator checks for the following conditions:
* request body - Checks if the request body is required and whether there is any body on the Camel Exchange.
* valid json - Checks if the content-type is `application/json` that the message body can be parsed as valid JSon.
* content-type - Validates whether the `Content-Type` header for the request is valid for the API operation.
The value is taken from the `Content-Type` Camel message exchange header.
* request parameters - Validates whether an HTTP header required by the API operation is present. The header is
expected to be present among the Camel message exchange headers.
* query parameters - Validates whether an HTTP query parameter required by the API operation is present. The query parameter is
expected to be present among the Camel message exchange headers.
* path parameters - Validates that every parameter in the path of the API operation, such as `sku` in
`/stock/\{sku}/reserve`, has a value: a Camel message exchange header or an exchange variable of that name. Without
the check, a missing value is sent as the literal `\{sku}` in the path.
If any of the validation checks fail, then a `RestOpenApiValidationException` is thrown. The exception object
has a `getValidationErrors` method that returns the error messages from the validator.
== Unmatched requests
By default, an incoming request that does not match any operation in the OpenAPI specification is answered by the
HTTP layer of the runtime, with HTTP 404, and a request that matches a path but not the HTTP method is answered
with HTTP 405 and an `Allow` header listing the allowed methods.
A request that matches an operation but whose `Content-Type` or `Accept` header does not match the `consumes` or
`produces` of the operation is answered by the HTTP layer, with HTTP 415 or 406 (see the `serverRequestValidation`
option of the platform-http component, enabled by default). When `unmatchedRequestHandling` is set to `camel`,
some runtimes also route these requests to Camel, so the unmatched request handler answers them with the same
415 or 406 status code.
To let Camel answer these requests instead, set the `unmatchedRequestHandling` option to `camel` on the
rest-openapi consumer endpoint or via the rest DSL `openApi` section. The rest-openapi component then registers a
catch-all for the API base path on the HTTP layer so requests that match no operation are routed to Camel, where
they are answered by the unmatched request handler.
[tabs]
====
Java::
+
[source,java]
----
from("rest-openapi:petstore-v3.json?missingOperation=ignore&unmatchedRequestHandling=camel")
.to("direct:businessLogic");
// ... or using the rest DSL
rest().openApi()
.specification("petstore-v3.json")
.missingOperation("ignore")
.unmatchedRequestHandling("camel");
----
YAML::
+
[source,yaml]
----
- route:
from:
uri: rest-openapi:petstore-v3.json
parameters:
missingOperation: ignore
unmatchedRequestHandling: camel
steps:
- to:
uri: direct:businessLogic
# ... or using the rest DSL
- rest:
openApi:
specification: petstore-v3.json
missingOperation: ignore
unmatchedRequestHandling: camel
----
====
The option is supported by the built-in `platform-http` consumer component: Camel Main when using
xref:platform-http-component.adoc[Platform HTTP], and Spring Boot when using the platform-http starter
(`camel-platform-http-starter`). Other consumer components are not tested and can decide to handle, ignore or
reject the parameter.
On Camel Main and Quarkus, the catch-all route is evaluated last on the HTTP server, so it never shadows the
operation routes of other APIs served by the same server, even when their base paths are nested under this API.
[NOTE]
====
With nested base paths (for example `/api` and `/api/v3`), the order of the catch-all follows the order in
which the consumers start: `PUT /api/v3/pet/123` may get a 404 from `/api` instead of 405 with an `Allow`
header from `/api/v3`. To work around this, avoid nesting the base paths of several APIs in `camel`
mode, or inspect the request in a custom `RestUnmatchedRequestHandler` and determine the proper response.
====
On Spring Boot, the catch-all is served by a Spring MVC handler mapping that is evaluated before the
application's own controller mappings and the static resources. With a base path of `/`, in `camel` mode
every request unmatched by the API is answered by the unmatched request handler, so controllers and static
resources are shadowed. When the application also serves MVC content, prefer to use a base path other
than `/` (for example by setting `servers` in the OpenAPI specification or `contextPath` in the rest
configuration), so the catch-all only claims requests under that path.
If the API and Spring MVC controllers must keep responding to their own paths at a base path of `/`, you can
register your own `RequestMappingHandlerMapping` bean with its order set to `-50`. This still does not work for
serving static resources.
When the request has been routed to Camel, the response body (and headers) can be customized by registering a
bean in the xref:manual::registry.adoc[Registry] that implements the `RestUnmatchedRequestHandler` interface.
The handler is called with the exchange, the status code (`404`, `405`, `415` or `406`) and the list of allowed HTTP
methods (empty for `404`, `415` and `406`), and can then set the response body, status code and headers as needed.
The handler can also be registered using a factory finder on the classpath. This is done by adding a
resource file `META-INF/services/org/apache/camel/rest-unmatched-request-handler-factory`
with the content `class=com.example.MyHandler`.
A single handler bean in the registry takes precedence over a handler found via the factory finder, which in
turn takes precedence over the default handler. Regardless of how the handler is registered *only one* handler
is used: when two or more beans of this type are found in the registry, the handler from the factory finder is
used (if present), otherwise the default handler is used.
[source,java]
----
public class JsonErrorHandler implements RestUnmatchedRequestHandler {
@Override
public void handle(Exchange exchange, int statusCode, List<String> allowedMethods) {
String message = switch (statusCode) {
case 404 -> "The requested resource was not found.";
case 405 -> "The HTTP method is not allowed for this resource.";
case 415 -> "The Content-Type of the request is not supported by this resource.";
case 406 -> "No representation matching the Accept header can be produced for this resource.";
default -> "The request cannot be processed.";
};
exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, statusCode);
exchange.getMessage().setHeader(Exchange.CONTENT_TYPE, "application/json");
if (!allowedMethods.isEmpty()) {
exchange.getMessage().setHeader("Allow", String.join(", ", allowedMethods));
}
exchange.getMessage().setBody(String.format(
"{\"message\":\"%s\"}", message));
}
}
----
== Examples
=== PetStore
Checkout the `rest-openapi-simple` example project in
the https://github.com/apache/camel-spring-boot-examples[camel-spring-boot-examples] repository.
For example, if you wanted to use the
https://petstore3.swagger.io/api/v3/[_PetStore_] provided REST API simply
reference the specification URI and desired operation id from the
OpenApi specification or download the specification and store it as
`openapi.json` (in the root) of `_CLASSPATH_` that way it will be
automatically used. Let's use the xref:http-component.adoc[HTTP]
component to perform all the requests and Camel's excellent support for
Spring Boot.
Here are our dependencies defined in Maven POM file:
.Example pom.xml
[source,xml]
----
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-http-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-rest-openapi-starter</artifactId>
</dependency>
----
Start by defining a `RestOpenApiComponent` bean:
._Java-only: defining a RestOpenApiComponent bean for PetStore_
[source,java]
----
@Bean
public Component petstore(CamelContext camelContext) {
RestOpenApiComponent petstore = new RestOpenApiComponent(camelContext);
petstore.setSpecificationUri("https://petstore3.swagger.io/api/v3/openapi.json");
petstore.setHost("https://petstore3.swagger.io");
return petstore;
}
----
[NOTE]
====
Support in Camel for Spring Boot will auto create the
`HttpComponent` Spring bean, and you can configure it using
`application.properties` (or `application.yml`) using prefix
`camel.component.http.`. We are defining the `petstore`
component here to have a named component in the Camel context
that we can use to interact with the PetStore REST API, if this is the
only `rest-openapi` component used we might configure it in the same
manner (using `application.properties`).
In this example, there is no need to explicitly associate the `petstore`
component with the `HttpComponent` as Camel will use the first class on
the `_CLASSPATH_` that implements `RestProducerFactory`. However, if a different
component is required, then calling `petstore.setComponentName("http")`
would use the named component from the Camel registry.
====
Now in our application we can simply use the `ProducerTemplate` to
invoke PetStore REST methods:
._Java-only: invoking PetStore REST methods with ProducerTemplate_
[source,java]
----
@Autowired
ProducerTemplate template;
String getPetJsonById(int petId) {
return template.requestBodyAndHeader("petstore:getPetById", null, "petId", petId);
}
----