All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.camel.component.rest.swagger.RestSwaggerEndpoint Maven / Gradle / Ivy

There is a newer version: 3.22.3
Show newest version
/**
 * 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 paths = swagger.getPaths();

        for (final Entry pathEntry : paths.entrySet()) {
            final Path path = pathEntry.getValue();

            final Optional> maybeOperationEntry = path.getOperationMap().entrySet()
                .stream().filter(operationEntry -> operationId.equals(operationEntry.getValue().getOperationId()))
                .findAny();

            if (maybeOperationEntry.isPresent()) {
                final Entry 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 determineEndpointParameters(final Swagger swagger, final Operation operation) {
        final Map 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 specificationLevel, final List 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 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();
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy