org.apache.camel.component.rest.openapi.RestOpenApiEndpoint Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of camel-rest-openapi Show documentation
Show all versions of camel-rest-openapi Show documentation
Camel REST support using OpenApi
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.camel.component.rest.openapi;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.PathItem.HttpMethod;
import io.swagger.v3.oas.models.Paths;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.security.SecurityScheme.In;
import io.swagger.v3.oas.models.servers.Server;
import io.swagger.v3.parser.OpenAPIV3Parser;
import io.swagger.v3.parser.core.models.ParseOptions;
import io.swagger.v3.parser.core.models.SwaggerParseResult;
import org.apache.camel.CamelContext;
import org.apache.camel.CamelContextAware;
import org.apache.camel.Category;
import org.apache.camel.Component;
import org.apache.camel.Consumer;
import org.apache.camel.Endpoint;
import org.apache.camel.ExchangePattern;
import org.apache.camel.NoSuchBeanException;
import org.apache.camel.Processor;
import org.apache.camel.Producer;
import org.apache.camel.component.platform.http.spi.PlatformHttpConsumerAware;
import org.apache.camel.component.rest.openapi.validator.DefaultRequestValidator;
import org.apache.camel.component.rest.openapi.validator.RequestValidator;
import org.apache.camel.component.rest.openapi.validator.RestOpenApiOperation;
import org.apache.camel.spi.Metadata;
import org.apache.camel.spi.Resource;
import org.apache.camel.spi.RestConfiguration;
import org.apache.camel.spi.RestOpenApiConsumerFactory;
import org.apache.camel.spi.UriEndpoint;
import org.apache.camel.spi.UriParam;
import org.apache.camel.spi.UriPath;
import org.apache.camel.support.CamelContextHelper;
import org.apache.camel.support.DefaultEndpoint;
import org.apache.camel.support.ResourceHelper;
import org.apache.camel.util.IOHelper;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.UnsafeUriCharactersEncoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.camel.component.rest.openapi.RestOpenApiHelper.isHostParam;
import static org.apache.camel.util.ObjectHelper.isNotEmpty;
import static org.apache.camel.util.StringHelper.after;
import static org.apache.camel.util.StringHelper.before;
/**
* To call REST services using OpenAPI specification as contract.
*/
@UriEndpoint(firstVersion = "3.1.0", scheme = "rest-openapi", title = "REST OpenApi",
syntax = "rest-openapi:specificationUri#operationId", category = { Category.REST, Category.API })
public final class RestOpenApiEndpoint extends DefaultEndpoint {
private static final Logger LOG = LoggerFactory.getLogger(RestOpenApiEndpoint.class);
public static final String[] DEFAULT_REST_OPENAPI_CONSUMER_COMPONENTS
= new String[] { "platform-http" };
/**
* Remaining parameters specified in the Endpoint URI.
*/
Map parameters = Collections.emptyMap();
@UriParam(description = "API basePath, for example \"`/v3`\". Default is unset, if set overrides the value present in"
+ " OpenApi specification and in the component configuration.",
label = "producer")
private String basePath;
@UriParam(description = "Name of the Camel component that will perform the requests. The component must be present"
+ " in Camel registry and it must implement RestProducerFactory service provider interface. If not set"
+ " CLASSPATH is searched for single component that implements RestProducerFactory SPI. Overrides"
+ " component configuration.",
label = "producer,advanced")
private String componentName;
@UriParam(description = "Name of the Camel component that will service the requests. The component must be present"
+ " in Camel registry and it must implement RestOpenApiConsumerFactory service provider interface. If not set"
+ " CLASSPATH is searched for single component that implements RestOpenApiConsumerFactory SPI. Overrides"
+ " component configuration.",
label = "consumer,advanced")
private String consumerComponentName;
@UriParam(description = "Scheme hostname and port to direct the HTTP requests to in the form of"
+ " `http[s]://hostname[:port]`. Can be configured at the endpoint, component or in the corresponding"
+ " REST configuration in the Camel Context. If you give this component a name (e.g. `petstore`) that"
+ " REST configuration is consulted first, `rest-openapi` next, and global configuration last. If set"
+ " overrides any value found in the OpenApi specification, RestConfiguration. Overrides all other "
+ " configuration.",
label = "producer")
private String host;
@UriPath(description = "ID of the operation from the OpenApi specification. This is required when using producer",
label = "producer")
private String operationId;
@UriParam(description = "What payload type this component capable of consuming. Could be one type, like `application/json`"
+ " or multiple types as `application/json, application/xml; q=0.5` according to the RFC7231. This equates"
+ " or multiple types as `application/json, application/xml; q=0.5` according to the RFC7231. This equates"
+ " to the value of `Accept` HTTP header. If set overrides any value found in the OpenApi specification and."
+ " in the component configuration",
label = "consumer")
private String consumes;
@UriParam(description = "What payload type this component is producing. For example `application/json`"
+ " according to the RFC7231. This equates to the value of `Content-Type` HTTP header. If set overrides"
+ " any value present in the OpenApi specification. Overrides all other configuration.",
label = "producer")
private String produces;
@UriPath(description = "Path to the OpenApi specification file. The scheme, host base path are taken from this"
+ " specification, but these can be overridden with properties on the component or endpoint level. If not"
+ " given the component tries to load `openapi.json` resource from the classpath. Note that the `host` defined on the"
+ " component and endpoint of this Component should contain the scheme, hostname and optionally the"
+ " port in the URI syntax (i.e. `http://api.example.com:8080`). Overrides component configuration."
+ " The OpenApi specification can be loaded from different sources by prefixing with file: classpath: http: https:."
+ " Support for https is limited to using the JDK installed UrlHandler, and as such it can be cumbersome to setup"
+ " TLS/SSL certificates for https (such as setting a number of javax.net.ssl JVM system properties)."
+ " How to do that consult the JDK documentation for UrlHandler.",
defaultValue = RestOpenApiComponent.DEFAULT_SPECIFICATION_URI,
defaultValueNote = "By default loads `openapi.json` file", label = "common")
private String specificationUri;
@Metadata(label = "consumer,advanced",
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.")
private String bindingPackageScan;
@UriParam(label = "consumer",
description = "Whether to enable validation of the client request to check if the incoming request is valid according to the OpenAPI specification")
private boolean clientRequestValidation;
@UriParam(label = "producer", description = "Enable validation of requests against the configured OpenAPI specification")
private boolean requestValidationEnabled;
@UriParam(description = "To use a custom strategy for how to process Rest DSL requests", label = "consumer,advanced")
private RestOpenapiProcessorStrategy restOpenapiProcessorStrategy;
@UriParam(description = "Whether the consumer should fail,ignore or return a mock response for OpenAPI operations that are not mapped to a corresponding route.",
enums = "fail,ignore,mock", label = "consumer", defaultValue = "fail")
private String missingOperation;
@UriParam(description = "Used for inclusive filtering of mock data from directories. The pattern is using Ant-path style pattern."
+ " Multiple patterns can be specified separated by comma.",
label = "consumer,advanced", defaultValue = "classpath:camel-mock/**")
private String mockIncludePattern;
@UriParam(label = "consumer", description = "Sets the context-path to use for servicing the OpenAPI specification")
private String apiContextPath;
public RestOpenApiEndpoint() {
// help tooling instantiate endpoint
}
public RestOpenApiEndpoint(final String uri, final String remaining, final RestOpenApiComponent component,
final Map parameters) {
super(uri, component);
if (remaining.contains("#")) {
operationId = after(remaining, "#");
String spec = before(remaining, "#");
if (spec != null && !spec.isEmpty()) {
specificationUri = spec;
}
} else {
if (remaining.endsWith(".json") || remaining.endsWith(".yaml") || remaining.endsWith(".yml")) {
specificationUri = remaining;
} else {
operationId = remaining;
}
}
if (specificationUri == null) {
specificationUri = component.getSpecificationUri();
}
if (specificationUri == null) {
specificationUri = RestOpenApiComponent.DEFAULT_SPECIFICATION_URI;
}
this.parameters = parameters;
setExchangePattern(ExchangePattern.InOut);
}
@Override
public RestOpenApiComponent getComponent() {
return (RestOpenApiComponent) super.getComponent();
}
@Override
public Consumer createConsumer(final Processor processor) throws Exception {
OpenAPI doc = loadSpecificationFrom(getCamelContext(), specificationUri);
String path = determineBasePath(doc);
RestOpenApiProcessor target
= new RestOpenApiProcessor(this, doc, path, apiContextPath, processor, restOpenapiProcessorStrategy);
CamelContextAware.trySetCamelContext(target, getCamelContext());
Consumer consumer = createConsumerFor(path, target);
target.setConsumer(consumer);
return consumer;
}
protected Consumer createConsumerFor(String basePath, RestOpenApiProcessor processor) throws Exception {
RestOpenApiConsumerFactory factory = null;
String cname = null;
if (getConsumerComponentName() != null) {
Object comp = getCamelContext().getRegistry().lookupByName(getConsumerComponentName());
if (comp instanceof RestOpenApiConsumerFactory) {
factory = (RestOpenApiConsumerFactory) comp;
} else {
comp = getCamelContext().getComponent(getConsumerComponentName());
if (comp instanceof RestOpenApiConsumerFactory) {
factory = (RestOpenApiConsumerFactory) comp;
}
}
if (factory == null) {
if (comp != null) {
throw new IllegalArgumentException(
"Component " + getConsumerComponentName() + " is not a RestOpenApiConsumerFactory");
} else {
throw new NoSuchBeanException(getConsumerComponentName(), RestOpenApiConsumerFactory.class.getName());
}
}
cname = getConsumerComponentName();
}
// try all components
if (factory == null) {
for (String name : getCamelContext().getComponentNames()) {
Component comp = getCamelContext().getComponent(name);
if (comp instanceof RestOpenApiConsumerFactory) {
factory = (RestOpenApiConsumerFactory) comp;
cname = name;
break;
}
}
}
// favour using platform-http if available on classpath
if (factory == null) {
Object comp = getCamelContext().getComponent("platform-http", true);
if (comp instanceof RestOpenApiConsumerFactory) {
factory = (RestOpenApiConsumerFactory) comp;
LOG.debug("Auto discovered platform-http as RestConsumerFactory");
}
}
// lookup in registry
if (factory == null) {
Set factories
= getCamelContext().getRegistry().findByType(RestOpenApiConsumerFactory.class);
if (factories != null && factories.size() == 1) {
factory = factories.iterator().next();
}
}
// no explicit factory found then try to see if we can find any of the default rest consumer components
// and there must only be exactly one so we safely can pick this one
if (factory == null) {
RestOpenApiConsumerFactory found = null;
String foundName = null;
for (String name : DEFAULT_REST_OPENAPI_CONSUMER_COMPONENTS) {
Object comp = getCamelContext().getComponent(name, true);
if (comp instanceof RestOpenApiConsumerFactory) {
if (found == null) {
found = (RestOpenApiConsumerFactory) comp;
foundName = name;
} else {
throw new IllegalArgumentException(
"Multiple RestOpenApiConsumerFactory found on classpath. Configure explicit which component to use");
}
}
}
if (found != null) {
LOG.debug("Auto discovered {} as RestOpenApiConsumerFactory", foundName);
factory = found;
}
}
if (factory != null) {
RestConfiguration config = CamelContextHelper.getRestConfiguration(getCamelContext(), cname);
Map copy = new HashMap<>(parameters); // defensive copy of the parameters
Consumer consumer = factory.createConsumer(getCamelContext(), processor, basePath, config, copy);
if (consumer instanceof PlatformHttpConsumerAware phca) {
processor.setPlatformHttpConsumer(phca);
}
configureConsumer(consumer);
return consumer;
} else {
throw new IllegalStateException("Cannot find RestOpenApiConsumerFactory in Registry or as a Component to use");
}
}
@Override
public Producer createProducer() throws Exception {
final CamelContext camelContext = getCamelContext();
final OpenAPI openapiDoc = loadSpecificationFrom(camelContext, specificationUri);
final Paths paths = openapiDoc.getPaths();
for (final Entry pathEntry : paths.entrySet()) {
final PathItem path = pathEntry.getValue();
Map operationMap = path.readOperationsMap();
final Optional> maybeOperationEntry = operationMap.entrySet()
.stream().filter(operationEntry -> operationId.equals(operationEntry.getValue().getOperationId()))
.findAny();
if (maybeOperationEntry.isPresent()) {
final Entry operationEntry = maybeOperationEntry.get();
final Operation operation = operationEntry.getValue();
Map pathParameters;
if (operation.getParameters() != null) {
pathParameters = operation.getParameters().stream()
.filter(p -> "path".equals(p.getIn()))
.collect(Collectors.toMap(Parameter::getName, Function.identity()));
} else {
pathParameters = new HashMap<>();
}
final String uriTemplate = resolveUri(pathEntry.getKey(), pathParameters);
final HttpMethod httpMethod = operationEntry.getKey();
final String method = httpMethod.name();
return createProducerFor(openapiDoc, operation, method, uriTemplate);
}
}
final String supportedOperations = paths.values().stream().flatMap(p -> p.readOperations().stream())
.map(Operation::getOperationId).collect(Collectors.joining(", "));
throw new IllegalArgumentException(
"The specified operation with ID: `" + operationId
+ "` cannot be found in the OpenApi specification loaded from `" + specificationUri
+ "`. Operations defined in the specification are: " + supportedOperations);
}
public String getBasePath() {
return basePath;
}
public String getComponentName() {
return componentName;
}
public String getConsumerComponentName() {
return consumerComponentName;
}
public String getConsumes() {
return consumes;
}
public String getHost() {
return host;
}
public String getOperationId() {
return operationId;
}
public String getProduces() {
return produces;
}
public String getSpecificationUri() {
return specificationUri;
}
@Override
public boolean isLenientProperties() {
return true;
}
public void setBasePath(final String basePath) {
this.basePath = basePath;
}
public void setComponentName(final String componentName) {
this.componentName = componentName;
}
public void setConsumerComponentName(String consumerComponentName) {
this.consumerComponentName = consumerComponentName;
}
public void setConsumes(final String consumes) {
this.consumes = consumes;
}
public void setHost(final String host) {
this.host = isHostParam(host);
}
public void setOperationId(final String operationId) {
this.operationId = operationId;
}
public void setProduces(final String produces) {
this.produces = produces;
}
public void setSpecificationUri(String specificationUri) {
this.specificationUri = specificationUri;
}
public void setRequestValidationEnabled(boolean requestValidationEnabled) {
this.requestValidationEnabled = requestValidationEnabled;
}
public boolean isRequestValidationEnabled() {
return requestValidationEnabled;
}
public boolean isClientRequestValidation() {
return clientRequestValidation;
}
public void setClientRequestValidation(boolean clientRequestValidation) {
this.clientRequestValidation = clientRequestValidation;
}
public RestOpenapiProcessorStrategy getRestOpenapiProcessorStrategy() {
return restOpenapiProcessorStrategy;
}
public void setRestOpenapiProcessorStrategy(RestOpenapiProcessorStrategy restOpenapiProcessorStrategy) {
this.restOpenapiProcessorStrategy = restOpenapiProcessorStrategy;
}
public String getMissingOperation() {
return missingOperation;
}
public void setMissingOperation(String missingOperation) {
this.missingOperation = missingOperation;
}
public void setMockIncludePattern(String mockIncludePattern) {
this.mockIncludePattern = mockIncludePattern;
}
public String getMockIncludePattern() {
return mockIncludePattern;
}
public String getApiContextPath() {
return apiContextPath;
}
public void setApiContextPath(String apiContextPath) {
this.apiContextPath = apiContextPath;
}
public String getBindingPackageScan() {
return bindingPackageScan;
}
public void setBindingPackageScan(String bindingPackageScan) {
this.bindingPackageScan = bindingPackageScan;
}
Producer createProducerFor(
final OpenAPI openapi, final Operation operation, final String method,
final String uriTemplate)
throws Exception {
CamelContext camelContext = getCamelContext();
Map params = determineEndpointParameters(openapi, operation);
boolean hasHost = params.containsKey("host");
String basePath = determineBasePath(openapi);
String componentEndpointUri = "rest:" + method + ":" + basePath + ":" + uriTemplate;
if (hasHost) {
componentEndpointUri += "?host=" + params.get("host");
}
Endpoint endpoint = camelContext.getEndpoint(componentEndpointUri);
// let the rest endpoint configure itself
endpoint.configureProperties(params);
RequestValidator requestValidator = null;
if (requestValidationEnabled) {
requestValidator = configureRequestValidator(openapi, operation, method, uriTemplate);
}
// if there is a host then we should use this hardcoded host instead of any Header that may have an existing
// Host header from some other HTTP input, and if so then lets remove it
return new RestOpenApiProducer(endpoint.createProducer(), hasHost, requestValidator);
}
String determineBasePath(final OpenAPI openapi) {
if (isNotEmpty(basePath)) {
return basePath;
}
final String componentBasePath = getComponent().getBasePath();
if (isNotEmpty(componentBasePath)) {
return componentBasePath;
}
final String specificationBasePath = RestOpenApiHelper.getBasePathFromOpenApi(openapi);
if (isNotEmpty(specificationBasePath)) {
return specificationBasePath;
}
final CamelContext camelContext = getCamelContext();
final RestConfiguration restConfiguration
= CamelContextHelper.getRestConfiguration(camelContext, null, determineComponentName());
final String restConfigurationBasePath = restConfiguration.getContextPath();
if (isNotEmpty(restConfigurationBasePath)) {
return restConfigurationBasePath;
}
return RestOpenApiComponent.DEFAULT_BASE_PATH;
}
String determineComponentName() {
return Optional.ofNullable(componentName).orElse(getComponent().getComponentName());
}
Map determineEndpointParameters(final OpenAPI openapi, final Operation operation) {
final Map parameters = new HashMap<>();
final String componentName = determineComponentName();
if (componentName != null) {
parameters.put("producerComponentName", componentName);
}
final String host = determineHost(openapi, operation);
if (host != null) {
parameters.put("host", host);
}
final RestOpenApiComponent component = getComponent();
// what we consume is what the API defined by OpenApi specification
// produces
List specificationLevelConsumers = new ArrayList<>();
Set operationLevelConsumers = new java.util.HashSet<>();
if (operation.getResponses() != null) {
for (ApiResponse response : operation.getResponses().values()) {
if (response.getContent() != null) {
operationLevelConsumers.addAll(response.getContent().keySet());
}
}
}
final String determinedConsumes = determineOption(specificationLevelConsumers, operationLevelConsumers,
component.getConsumes(), consumes);
if (isNotEmpty(determinedConsumes)) {
parameters.put("consumes", determinedConsumes);
}
// what we produce is what the API defined by OpenApi specification consumes
List specificationLevelProducers = new ArrayList<>();
Set operationLevelProducers = new java.util.HashSet<>();
if (operation.getRequestBody() != null && operation.getRequestBody().getContent() != null) {
operationLevelProducers.addAll(operation.getRequestBody().getContent().keySet());
}
final String determinedProducers = determineOption(specificationLevelProducers, operationLevelProducers,
component.getProduces(), produces);
if (isNotEmpty(determinedProducers)) {
parameters.put("produces", determinedProducers);
}
final String queryParameters = determineQueryParameters(openapi, operation).map(this::queryParameter)
.collect(Collectors.joining("&"));
if (isNotEmpty(queryParameters)) {
parameters.put("queryParameters", queryParameters);
}
// pass properties that might be applied if the delegate component is
// created, i.e. if it's not
// present in the Camel Context already
final Map componentParameters = new HashMap<>();
if (component.isUseGlobalSslContextParameters()) {
// by default it's false
componentParameters.put("useGlobalSslContextParameters", component.isUseGlobalSslContextParameters());
}
if (component.getSslContextParameters() != null) {
componentParameters.put("sslContextParameters", component.getSslContextParameters());
}
final Map
© 2015 - 2025 Weber Informatics LLC | Privacy Policy