/** * 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.swagger; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.stream.Collectors; import static java.util.Optional.ofNullable; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.models.HttpMethod; import io.swagger.models.Operation; import io.swagger.models.Path; import io.swagger.models.Scheme; import io.swagger.models.Swagger; import io.swagger.models.parameters.Parameter; import io.swagger.parser.SwaggerParser; import io.swagger.util.Json; import org.apache.camel.CamelContext; import org.apache.camel.Consumer; import org.apache.camel.Endpoint; import org.apache.camel.ExchangePattern; import org.apache.camel.Processor; import org.apache.camel.Producer; import org.apache.camel.impl.DefaultEndpoint; import org.apache.camel.spi.Metadata; import org.apache.camel.spi.RestConfiguration; import org.apache.camel.spi.UriEndpoint; import org.apache.camel.spi.UriParam; import org.apache.camel.spi.UriPath; import org.apache.camel.util.ObjectHelper; import org.apache.camel.util.ResourceHelper; import org.apache.camel.util.StringHelper; import static org.apache.camel.component.rest.swagger.RestSwaggerHelper.isHostParam; import static org.apache.camel.component.rest.swagger.RestSwaggerHelper.isMediaRange; import static org.apache.camel.util.ObjectHelper.isNotEmpty; import static org.apache.camel.util.ObjectHelper.notNull; import static org.apache.camel.util.StringHelper.after; import static org.apache.camel.util.StringHelper.before; import static org.apache.camel.util.StringHelper.notEmpty; /** * An awesome REST endpoint backed by Swagger specifications. */ @UriEndpoint(firstVersion = "2.19.0", scheme = "rest-swagger", title = "REST Swagger", syntax = "rest-swagger:specificationUri#operationId", label = "rest,swagger,http", producerOnly = true) public final class RestSwaggerEndpoint extends DefaultEndpoint { /** The name of the Camel component, be it `rest-swagger` or `petstore` */ private String assignedComponentName; @UriParam( description = "API basePath, for example \"`/v2`\". Default is unset, if set overrides the value present in" + " Swagger specification and in the component configuration.", defaultValue = "", label = "producer") @Metadata(required = "false") private String basePath; @UriParam(description = "Name of the Camel component that will perform the requests. The compnent 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") @Metadata(required = "false") private String componentName; @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" + " to the value of `Accept` HTTP header. If set overrides any value found in the Swagger specification and." + " in the component configuration", label = "producer") private String consumes; @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 correspoding" + " REST configuration in the Camel Context. If you give this component a name (e.g. `petstore`) that" + " REST configuration is consulted first, `rest-swagger` next, and global configuration last. If set" + " overrides any value found in the Swagger specification, RestConfiguration. Overrides all other " + " configuration.", label = "producer") private String host; @UriPath(description = "ID of the operation from the Swagger specification.", label = "producer") @Metadata(required = "true") private String operationId; @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 Swagger specification. Overrides all other configuration.", label = "producer") private String produces; @UriPath( description = "Path to the Swagger specification file. The scheme, host base path are taken from this" + " specification, but these can be overriden with properties on the component or endpoint level. If not" + " given the component tries to load `swagger.json` resource. 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. `https://api.example.com:8080`). Overrides component configuration.", defaultValue = RestSwaggerComponent.DEFAULT_SPECIFICATION_URI_STR, defaultValueNote = "By default loads `swagger.json` file", label = "producer") private URI specificationUri = RestSwaggerComponent.DEFAULT_SPECIFICATION_URI; public RestSwaggerEndpoint() { // help tooling instantiate endpoint } public RestSwaggerEndpoint(final String uri, final String remaining, final RestSwaggerComponent component) { super(notEmpty(uri, "uri"), notNull(component, "component")); assignedComponentName = before(uri, ":"); final URI componentSpecificationUri = component.getSpecificationUri(); specificationUri = before(remaining, "#", StringHelper::trimToNull).map(URI::create) .orElse(ofNullable(componentSpecificationUri).orElse(RestSwaggerComponent.DEFAULT_SPECIFICATION_URI)); operationId = ofNullable(after(remaining, "#")).orElse(remaining); setExchangePattern(ExchangePattern.InOut); } @Override public Consumer createConsumer(final Processor processor) throws Exception { throw new UnsupportedOperationException("Consumer not supported"); } @Override public Producer createProducer() throws Exception { final CamelContext camelContext = getCamelContext(); final Swagger swagger = loadSpecificationFrom(camelContext, specificationUri); final Map<String, Path> paths = swagger.getPaths(); for (final Entry<String, Path> pathEntry : paths.entrySet()) { final Path path = pathEntry.getValue(); final Optional<Entry<HttpMethod, Operation>> maybeOperationEntry = path.getOperationMap().entrySet() .stream().filter(operationEntry -> operationId.equals(operationEntry.getValue().getOperationId())) .findAny(); if (maybeOperationEntry.isPresent()) { final Entry<HttpMethod, Operation> operationEntry = maybeOperationEntry.get(); final String uriTemplate = pathEntry.getKey(); final HttpMethod httpMethod = operationEntry.getKey(); final String method = httpMethod.name(); final Operation operation = operationEntry.getValue(); return createProducerFor(swagger, operation, method, uriTemplate); } } final String supportedOperations = paths.values().stream().flatMap(p -> p.getOperations().stream()) .map(Operation::getOperationId).collect(Collectors.joining(", ")); throw new IllegalArgumentException("The specified operation with ID: `" + operationId + "` cannot be found in the Swagger specification loaded from `" + specificationUri + "`. Operations defined in the specification are: " + supportedOperations); } public String getBasePath() { return basePath; } public String getComponentName() { return componentName; } public String getConsumes() { return consumes; } public String getHost() { return host; } public String getOperationId() { return operationId; } public String getProduces() { return produces; } public URI getSpecificationUri() { return specificationUri; } @Override public boolean isSingleton() { return true; } public void setBasePath(final String basePath) { this.basePath = notEmpty(basePath, "basePath"); } public void setComponentName(final String componentName) { this.componentName = notEmpty(componentName, "componentName"); } public void setConsumes(final String consumes) { this.consumes = isMediaRange(consumes, "consumes"); } public void setHost(final String host) { this.host = isHostParam(host); } public void setOperationId(final String operationId) { this.operationId = notEmpty(operationId, "operationId"); } public void setProduces(final String produces) { this.produces = isMediaRange(produces, "produces"); } public void setSpecificationUri(final URI specificationUri) { this.specificationUri = notNull(specificationUri, "specificationUri"); } RestSwaggerComponent component() { return (RestSwaggerComponent) getComponent(); } Producer createProducerFor(final Swagger swagger, final Operation operation, final String method, final String uriTemplate) throws Exception { final String basePath = determineBasePath(swagger); final StringBuilder componentEndpointUri = new StringBuilder(200).append("rest:").append(method).append(":") .append(basePath).append(":").append(uriTemplate); final CamelContext camelContext = getCamelContext(); final Endpoint endpoint = camelContext.getEndpoint(componentEndpointUri.toString()); setProperties(endpoint, determineEndpointParameters(swagger, operation)); return endpoint.createProducer(); } String determineBasePath(final Swagger swagger) { if (isNotEmpty(basePath)) { return basePath; } final String componentBasePath = component().getBasePath(); if (isNotEmpty(componentBasePath)) { return componentBasePath; } final String specificationBasePath = swagger.getBasePath(); if (isNotEmpty(specificationBasePath)) { return specificationBasePath; } final CamelContext camelContext = getCamelContext(); final RestConfiguration specificConfiguration = camelContext.getRestConfiguration(assignedComponentName, false); if (specificConfiguration != null && isNotEmpty(specificConfiguration.getContextPath())) { return specificConfiguration.getContextPath(); } final RestConfiguration restConfiguration = camelContext.getRestConfiguration("rest-swagger", true); final String restConfigurationBasePath = restConfiguration.getContextPath(); if (isNotEmpty(restConfigurationBasePath)) { return restConfigurationBasePath; } return RestSwaggerComponent.DEFAULT_BASE_PATH; } String determineComponentName() { return Optional.ofNullable(componentName).orElse(component().getComponentName()); } Map<String, Object> determineEndpointParameters(final Swagger swagger, final Operation operation) { final Map<String, Object> parameters = new HashMap<>(); final String componentName = determineComponentName(); if (componentName != null) { parameters.put("componentName", componentName); } final String host = determineHost(swagger); if (host != null) { parameters.put("host", host); } // what we consume is what the API defined by Swagger specification // produces final String determinedConsumes = determineOption(swagger.getProduces(), operation.getProduces(), component().getConsumes(), consumes); if (isNotEmpty(determinedConsumes)) { parameters.put("consumes", determinedConsumes); } // what we produce is what the API defined by Swagger specification // consumes final String determinedProducers = determineOption(swagger.getConsumes(), operation.getConsumes(), component().getProduces(), produces); if (isNotEmpty(determinedProducers)) { parameters.put("produces", determinedProducers); } final String queryParameters = operation.getParameters().stream().filter(p -> "query".equals(p.getIn())) .map(RestSwaggerEndpoint::queryParameterExpression).collect(Collectors.joining("&")); if (isNotEmpty(queryParameters)) { parameters.put("queryParameters", queryParameters); } return parameters; } String determineHost(final Swagger swagger) { if (isNotEmpty(host)) { return host; } final String componentHost = component().getHost(); if (isNotEmpty(componentHost)) { return componentHost; } final String swaggerScheme = pickBestScheme(specificationUri.getScheme(), swagger.getSchemes()); final String swaggerHost = swagger.getHost(); if (isNotEmpty(swaggerScheme) && isNotEmpty(swaggerHost)) { return swaggerScheme + "://" + swaggerHost; } final CamelContext camelContext = getCamelContext(); final RestConfiguration specificRestConfiguration = camelContext.getRestConfiguration(assignedComponentName, false); final String specificConfigurationHost = hostFrom(specificRestConfiguration); if (specificConfigurationHost != null) { return specificConfigurationHost; } final RestConfiguration componentRestConfiguration = camelContext.getRestConfiguration("rest-swagger", false); final String componentConfigurationHost = hostFrom(componentRestConfiguration); if (componentConfigurationHost != null) { return componentConfigurationHost; } final RestConfiguration globalRestConfiguration = camelContext.getRestConfiguration(); final String globalConfigurationHost = hostFrom(globalRestConfiguration); if (globalConfigurationHost != null) { return globalConfigurationHost; } final String specificationScheme = specificationUri.getScheme(); if (specificationUri.isAbsolute() && specificationScheme.toLowerCase().startsWith("http")) { try { return new URI(specificationUri.getScheme(), specificationUri.getUserInfo(), specificationUri.getHost(), specificationUri.getPort(), null, null, null).toString(); } catch (final URISyntaxException e) { throw new IllegalStateException("Unable to create a new URI from: " + specificationUri, e); } } final boolean areTheSame = "rest-swagger".equals(assignedComponentName); throw new IllegalStateException("Unable to determine destionation host for requests. The Swagger specification" + " does not specify `scheme` and `host` parameters, the specification URI is not absolute with `http` or" + " `https` scheme, and no RestConfigurations configured with `scheme`, `host` and `port` were found for `" + (areTheSame ? "rest-swagger` component" : assignedComponentName + "` or `rest-swagger` components") + " and there is no global RestConfiguration with those properties"); } static String determineOption(final List<String> specificationLevel, final List<String> operationLevel, final String componentLevel, final String endpointLevel) { if (isNotEmpty(endpointLevel)) { return endpointLevel; } if (isNotEmpty(componentLevel)) { return componentLevel; } if (operationLevel != null && !operationLevel.isEmpty()) { return String.join(", ", operationLevel); } if (specificationLevel != null && !specificationLevel.isEmpty()) { return String.join(", ", specificationLevel); } return null; } static String hostFrom(final RestConfiguration restConfiguration) { if (restConfiguration == null) { return null; } final String scheme = restConfiguration.getScheme(); final String host = restConfiguration.getHost(); final int port = restConfiguration.getPort(); if (scheme == null || host == null) { return null; } final StringBuilder answer = new StringBuilder(scheme).append("://").append(host); if (port > 0 && !("http".equalsIgnoreCase(scheme) && port == 80) && !("https".equalsIgnoreCase(scheme) && port == 443)) { answer.append(':').append(port); } return answer.toString(); } /** * Loads the Swagger definition model from the given path. Tries to resolve * the resource using Camel's resource loading support, if it fails uses * Swagger's resource loading support instead. * * @param uri URI of the specification * @param camelContext context to use * @return the specification * @throws IOException */ static Swagger loadSpecificationFrom(final CamelContext camelContext, final URI uri) throws IOException { final ObjectMapper mapper = Json.mapper(); final SwaggerParser swaggerParser = new SwaggerParser(); final String uriAsString = uri.toString(); try (InputStream stream = ResourceHelper.resolveMandatoryResourceAsInputStream(camelContext, uriAsString)) { final JsonNode node = mapper.readTree(stream); return swaggerParser.read(node); } catch (final IOException e) { // try Swaggers loader final Swagger swagger = swaggerParser.read(uriAsString); if (swagger != null) { return swagger; } throw new IllegalArgumentException("The given Swagger specification could not be loaded from `" + uri + "`. Tried loading using Camel's resource resolution and using Swagger's own resource resolution." + " Swagger tends to swallow exceptions while parsing, try specifying Java system property `debugParser`" + " (e.g. `-DdebugParser=true`), the exception that occured when loading using Camel's resource" + " loader follows", e); } } static String pickBestScheme(final String specificationScheme, final List<Scheme> schemes) { if (schemes != null && !schemes.isEmpty()) { if (schemes.contains(Scheme.HTTPS)) { return "https"; } if (schemes.contains(Scheme.HTTP)) { return "http"; } } if (specificationScheme != null) { return specificationScheme; } // there is no support for WebSocket (Scheme.WS, Scheme.WSS) return null; } static String queryParameterExpression(final Parameter parameter) { final String name = parameter.getName(); if (ObjectHelper.isEmpty(name)) { return ""; } final StringBuilder expression = new StringBuilder(name).append("={").append(name); if (!parameter.getRequired()) { expression.append('?'); } expression.append('}'); return expression.toString(); } }