CAMEL-10164: swagger component for making rest calls with swagger schema validation and facade to actual HTTP client in use
Project: http://git-wip-us.apache.org/repos/asf/camel/repo Commit: http://git-wip-us.apache.org/repos/asf/camel/commit/9408d9f6 Tree: http://git-wip-us.apache.org/repos/asf/camel/tree/9408d9f6 Diff: http://git-wip-us.apache.org/repos/asf/camel/diff/9408d9f6 Branch: refs/heads/master Commit: 9408d9f66ea97aca208615601d3630c17cca2f7d Parents: ec680ad Author: Claus Ibsen <[email protected]> Authored: Thu Aug 25 14:04:38 2016 +0200 Committer: Claus Ibsen <[email protected]> Committed: Fri Aug 26 16:53:31 2016 +0200 ---------------------------------------------------------------------- .../camel/component/rest/RestEndpoint.java | 32 ++- .../camel/component/rest/RestProducer.java | 10 +- .../rest/producer/JettyRestProducerGetTest.java | 6 +- .../swagger/SwaggerRestProducerFactory.java | 209 +++++++++++++++++++ .../services/org/apache/camel/rest/swagger | 18 ++ .../component/DummyRestProducerFactory.java | 3 - .../swagger/component/RestSwaggerGetTest.java | 57 +++++ .../component/RestSwaggerGetUriParamTest.java | 56 +++++ 8 files changed, 376 insertions(+), 15 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/camel/blob/9408d9f6/camel-core/src/main/java/org/apache/camel/component/rest/RestEndpoint.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/component/rest/RestEndpoint.java b/camel-core/src/main/java/org/apache/camel/component/rest/RestEndpoint.java index 00ed667..5df59bd 100644 --- a/camel-core/src/main/java/org/apache/camel/component/rest/RestEndpoint.java +++ b/camel-core/src/main/java/org/apache/camel/component/rest/RestEndpoint.java @@ -22,11 +22,14 @@ import java.util.Set; import org.apache.camel.Component; import org.apache.camel.Consumer; import org.apache.camel.ExchangePattern; +import org.apache.camel.NoFactoryAvailableException; import org.apache.camel.NoSuchBeanException; import org.apache.camel.Processor; import org.apache.camel.Producer; import org.apache.camel.impl.DefaultEndpoint; +import org.apache.camel.spi.FactoryFinder; import org.apache.camel.spi.Metadata; +import org.apache.camel.spi.RestApiProcessorFactory; import org.apache.camel.spi.RestConfiguration; import org.apache.camel.spi.RestConsumerFactory; import org.apache.camel.spi.RestProducerFactory; @@ -35,6 +38,8 @@ import org.apache.camel.spi.UriParam; import org.apache.camel.spi.UriPath; import org.apache.camel.util.HostUtils; import org.apache.camel.util.ObjectHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The rest component is used for either hosting REST services (consumer) or calling external REST services (producer). @@ -42,6 +47,8 @@ import org.apache.camel.util.ObjectHelper; @UriEndpoint(scheme = "rest", title = "REST", syntax = "rest:method:path:uriTemplate", label = "core,rest", lenientProperties = true) public class RestEndpoint extends DefaultEndpoint { + private static final Logger LOG = LoggerFactory.getLogger(RestEndpoint.class); + public static final String DEFAULT_API_COMPONENT_NAME = "swagger"; public static final String RESOURCE_PATH = "META-INF/services/org/apache/camel/rest/"; @@ -246,8 +253,25 @@ public class RestEndpoint extends DefaultEndpoint { @Override public Producer createProducer() throws Exception { RestProducerFactory factory = null; + + if (apiDoc != null) { + LOG.debug("Discovering camel-swagger-java on classpath for using api-doc: {}", apiDoc); + // lookup on classpath using factory finder to automatic find it (just add camel-swagger-java to classpath etc) + try { + FactoryFinder finder = getCamelContext().getFactoryFinder(RESOURCE_PATH); + Object instance = finder.newInstance(DEFAULT_API_COMPONENT_NAME); + if (instance instanceof RestProducerFactory) { + // this factory from camel-swagger-java will facade the http component in use + factory = (RestProducerFactory) instance; + } + parameters.put("apiDoc", apiDoc); + } catch (NoFactoryAvailableException e) { + throw new IllegalStateException("Cannot find camel-swagger-java on classpath to use with api-doc: " + apiDoc); + } + } + String cname = null; - if (getComponentName() != null) { + if (factory == null && getComponentName() != null) { Object comp = getCamelContext().getRegistry().lookupByName(getComponentName()); if (comp != null && comp instanceof RestProducerFactory) { factory = (RestProducerFactory) comp; @@ -280,6 +304,8 @@ public class RestEndpoint extends DefaultEndpoint { } } + parameters.put("componentName", cname); + // lookup in registry if (factory == null) { Set<RestProducerFactory> factories = getCamelContext().getRegistry().findByType(RestProducerFactory.class); @@ -288,10 +314,8 @@ public class RestEndpoint extends DefaultEndpoint { } } - // TODO: if api-doc is enabled then lookup that swagger factory and use it to create the producer using the factory - // so we can lookup the operation and find its consumes/producers/and other details - if (factory != null) { + LOG.debug("Using RestProducerFactory: {}", factory); String uriTemplate = path; Producer producer = factory.createProducer(getCamelContext(), host, method, path, uriTemplate, consumes, produces, parameters); return new RestProducer(this, producer); http://git-wip-us.apache.org/repos/asf/camel/blob/9408d9f6/camel-core/src/main/java/org/apache/camel/component/rest/RestProducer.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/component/rest/RestProducer.java b/camel-core/src/main/java/org/apache/camel/component/rest/RestProducer.java index 6b11645..154f280 100644 --- a/camel-core/src/main/java/org/apache/camel/component/rest/RestProducer.java +++ b/camel-core/src/main/java/org/apache/camel/component/rest/RestProducer.java @@ -16,6 +16,7 @@ */ package org.apache.camel.component.rest; +import java.net.URLDecoder; import java.util.Map; import org.apache.camel.AsyncCallback; @@ -81,9 +82,8 @@ public class RestProducer extends DefaultAsyncProducer { if (value != null) { hasPath = true; // we need to remove the header as they are sent as path instead - // TODO: we could use a header filter strategy to skip these headers exchange.getIn().removeHeader(key); - csb.append(key + "=" + value); + csb.append(value); } else { csb.append(a); } @@ -102,11 +102,15 @@ public class RestProducer extends DefaultAsyncProducer { Object v = entry.getValue(); if (v != null) { String a = v.toString(); + // decode the key as { may be decoded to %NN + a = URLDecoder.decode(a, "UTF-8"); if (a.startsWith("{") && a.endsWith("}")) { String key = a.substring(1, a.length() - 1); String value = exchange.getIn().getHeader(key, String.class); if (value != null) { - params.put(entry.getKey(), value); + // we need to remove the header as they are sent in query parameter instead + exchange.getIn().removeHeader(key); + params.put(key, value); } else { params.put(entry.getKey(), entry.getValue()); } http://git-wip-us.apache.org/repos/asf/camel/blob/9408d9f6/components/camel-jetty9/src/test/java/org/apache/camel/component/jetty/rest/producer/JettyRestProducerGetTest.java ---------------------------------------------------------------------- diff --git a/components/camel-jetty9/src/test/java/org/apache/camel/component/jetty/rest/producer/JettyRestProducerGetTest.java b/components/camel-jetty9/src/test/java/org/apache/camel/component/jetty/rest/producer/JettyRestProducerGetTest.java index c20f62c..1798084 100644 --- a/components/camel-jetty9/src/test/java/org/apache/camel/component/jetty/rest/producer/JettyRestProducerGetTest.java +++ b/components/camel-jetty9/src/test/java/org/apache/camel/component/jetty/rest/producer/JettyRestProducerGetTest.java @@ -19,7 +19,6 @@ package org.apache.camel.component.jetty.rest.producer; import org.apache.camel.RoutesBuilder; import org.apache.camel.builder.RouteBuilder; import org.apache.camel.component.jetty.BaseJettyTest; -import org.apache.camel.component.rest.RestComponent; import org.junit.Test; public class JettyRestProducerGetTest extends BaseJettyTest { @@ -40,10 +39,7 @@ public class JettyRestProducerGetTest extends BaseJettyTest { public void configure() throws Exception { String host = "http://localhost:" + getPort(); - RestComponent sc = new RestComponent(); - sc.setComponentName("jetty"); - sc.setHost(host); - context.addComponent("rest", sc); + restConfiguration().component("jetty").host(host); from("direct:start") .to("rest:get:api:hello/hi/{name}") http://git-wip-us.apache.org/repos/asf/camel/blob/9408d9f6/components/camel-swagger-java/src/main/java/org/apache/camel/swagger/SwaggerRestProducerFactory.java ---------------------------------------------------------------------- diff --git a/components/camel-swagger-java/src/main/java/org/apache/camel/swagger/SwaggerRestProducerFactory.java b/components/camel-swagger-java/src/main/java/org/apache/camel/swagger/SwaggerRestProducerFactory.java new file mode 100644 index 0000000..3a74778 --- /dev/null +++ b/components/camel-swagger-java/src/main/java/org/apache/camel/swagger/SwaggerRestProducerFactory.java @@ -0,0 +1,209 @@ +/** + * 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 + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * 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.swagger; + +import java.io.InputStream; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.swagger.models.Operation; +import io.swagger.models.Path; +import io.swagger.models.Swagger; +import io.swagger.parser.SwaggerParser; +import org.apache.camel.CamelContext; +import org.apache.camel.Component; +import org.apache.camel.NoSuchBeanException; +import org.apache.camel.Producer; +import org.apache.camel.spi.RestProducerFactory; +import org.apache.camel.util.CollectionStringBuffer; +import org.apache.camel.util.IOHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.apache.camel.util.ResourceHelper.resolveMandatoryResourceAsInputStream; + +public class SwaggerRestProducerFactory implements RestProducerFactory { + + private static final Logger LOG = LoggerFactory.getLogger(SwaggerRestProducerFactory.class); + + @Override + public Producer createProducer(CamelContext camelContext, String host, + String verb, String basePath, String uriTemplate, + String consumes, String produces, Map<String, Object> parameters) throws Exception { + + String apiDoc = (String) parameters.get("apiDoc"); + // load json model + if (apiDoc == null) { + throw new IllegalArgumentException("Swagger api-doc must be configured using the apiDoc option"); + } + + String path = uriTemplate != null ? uriTemplate : basePath; + // path must start with a leading slash + if (!path.startsWith("/")) { + path = "/" + path; + } + + Swagger swagger = loadSwaggerModel(camelContext, apiDoc); + Operation operation = getSwaggerOperation(swagger, verb, path); + if (operation == null) { + throw new IllegalArgumentException("Swagger api-doc does not contain operation for " + verb + ":" + path); + } + + String componentName = (String) parameters.get("componentName"); + + Producer producer = createHttpProducer(camelContext, swagger, operation, host, verb, path, produces, consumes, componentName, parameters); + return producer; + } + + private Swagger loadSwaggerModel(CamelContext camelContext, String apiDoc) throws Exception { + InputStream is = resolveMandatoryResourceAsInputStream(camelContext, apiDoc); + try { + SwaggerParser parser = new SwaggerParser(); + String json = camelContext.getTypeConverter().mandatoryConvertTo(String.class, is); + LOG.debug("Loaded swagger api-doc:\n{}", json); + return parser.parse(json); + } finally { + IOHelper.close(is); + } + } + + private Operation getSwaggerOperation(Swagger swagger, String verb, String path) { + Path modelPath = swagger.getPath(path); + if (modelPath == null) { + return null; + } + + // get,put,post,head,delete,patch,options + Operation op = null; + if ("get".equals(verb)) { + op = modelPath.getGet(); + } else if ("put".equals(verb)) { + op = modelPath.getPut(); + } else if ("post".equals(verb)) { + op = modelPath.getPost(); + } else if ("head".equals(verb)) { + op = modelPath.getHead(); + } else if ("delete".equals(verb)) { + op = modelPath.getDelete(); + } else if ("patch".equals(verb)) { + op = modelPath.getPatch(); + } else if ("options".equals(verb)) { + op = modelPath.getOptions(); + } + return op; + } + + private Producer createHttpProducer(CamelContext camelContext, Swagger swagger, Operation operation, + String host, String verb, String path, String consumes, String produces, + String componentName, Map<String, Object> parameters) throws Exception { + + LOG.debug("Using Swagger operation: {} with {} {}", operation, verb, path); + + RestProducerFactory factory = null; + String cname = null; + if (componentName != null) { + Object comp = camelContext.getRegistry().lookupByName(componentName); + if (comp != null && comp instanceof RestProducerFactory) { + factory = (RestProducerFactory) comp; + } else { + comp = camelContext.getComponent(componentName); + if (comp != null && comp instanceof RestProducerFactory) { + factory = (RestProducerFactory) comp; + } + } + + if (factory == null) { + if (comp != null) { + throw new IllegalArgumentException("Component " + componentName + " is not a RestProducerFactory"); + } else { + throw new NoSuchBeanException(componentName, RestProducerFactory.class.getName()); + } + } + cname = componentName; + } + + // try all components + if (factory == null) { + for (String name : camelContext.getComponentNames()) { + Component comp = camelContext.getComponent(name); + if (comp != null && comp instanceof RestProducerFactory) { + factory = (RestProducerFactory) comp; + cname = name; + break; + } + } + } + + // lookup in registry + if (factory == null) { + Set<RestProducerFactory> factories = camelContext.getRegistry().findByType(RestProducerFactory.class); + if (factories != null && factories.size() == 1) { + factory = factories.iterator().next(); + } + } + + if (factory != null) { + LOG.debug("Using RestProducerFactory: {}", factory); + + if (produces == null) { + CollectionStringBuffer csb = new CollectionStringBuffer(","); + List<String> list = operation.getProduces(); + if (list == null) { + list = swagger.getProduces(); + } + if (list != null) { + for (String s : list) { + csb.append(s); + } + } + produces = csb.isEmpty() ? null : csb.toString(); + } + if (consumes == null) { + CollectionStringBuffer csb = new CollectionStringBuffer(","); + List<String> list = operation.getConsumes(); + if (list == null) { + list = swagger.getConsumes(); + } + if (list != null) { + for (String s : list) { + csb.append(s); + } + } + consumes = csb.isEmpty() ? null : csb.toString(); + } + + String basePath; + String uriTemplate; + if (host == null) { + // if no explicit host has been configured then use host and base path from the swagger api-doc + host = swagger.getHost(); + basePath = swagger.getBasePath(); + uriTemplate = path; + } else { + // path includes also uri template + basePath = path; + uriTemplate = null; + } + + return factory.createProducer(camelContext, host, verb, basePath, uriTemplate, consumes, produces, parameters); + + } else { + throw new IllegalStateException("Cannot find RestProducerFactory in Registry or as a Component to use"); + } + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/9408d9f6/components/camel-swagger-java/src/main/resources/META-INF/services/org/apache/camel/rest/swagger ---------------------------------------------------------------------- diff --git a/components/camel-swagger-java/src/main/resources/META-INF/services/org/apache/camel/rest/swagger b/components/camel-swagger-java/src/main/resources/META-INF/services/org/apache/camel/rest/swagger new file mode 100644 index 0000000..b9e892f --- /dev/null +++ b/components/camel-swagger-java/src/main/resources/META-INF/services/org/apache/camel/rest/swagger @@ -0,0 +1,18 @@ +# +# 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. +# + +class=org.apache.camel.swagger.SwaggerRestProducerFactory http://git-wip-us.apache.org/repos/asf/camel/blob/9408d9f6/components/camel-swagger-java/src/test/java/org/apache/camel/swagger/component/DummyRestProducerFactory.java ---------------------------------------------------------------------- diff --git a/components/camel-swagger-java/src/test/java/org/apache/camel/swagger/component/DummyRestProducerFactory.java b/components/camel-swagger-java/src/test/java/org/apache/camel/swagger/component/DummyRestProducerFactory.java index 4c27856..6688e6f 100644 --- a/components/camel-swagger-java/src/test/java/org/apache/camel/swagger/component/DummyRestProducerFactory.java +++ b/components/camel-swagger-java/src/test/java/org/apache/camel/swagger/component/DummyRestProducerFactory.java @@ -25,10 +25,7 @@ import org.apache.camel.Producer; import org.apache.camel.impl.DefaultProducer; import org.apache.camel.spi.RestProducerFactory; import org.apache.camel.util.ObjectHelper; -import org.junit.Ignore; -@Deprecated -@Ignore public class DummyRestProducerFactory implements RestProducerFactory { @Override http://git-wip-us.apache.org/repos/asf/camel/blob/9408d9f6/components/camel-swagger-java/src/test/java/org/apache/camel/swagger/component/RestSwaggerGetTest.java ---------------------------------------------------------------------- diff --git a/components/camel-swagger-java/src/test/java/org/apache/camel/swagger/component/RestSwaggerGetTest.java b/components/camel-swagger-java/src/test/java/org/apache/camel/swagger/component/RestSwaggerGetTest.java new file mode 100644 index 0000000..766936d --- /dev/null +++ b/components/camel-swagger-java/src/test/java/org/apache/camel/swagger/component/RestSwaggerGetTest.java @@ -0,0 +1,57 @@ +/** + * 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.swagger.component; + +import org.apache.camel.RoutesBuilder; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.impl.JndiRegistry; +import org.apache.camel.test.junit4.CamelTestSupport; +import org.junit.Ignore; +import org.junit.Test; + +public class RestSwaggerGetTest extends CamelTestSupport { + + @Override + protected JndiRegistry createRegistry() throws Exception { + JndiRegistry jndi = super.createRegistry(); + jndi.bind("dummy", new DummyRestProducerFactory()); + return jndi; + } + + @Test + public void testSwaggerGet() throws Exception { + getMockEndpoint("mock:result").expectedBodiesReceived("Hello Donald Duck"); + + template.sendBodyAndHeader("direct:start", null, "name", "Donald Duck"); + + assertMockEndpointsSatisfied(); + } + + @Override + protected RoutesBuilder createRouteBuilder() throws Exception { + return new RouteBuilder() { + @Override + public void configure() throws Exception { + restConfiguration().setComponent("dummy"); + + from("direct:start") + .to("rest:get:hello/hi/{name}?apiDoc=hello-api.json") + .to("mock:result"); + } + }; + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/9408d9f6/components/camel-swagger-java/src/test/java/org/apache/camel/swagger/component/RestSwaggerGetUriParamTest.java ---------------------------------------------------------------------- diff --git a/components/camel-swagger-java/src/test/java/org/apache/camel/swagger/component/RestSwaggerGetUriParamTest.java b/components/camel-swagger-java/src/test/java/org/apache/camel/swagger/component/RestSwaggerGetUriParamTest.java new file mode 100644 index 0000000..a1db6e9 --- /dev/null +++ b/components/camel-swagger-java/src/test/java/org/apache/camel/swagger/component/RestSwaggerGetUriParamTest.java @@ -0,0 +1,56 @@ +/** + * 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.swagger.component; + +import org.apache.camel.RoutesBuilder; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.impl.JndiRegistry; +import org.apache.camel.test.junit4.CamelTestSupport; +import org.junit.Test; + +public class RestSwaggerGetUriParamTest extends CamelTestSupport { + + @Override + protected JndiRegistry createRegistry() throws Exception { + JndiRegistry jndi = super.createRegistry(); + jndi.bind("dummy", new DummyRestProducerFactory()); + return jndi; + } + + @Test + public void testSwaggerGet() throws Exception { + getMockEndpoint("mock:result").expectedBodiesReceived("Bye Donald+Duck"); + + template.sendBodyAndHeader("direct:start", null, "name", "Donald Duck"); + + assertMockEndpointsSatisfied(); + } + + @Override + protected RoutesBuilder createRouteBuilder() throws Exception { + return new RouteBuilder() { + @Override + public void configure() throws Exception { + restConfiguration().setComponent("dummy"); + + from("direct:start") + .to("rest:get:bye?name={name}&apiDoc=hello-api.json") + .to("mock:result"); + } + }; + } +}
