From 45c98dc27d0f2bde7cccd887e3c7c0cfe597589e Mon Sep 17 00:00:00 2001 From: Zoran Regvart Date: Tue, 17 Oct 2017 18:44:30 +0200 Subject: [PATCH] feat: Swagger Connector Adds a Initial implementation of a Connector that uses Swagger specification to invoke REST services. Fixes #96 --- connectors/pom.xml | 23 +- connectors/swagger-connector/pom.xml | 168 +++ .../swagger/DelegateEndpointImpl.java | 167 +++ .../swagger/SwaggerConnectorComponent.java | 107 ++ ...erConnectorConnectorAutoConfiguration.java | 132 +++ ...waggerConnectorConnectorConfiguration.java | 22 + ...ConnectorConnectorConfigurationCommon.java | 52 + .../apache/camel/component/swagger-operation | 1 + .../main/resources/META-INF/spring.factories | 3 + .../resources/camel-connector-schema.json | 57 + .../src/main/resources/camel-connector.json | 31 + .../SwaggerConnectorComponentTest.java | 82 ++ .../src/test/resources/logback-test.xml | 5 + .../src/test/resources/petstore.json | 1035 +++++++++++++++++ .../syndesis/example/HelloRestController.java | 4 - pom.xml | 9 +- 16 files changed, 1876 insertions(+), 22 deletions(-) create mode 100644 connectors/swagger-connector/pom.xml create mode 100644 connectors/swagger-connector/src/main/java/io/syndesis/connector/swagger/DelegateEndpointImpl.java create mode 100644 connectors/swagger-connector/src/main/java/io/syndesis/connector/swagger/SwaggerConnectorComponent.java create mode 100644 connectors/swagger-connector/src/main/java/io/syndesis/connector/swagger/springboot/SwaggerConnectorConnectorAutoConfiguration.java create mode 100644 connectors/swagger-connector/src/main/java/io/syndesis/connector/swagger/springboot/SwaggerConnectorConnectorConfiguration.java create mode 100644 connectors/swagger-connector/src/main/java/io/syndesis/connector/swagger/springboot/SwaggerConnectorConnectorConfigurationCommon.java create mode 100644 connectors/swagger-connector/src/main/resources/META-INF/services/org/apache/camel/component/swagger-operation create mode 100644 connectors/swagger-connector/src/main/resources/META-INF/spring.factories create mode 100644 connectors/swagger-connector/src/main/resources/camel-connector-schema.json create mode 100644 connectors/swagger-connector/src/main/resources/camel-connector.json create mode 100644 connectors/swagger-connector/src/test/java/io/syndesis/connector/swagger/SwaggerConnectorComponentTest.java create mode 100644 connectors/swagger-connector/src/test/resources/logback-test.xml create mode 100644 connectors/swagger-connector/src/test/resources/petstore.json diff --git a/connectors/pom.xml b/connectors/pom.xml index c83fd6c..51e0722 100644 --- a/connectors/pom.xml +++ b/connectors/pom.xml @@ -30,29 +30,30 @@ aws-s3-copy-object-connector + day-trade-get-connector + day-trade-place-connector http-get-connector http-post-connector - timer-connector - twitter-mention-connector - twitter-search-connector - salesforce-model salesforce-create-sobject-connector salesforce-delete-sobject-connector salesforce-delete-sobject-with-id-connector salesforce-get-sobject-connector salesforce-get-sobject-with-id-connector - salesforce-update-sobject-connector - salesforce-upsert-sobject-connector - salesforce-upsert-contact-connector + salesforce-model salesforce-on-create-connector - salesforce-on-update-connector salesforce-on-delete-connector + salesforce-on-update-connector + salesforce-update-sobject-connector + salesforce-upsert-contact-connector + salesforce-upsert-sobject-connector sql-stored-connector - day-trade-get-connector - day-trade-place-connector + swagger-connector + timer-connector trade-insight-buy-connector trade-insight-sell-connector trade-insight-top-connector + twitter-mention-connector + twitter-search-connector @@ -73,4 +74,4 @@ - + \ No newline at end of file diff --git a/connectors/swagger-connector/pom.xml b/connectors/swagger-connector/pom.xml new file mode 100644 index 0000000..c651ced --- /dev/null +++ b/connectors/swagger-connector/pom.xml @@ -0,0 +1,168 @@ + + + + + 4.0.0 + + + io.syndesis + connectors + 0.5-SNAPSHOT + + + swagger-connector + Syndesis Connectors :: Swagger Connector + Swagger Connector + + + + + + org.apache.camel + camel-parent + ${camel.version} + import + pom + + + + + + + + + org.apache.camel + camel-rest-swagger + + + + + org.apache.camel + camel-connector + + + + + org.apache.camel + apt + + + + + org.apache.camel + camel-spring-boot + + + + org.springframework.boot + spring-boot-configuration-processor + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.apache.camel + camel-test + test + + + + org.slf4j + jcl-over-slf4j + test + + + + ch.qos.logback + logback-classic + + + + org.assertj + assertj-core + 3.8.0 + + + + + + install + + + + + org.apache.camel + camel-package-maven-plugin + ${camel.version} + + + prepare + + prepare-components + + generate-resources + + + validate + + validate-components + + prepare-package + + + + + + + org.apache.camel + camel-connector-maven-plugin + ${camel.version} + + + boot + + prepare-spring-boot-auto-configuration + + + + false + + false + + + + connector + + jar + + + + + + + + + + diff --git a/connectors/swagger-connector/src/main/java/io/syndesis/connector/swagger/DelegateEndpointImpl.java b/connectors/swagger-connector/src/main/java/io/syndesis/connector/swagger/DelegateEndpointImpl.java new file mode 100644 index 0000000..de2bed5 --- /dev/null +++ b/connectors/swagger-connector/src/main/java/io/syndesis/connector/swagger/DelegateEndpointImpl.java @@ -0,0 +1,167 @@ +package io.syndesis.connector.swagger; + +import java.util.Map; + +import org.apache.camel.CamelContext; +import org.apache.camel.Consumer; +import org.apache.camel.DelegateEndpoint; +import org.apache.camel.Endpoint; +import org.apache.camel.EndpointConfiguration; +import org.apache.camel.Exchange; +import org.apache.camel.ExchangePattern; +import org.apache.camel.PollingConsumer; +import org.apache.camel.Processor; +import org.apache.camel.Producer; +import org.apache.camel.component.connector.DefaultConnectorEndpoint; + +@SuppressWarnings("deprecation") +final class DelegateEndpointImpl implements DelegateEndpoint { + + private final DefaultConnectorEndpoint endpoint; + + private final Map headers; + + private static final class DelegateProducerImpl implements Producer { + private final Producer delegate; + + private final Map headers; + + private DelegateProducerImpl(final Producer delegate, final Map headers) { + this.delegate = delegate; + this.headers = headers; + } + + @Override + public Exchange createExchange() { + return delegate.createExchange(); + } + + @Override + public Exchange createExchange(final Exchange exchange) { + return delegate.createExchange(exchange); + } + + @Override + public Exchange createExchange(final ExchangePattern pattern) { + return delegate.createExchange(pattern); + } + + @Override + public Endpoint getEndpoint() { + return delegate.getEndpoint(); + } + + @Override + public boolean isSingleton() { + return delegate.isSingleton(); + } + + @Override + public void process(final Exchange exchange) throws Exception { + exchange.getIn().setHeaders(headers); + delegate.process(exchange); + } + + @Override + public void start() throws Exception { + delegate.start(); + } + + @Override + public void stop() throws Exception { + delegate.stop(); + } + } + + DelegateEndpointImpl(final DefaultConnectorEndpoint endpoint, final Map headers) { + this.endpoint = endpoint; + this.headers = headers; + } + + @Override + public void configureProperties(final Map options) { + endpoint.configureProperties(options); + } + + @Override + public Consumer createConsumer(final Processor processor) throws Exception { + return endpoint.createConsumer(processor); + } + + @Override + public Exchange createExchange() { + return endpoint.createExchange(); + } + + @Override + public Exchange createExchange(final Exchange exchange) { + return endpoint.createExchange(exchange); + } + + @Override + public Exchange createExchange(final ExchangePattern pattern) { + return endpoint.createExchange(pattern); + } + + @Override + public PollingConsumer createPollingConsumer() throws Exception { + return endpoint.createPollingConsumer(); + } + + @Override + public Producer createProducer() throws Exception { + final Producer delegate = endpoint.createProducer(); + + return new DelegateProducerImpl(delegate, headers); + } + + @Override + public CamelContext getCamelContext() { + return endpoint.getCamelContext(); + } + + @Override + public Endpoint getEndpoint() { + return endpoint.getEndpoint(); + } + + @Override + public EndpointConfiguration getEndpointConfiguration() { + return endpoint.getEndpointConfiguration(); + } + + @Override + public String getEndpointKey() { + return endpoint.getEndpointKey(); + } + + @Override + public String getEndpointUri() { + return endpoint.getEndpointUri(); + } + + @Override + public boolean isLenientProperties() { + return endpoint.isLenientProperties(); + } + + @Override + public boolean isSingleton() { + return endpoint.isSingleton(); + } + + @Override + public void setCamelContext(final CamelContext context) { + endpoint.setCamelContext(context); + } + + @Override + public void start() throws Exception { + endpoint.start(); + } + + @Override + public void stop() throws Exception { + endpoint.stop(); + } +} \ No newline at end of file diff --git a/connectors/swagger-connector/src/main/java/io/syndesis/connector/swagger/SwaggerConnectorComponent.java b/connectors/swagger-connector/src/main/java/io/syndesis/connector/swagger/SwaggerConnectorComponent.java new file mode 100644 index 0000000..eb1c039 --- /dev/null +++ b/connectors/swagger-connector/src/main/java/io/syndesis/connector/swagger/SwaggerConnectorComponent.java @@ -0,0 +1,107 @@ +/** + * Copyright (C) 2017 Red Hat, Inc. + * + * Licensed 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 io.syndesis.connector.swagger; + +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.camel.Endpoint; +import org.apache.camel.component.connector.DefaultConnectorComponent; +import org.apache.camel.component.connector.DefaultConnectorEndpoint; +import org.apache.camel.component.rest.swagger.RestSwaggerEndpoint; +import org.apache.camel.util.IntrospectionSupport; +import org.apache.camel.util.IntrospectionSupport.ClassInfo; +import org.apache.commons.io.IOUtils; + +public class SwaggerConnectorComponent extends DefaultConnectorComponent { + + private String specification; + + public SwaggerConnectorComponent() { + super("swagger-connector", SwaggerConnectorComponent.class.getName()); + } + + public String getSpecification() { + return specification; + } + + public void setSpecification(final String specification) { + this.specification = specification; + } + + @Override + protected Endpoint createEndpoint(final String uri, final String remaining, final Map parameters) + throws Exception { + final URI baseEndpointUri = URI.create(uri); + + final String scheme = Optional.ofNullable(baseEndpointUri.getScheme()).orElse(baseEndpointUri.getPath()); + + final String swaggerSpecificationPath = scheme + ".swagger"; + + try (final OutputStream out = new FileOutputStream(swaggerSpecificationPath)) { + IOUtils.write(specification, out, StandardCharsets.UTF_8); + } + + final String operationId = Optional.ofNullable((String) parameters.get("operationId")).orElse(remaining); + + final Map headers = determineHeaders(parameters); + + final DefaultConnectorEndpoint endpoint = (DefaultConnectorEndpoint) super.createEndpoint(uri, + "file:" + swaggerSpecificationPath + "#" + operationId, parameters); + + return new DelegateEndpointImpl(endpoint, headers); + } + + @Override + protected void doStart() throws Exception { + super.doStart(); + + // workaround for CAMEL-11986 + getCamelContext().addComponent("http4s", getCamelContext().getComponent("https4")); + } + + /* default */ static Map determineHeaders(final Map parameters) { + final Map headers = new HashMap<>(); + final ClassInfo classInfo = IntrospectionSupport.cacheClass(RestSwaggerEndpoint.class); + + final Set knownParameters = Arrays.stream(classInfo.methods).map(i -> i.getterOrSetterShorthandName) + .filter(Objects::nonNull).collect(Collectors.toSet()); + + for (final Iterator> i = parameters.entrySet().iterator(); i.hasNext();) { + final Entry entry = i.next(); + final String name = entry.getKey(); + + if (!knownParameters.contains(name)) { + headers.put(name, entry.getValue()); + + i.remove(); + } + } + return headers; + } + +} diff --git a/connectors/swagger-connector/src/main/java/io/syndesis/connector/swagger/springboot/SwaggerConnectorConnectorAutoConfiguration.java b/connectors/swagger-connector/src/main/java/io/syndesis/connector/swagger/springboot/SwaggerConnectorConnectorAutoConfiguration.java new file mode 100644 index 0000000..8561e05 --- /dev/null +++ b/connectors/swagger-connector/src/main/java/io/syndesis/connector/swagger/springboot/SwaggerConnectorConnectorAutoConfiguration.java @@ -0,0 +1,132 @@ +package io.syndesis.connector.swagger.springboot; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Generated; +import javax.annotation.PostConstruct; +import io.syndesis.connector.swagger.SwaggerConnectorComponent; +import org.apache.camel.CamelContext; +import org.apache.camel.component.connector.ConnectorCustomizer; +import org.apache.camel.spi.HasId; +import org.apache.camel.spring.boot.util.CamelPropertiesHelper; +import org.apache.camel.spring.boot.util.HierarchicalPropertiesEvaluator; +import org.apache.camel.util.IntrospectionSupport; +import org.apache.camel.util.ObjectHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; + +/** + * Generated by camel-connector-maven-plugin - do not edit this file! + */ +@Generated("org.apache.camel.maven.connector.SpringBootAutoConfigurationMojo") +@Configuration +@ConditionalOnBean(type = "org.apache.camel.spring.boot.CamelAutoConfiguration") +@AutoConfigureAfter(name = "org.apache.camel.spring.boot.CamelAutoConfiguration") +@EnableConfigurationProperties(SwaggerConnectorConnectorConfiguration.class) +public class SwaggerConnectorConnectorAutoConfiguration { + + private static final Logger LOGGER = LoggerFactory + .getLogger(SwaggerConnectorConnectorAutoConfiguration.class); + @Autowired + private ApplicationContext applicationContext; + @Autowired + private CamelContext camelContext; + @Autowired + private SwaggerConnectorConnectorConfiguration configuration; + @Autowired(required = false) + private List> customizers; + + @Lazy + @Bean(name = "swagger-operation-component") + @ConditionalOnClass(CamelContext.class) + @ConditionalOnMissingBean + public SwaggerConnectorComponent configureSwaggerConnectorComponent() + throws Exception { + SwaggerConnectorComponent connector = new SwaggerConnectorComponent(); + connector.setCamelContext(camelContext); + Map parameters = new HashMap<>(); + IntrospectionSupport.getProperties(configuration, parameters, null, + false); + CamelPropertiesHelper.setCamelProperties(camelContext, connector, + parameters, false); + connector.setOptions(parameters); + if (ObjectHelper.isNotEmpty(customizers)) { + for (ConnectorCustomizer customizer : customizers) { + boolean useCustomizer = (customizer instanceof HasId) + ? HierarchicalPropertiesEvaluator.evaluate( + applicationContext.getEnvironment(), + "camel.connector.customizer", + "camel.connector.swagger-operation.customizer", + ((HasId) customizer).getId()) + : HierarchicalPropertiesEvaluator.evaluate( + applicationContext.getEnvironment(), + "camel.connector.customizer", + "camel.connector.swagger-operation.customizer"); + if (useCustomizer) { + LOGGER.debug("Configure connector {}, with customizer {}", + connector, customizer); + customizer.customize(connector); + } + } + } + return connector; + } + + @PostConstruct + public void postConstructSwaggerConnectorComponent() { + Map parameters = new HashMap<>(); + for (Map.Entry entry : configuration + .getConfigurations().entrySet()) { + parameters.clear(); + SwaggerConnectorComponent connector = new SwaggerConnectorComponent(); + connector.setCamelContext(camelContext); + try { + IntrospectionSupport.getProperties(entry.getValue(), + parameters, null, false); + CamelPropertiesHelper.setCamelProperties(camelContext, + connector, parameters, false); + connector.setOptions(parameters); + if (ObjectHelper.isNotEmpty(customizers)) { + for (ConnectorCustomizer customizer : customizers) { + boolean useCustomizer = (customizer instanceof HasId) + ? HierarchicalPropertiesEvaluator.evaluate( + applicationContext.getEnvironment(), + "camel.connector.customizer", + "camel.connector.swagger-operation." + + entry.getKey() + + ".customizer", + ((HasId) customizer).getId()) + : HierarchicalPropertiesEvaluator.evaluate( + applicationContext.getEnvironment(), + "camel.connector.customizer", + "camel.connector.swagger-operation." + + entry.getKey() + + ".customizer"); + if (useCustomizer) { + LOGGER.debug( + "Configure connector {}, with customizer {}", + connector, customizer); + customizer.customize(connector); + } + } + } + camelContext.addComponent(entry.getKey(), connector); + } catch (Exception e) { + throw new BeanCreationException(entry.getKey(), e.getMessage(), + e); + } + } + } +} \ No newline at end of file diff --git a/connectors/swagger-connector/src/main/java/io/syndesis/connector/swagger/springboot/SwaggerConnectorConnectorConfiguration.java b/connectors/swagger-connector/src/main/java/io/syndesis/connector/swagger/springboot/SwaggerConnectorConnectorConfiguration.java new file mode 100644 index 0000000..39a435c --- /dev/null +++ b/connectors/swagger-connector/src/main/java/io/syndesis/connector/swagger/springboot/SwaggerConnectorConnectorConfiguration.java @@ -0,0 +1,22 @@ +package io.syndesis.connector.swagger.springboot; + +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Generated; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Generated("org.apache.camel.maven.connector.SpringBootAutoConfigurationMojo") +@ConfigurationProperties(prefix = "swagger-operation") +public class SwaggerConnectorConnectorConfiguration + extends + SwaggerConnectorConnectorConfigurationCommon { + + /** + * Define additional configuration definitions + */ + private Map configurations = new HashMap<>(); + + public Map getConfigurations() { + return configurations; + } +} \ No newline at end of file diff --git a/connectors/swagger-connector/src/main/java/io/syndesis/connector/swagger/springboot/SwaggerConnectorConnectorConfigurationCommon.java b/connectors/swagger-connector/src/main/java/io/syndesis/connector/swagger/springboot/SwaggerConnectorConnectorConfigurationCommon.java new file mode 100644 index 0000000..e6471d5 --- /dev/null +++ b/connectors/swagger-connector/src/main/java/io/syndesis/connector/swagger/springboot/SwaggerConnectorConnectorConfigurationCommon.java @@ -0,0 +1,52 @@ +package io.syndesis.connector.swagger.springboot; + +import javax.annotation.Generated; + +/** + * REST Swagger Connector + * + * Generated by camel-package-maven-plugin - do not edit this file! + */ +@Generated("org.apache.camel.maven.connector.SpringBootAutoConfigurationMojo") +public class SwaggerConnectorConnectorConfigurationCommon { + + /** + * Scheme hostname and port to direct the HTTP requests to in the form of + * https://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. + * Can be overriden in endpoint configuration. + */ + private String host; + /** + * ID of the operation from the Swagger specification. + */ + private String operationId; + private String specification; + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public String getOperationId() { + return operationId; + } + + public void setOperationId(String operationId) { + this.operationId = operationId; + } + + public String getSpecification() { + return specification; + } + + public void setSpecification(String specification) { + this.specification = specification; + } +} \ No newline at end of file diff --git a/connectors/swagger-connector/src/main/resources/META-INF/services/org/apache/camel/component/swagger-operation b/connectors/swagger-connector/src/main/resources/META-INF/services/org/apache/camel/component/swagger-operation new file mode 100644 index 0000000..ca403c7 --- /dev/null +++ b/connectors/swagger-connector/src/main/resources/META-INF/services/org/apache/camel/component/swagger-operation @@ -0,0 +1 @@ +class=io.syndesis.connector.swagger.SwaggerConnectorComponent diff --git a/connectors/swagger-connector/src/main/resources/META-INF/spring.factories b/connectors/swagger-connector/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..b7b6979 --- /dev/null +++ b/connectors/swagger-connector/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ + +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +io.syndesis.connector.swagger.springboot.SwaggerConnectorConnectorAutoConfiguration diff --git a/connectors/swagger-connector/src/main/resources/camel-connector-schema.json b/connectors/swagger-connector/src/main/resources/camel-connector-schema.json new file mode 100644 index 0000000..c9da9b1 --- /dev/null +++ b/connectors/swagger-connector/src/main/resources/camel-connector-schema.json @@ -0,0 +1,57 @@ +{ + "component":{ + "kind":"component", + "baseScheme":"rest-swagger", + "scheme":"swagger-operation", + "syntax":"swagger-operation:specificationUri#operationId", + "title":"swagger-connector", + "description":"REST Swagger Connector", + "deprecated":false, + "async":false, + "consumerOnly":true, + "lenientProperties":false, + "javaType":"io.syndesis.connector.swagger.SwaggerConnectorComponent", + "groupId":"io.syndesis", + "artifactId":"swagger-connector", + "version":"0.5-SNAPSHOT" + }, + "componentProperties":{ + "host":{ + "kind":"property", + "displayName":"Host", + "group":"producer", + "label":"producer", + "required":false, + "type":"string", + "javaType":"java.lang.String", + "deprecated":false, + "secret":false, + "description":"Scheme hostname and port to direct the HTTP requests to in the form of https:\/\/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. Can be overriden in endpoint configuration." + } + }, + "properties":{ + "operationId":{ + "kind":"path", + "displayName":"Operation Id", + "group":"producer", + "label":"producer", + "required":true, + "type":"string", + "javaType":"java.lang.String", + "deprecated":false, + "secret":false, + "description":"ID of the operation from the Swagger specification." + } + }, + "connectorProperties":{ + "specification":{ + "kind":"property", + "displayName":"Swagger specification", + "required":false, + "type":"string", + "javaType":"java.lang.String", + "deprecated":false, + "secret":false + } + } +} \ No newline at end of file diff --git a/connectors/swagger-connector/src/main/resources/camel-connector.json b/connectors/swagger-connector/src/main/resources/camel-connector.json new file mode 100644 index 0000000..39d4e31 --- /dev/null +++ b/connectors/swagger-connector/src/main/resources/camel-connector.json @@ -0,0 +1,31 @@ +{ + "baseScheme": "rest-swagger", + "baseGroupId": "org.apache.camel", + "baseArtifactId": "camel-rest-swagger", + "baseVersion": "2.20.0", + "baseJavaType" : "org.apache.camel.component.rest.swagger.RestSwaggerComponent", + "name": "swagger-connector", + "scheme": "swagger-operation", + "javaType": "io.syndesis.connector.swagger.SwaggerConnectorComponent", + "groupId": "io.syndesis", + "artifactId": "swagger-connector", + "version": "0.5-SNAPSHOT", + "description": "REST Swagger Connector", + "pattern": "From", + "inputDataType" : "json", + "outputDataType" : "json", + "componentOptions": [ "host" ], + "endpointValues" : { + "consumes" : "application/json" + }, + "endpointOptions": [ "operationId" ], + "connectorProperties": { + "specification": { + "name": "specification", + "displayName": "Swagger specification", + "kind": "property", + "type": "string", + "javaType": "java.lang.String" + } + } +} \ No newline at end of file diff --git a/connectors/swagger-connector/src/test/java/io/syndesis/connector/swagger/SwaggerConnectorComponentTest.java b/connectors/swagger-connector/src/test/java/io/syndesis/connector/swagger/SwaggerConnectorComponentTest.java new file mode 100644 index 0000000..efc6703 --- /dev/null +++ b/connectors/swagger-connector/src/test/java/io/syndesis/connector/swagger/SwaggerConnectorComponentTest.java @@ -0,0 +1,82 @@ +/** + * Copyright (C) 2017 Red Hat, Inc. + * + * Licensed 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 io.syndesis.connector.swagger; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.Optional; + +import org.apache.camel.CamelContext; +import org.apache.camel.Component; +import org.apache.camel.Endpoint; +import org.apache.camel.component.rest.swagger.RestSwaggerEndpoint; +import org.apache.camel.util.IntrospectionSupport; +import org.apache.commons.io.IOUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringRunner.class) +@SpringBootApplication +@SpringBootTest(properties = {"spring.main.banner-mode = off"}) +public class SwaggerConnectorComponentTest { + + @Autowired + private CamelContext camelContext; + + @Test + public void shouldDetermineScheme() { + final Endpoint endpoint = camelContext.getEndpoint("swagger-operation?petId=3"); + + assertThat(endpoint).isNotNull(); + + final Optional maybeRestSwagger = camelContext.getEndpoints().stream() + .filter(RestSwaggerEndpoint.class::isInstance).map(RestSwaggerEndpoint.class::cast).findFirst(); + + assertThat(maybeRestSwagger).hasValueSatisfying(restSwagger -> { + assertThat(restSwagger.getSpecificationUri()).isEqualTo(URI.create("file:swagger-operation.swagger")); + }); + } + + @Test + public void shouldPassSpecificationToRestSwaggerComponent() throws Exception { + final Component component = camelContext.getComponent("swagger-operation"); + assertThat(component).isNotNull(); + + final String specification = IOUtils.toString(SwaggerConnectorComponentTest.class.getResource("/petstore.json"), + StandardCharsets.UTF_8); + IntrospectionSupport.setProperties(component, + new HashMap<>(Collections.singletonMap("specification", specification))); + + final Endpoint endpoint = component.createEndpoint("swagger-operation://?operationId=addPet"); + assertThat(endpoint).isNotNull(); + + final Optional maybeRestSwagger = camelContext.getEndpoints().stream() + .filter(RestSwaggerEndpoint.class::isInstance).map(RestSwaggerEndpoint.class::cast).findFirst(); + + assertThat(maybeRestSwagger).hasValueSatisfying(restSwagger -> { + assertThat(restSwagger.getSpecificationUri()).isNotNull(); + assertThat(restSwagger.getOperationId()).isEqualTo("addPet"); + }); + } +} diff --git a/connectors/swagger-connector/src/test/resources/logback-test.xml b/connectors/swagger-connector/src/test/resources/logback-test.xml new file mode 100644 index 0000000..8017802 --- /dev/null +++ b/connectors/swagger-connector/src/test/resources/logback-test.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/connectors/swagger-connector/src/test/resources/petstore.json b/connectors/swagger-connector/src/test/resources/petstore.json new file mode 100644 index 0000000..65f39d5 --- /dev/null +++ b/connectors/swagger-connector/src/test/resources/petstore.json @@ -0,0 +1,1035 @@ +{ + "swagger": "2.0", + "info": { + "description": "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.", + "version": "1.0.0", + "title": "Swagger Petstore", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "email": "apiteam@swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "host": "petstore.swagger.io", + "basePath": "/v2", + "tags": [ + { + "name": "pet", + "description": "Everything about your Pets", + "externalDocs": { + "description": "Find out more", + "url": "http://swagger.io" + } + }, + { + "name": "store", + "description": "Access to Petstore orders" + }, + { + "name": "user", + "description": "Operations about user", + "externalDocs": { + "description": "Find out more about our store", + "url": "http://swagger.io" + } + } + ], + "schemes": [ + "http" + ], + "paths": { + "/pet": { + "post": { + "tags": [ + "pet" + ], + "summary": "Add a new pet to the store", + "description": "", + "operationId": "addPet", + "consumes": [ + "application/json", + "application/xml" + ], + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "Pet object that needs to be added to the store", + "required": true, + "schema": { + "$ref": "#/definitions/Pet" + } + } + ], + "responses": { + "405": { + "description": "Invalid input" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + }, + "put": { + "tags": [ + "pet" + ], + "summary": "Update an existing pet", + "description": "", + "operationId": "updatePet", + "consumes": [ + "application/json", + "application/xml" + ], + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "Pet object that needs to be added to the store", + "required": true, + "schema": { + "$ref": "#/definitions/Pet" + } + } + ], + "responses": { + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + }, + "405": { + "description": "Validation exception" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/findByStatus": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by status", + "description": "Multiple status values can be provided with comma separated strings", + "operationId": "findPetsByStatus", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Status values that need to be considered for filter", + "required": true, + "type": "array", + "items": { + "type": "string", + "enum": [ + "available", + "pending", + "sold" + ], + "default": "available" + }, + "collectionFormat": "multi" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Pet" + } + } + }, + "400": { + "description": "Invalid status value" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/findByTags": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by tags", + "description": "Muliple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", + "operationId": "findPetsByTags", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "tags", + "in": "query", + "description": "Tags to filter by", + "required": true, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Pet" + } + } + }, + "400": { + "description": "Invalid tag value" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ], + "deprecated": true + } + }, + "/pet/{petId}": { + "get": { + "tags": [ + "pet" + ], + "summary": "Find pet by ID", + "description": "Returns a single pet", + "operationId": "getPetById", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to return", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Pet" + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + } + }, + "security": [ + { + "api_key": [] + } + ] + }, + "post": { + "tags": [ + "pet" + ], + "summary": "Updates a pet in the store with form data", + "description": "", + "operationId": "updatePetWithForm", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet that needs to be updated", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "name", + "in": "formData", + "description": "Updated name of the pet", + "required": false, + "type": "string" + }, + { + "name": "status", + "in": "formData", + "description": "Updated status of the pet", + "required": false, + "type": "string" + } + ], + "responses": { + "405": { + "description": "Invalid input" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + }, + "delete": { + "tags": [ + "pet" + ], + "summary": "Deletes a pet", + "description": "", + "operationId": "deletePet", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "api_key", + "in": "header", + "required": false, + "type": "string" + }, + { + "name": "petId", + "in": "path", + "description": "Pet id to delete", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "responses": { + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/{petId}/uploadImage": { + "post": { + "tags": [ + "pet" + ], + "summary": "uploads an image", + "description": "", + "operationId": "uploadFile", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to update", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "additionalMetadata", + "in": "formData", + "description": "Additional data to pass to server", + "required": false, + "type": "string" + }, + { + "name": "file", + "in": "formData", + "description": "file to upload", + "required": false, + "type": "file" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/ApiResponse" + } + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/store/inventory": { + "get": { + "tags": [ + "store" + ], + "summary": "Returns pet inventories by status", + "description": "Returns a map of status codes to quantities", + "operationId": "getInventory", + "produces": [ + "application/json" + ], + "parameters": [], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + } + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/store/order": { + "post": { + "tags": [ + "store" + ], + "summary": "Place an order for a pet", + "description": "", + "operationId": "placeOrder", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "order placed for purchasing the pet", + "required": true, + "schema": { + "$ref": "#/definitions/Order" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Order" + } + }, + "400": { + "description": "Invalid Order" + } + } + } + }, + "/store/order/{orderId}": { + "get": { + "tags": [ + "store" + ], + "summary": "Find purchase order by ID", + "description": "For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions", + "operationId": "getOrderById", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of pet that needs to be fetched", + "required": true, + "type": "integer", + "maximum": 10.0, + "minimum": 1.0, + "format": "int64" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Order" + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Order not found" + } + } + }, + "delete": { + "tags": [ + "store" + ], + "summary": "Delete purchase order by ID", + "description": "For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors", + "operationId": "deleteOrder", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of the order that needs to be deleted", + "required": true, + "type": "integer", + "minimum": 1.0, + "format": "int64" + } + ], + "responses": { + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Order not found" + } + } + } + }, + "/user": { + "post": { + "tags": [ + "user" + ], + "summary": "Create user", + "description": "This can only be done by the logged in user.", + "operationId": "createUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "Created user object", + "required": true, + "schema": { + "$ref": "#/definitions/User" + } + } + ], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/createWithArray": { + "post": { + "tags": [ + "user" + ], + "summary": "Creates list of users with given input array", + "description": "", + "operationId": "createUsersWithArrayInput", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "List of user object", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/User" + } + } + } + ], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/createWithList": { + "post": { + "tags": [ + "user" + ], + "summary": "Creates list of users with given input array", + "description": "", + "operationId": "createUsersWithListInput", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "List of user object", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/User" + } + } + } + ], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/login": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs user into the system", + "description": "", + "operationId": "loginUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "username", + "in": "query", + "description": "The user name for login", + "required": true, + "type": "string" + }, + { + "name": "password", + "in": "query", + "description": "The password for login in clear text", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "string" + }, + "headers": { + "X-Rate-Limit": { + "type": "integer", + "format": "int32", + "description": "calls per hour allowed by the user" + }, + "X-Expires-After": { + "type": "string", + "format": "date-time", + "description": "date in UTC when token expires" + } + } + }, + "400": { + "description": "Invalid username/password supplied" + } + } + } + }, + "/user/logout": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs out current logged in user session", + "description": "", + "operationId": "logoutUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/{username}": { + "get": { + "tags": [ + "user" + ], + "summary": "Get user by user name", + "description": "", + "operationId": "getUserByName", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be fetched. Use user1 for testing. ", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/User" + } + }, + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + } + } + }, + "put": { + "tags": [ + "user" + ], + "summary": "Updated user", + "description": "This can only be done by the logged in user.", + "operationId": "updateUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "username", + "in": "path", + "description": "name that need to be updated", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "description": "Updated user object", + "required": true, + "schema": { + "$ref": "#/definitions/User" + } + } + ], + "responses": { + "400": { + "description": "Invalid user supplied" + }, + "404": { + "description": "User not found" + } + } + }, + "delete": { + "tags": [ + "user" + ], + "summary": "Delete user", + "description": "This can only be done by the logged in user.", + "operationId": "deleteUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be deleted", + "required": true, + "type": "string" + } + ], + "responses": { + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + } + } + } + } + }, + "securityDefinitions": { + "petstore_auth": { + "type": "oauth2", + "authorizationUrl": "http://petstore.swagger.io/oauth/dialog", + "flow": "implicit", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets" + } + }, + "api_key": { + "type": "apiKey", + "name": "api_key", + "in": "header" + } + }, + "definitions": { + "Order": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "petId": { + "type": "integer", + "format": "int64" + }, + "quantity": { + "type": "integer", + "format": "int32" + }, + "shipDate": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string", + "description": "Order Status", + "enum": [ + "placed", + "approved", + "delivered" + ] + }, + "complete": { + "type": "boolean", + "default": false + } + }, + "xml": { + "name": "Order" + } + }, + "Category": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "Category" + } + }, + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "username": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "userStatus": { + "type": "integer", + "format": "int32", + "description": "User Status" + } + }, + "xml": { + "name": "User" + } + }, + "Tag": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "Tag" + } + }, + "Pet": { + "type": "object", + "required": [ + "name", + "photoUrls" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "category": { + "$ref": "#/definitions/Category" + }, + "name": { + "type": "string", + "example": "doggie" + }, + "photoUrls": { + "type": "array", + "xml": { + "name": "photoUrl", + "wrapped": true + }, + "items": { + "type": "string" + } + }, + "tags": { + "type": "array", + "xml": { + "name": "tag", + "wrapped": true + }, + "items": { + "$ref": "#/definitions/Tag" + } + }, + "status": { + "type": "string", + "description": "pet status in the store", + "enum": [ + "available", + "pending", + "sold" + ] + } + }, + "xml": { + "name": "Pet" + } + }, + "ApiResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string" + }, + "message": { + "type": "string" + } + } + } + }, + "externalDocs": { + "description": "Find out more about Swagger", + "url": "http://swagger.io" + } +} \ No newline at end of file diff --git a/examples/http-pull-push-example/src/main/java/io/syndesis/example/HelloRestController.java b/examples/http-pull-push-example/src/main/java/io/syndesis/example/HelloRestController.java index 79ab83e..eb6b92c 100644 --- a/examples/http-pull-push-example/src/main/java/io/syndesis/example/HelloRestController.java +++ b/examples/http-pull-push-example/src/main/java/io/syndesis/example/HelloRestController.java @@ -15,8 +15,6 @@ */ package io.syndesis.example; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @@ -27,8 +25,6 @@ @RestController public class HelloRestController { - private static final Logger LOG = LoggerFactory.getLogger(HelloRestController.class); - @RequestMapping(value = "/myservice/hello", method = RequestMethod.GET, produces = "application/json") public String hello() { return "{ \"message\": \"Hello World\" }"; diff --git a/pom.xml b/pom.xml index 0b46e58..9bad471 100644 --- a/pom.xml +++ b/pom.xml @@ -68,14 +68,9 @@ - sonatype-nexus-staging - https://oss.sonatype.org/service/local/staging/deploy/maven2 - false + nexus + http://nexus-syndesis.192.168.42.112.nip.io/nexus/content/repositories/snapshots - - sonatype-nexus-snapshots - https://oss.sonatype.org/content/repositories/snapshots -