[BROOKLYN-185] Move ApidocResource(s) to swagger 2.0 spec standard Add SwaggerFilter to web.xml, initializing the appropriate context parameters for new swagger resource scanners.
Use a custom ApiListingResource instead of swagger-provided resource so we can use swagger within a jersey servlet filter context. Project: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/commit/5c625626 Tree: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/tree/5c625626 Diff: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/diff/5c625626 Branch: refs/heads/master Commit: 5c62562640affb17d23eb87048933aa47b8230bd Parents: 55c5904 Author: Ciprian Ciubotariu <[email protected]> Authored: Tue Nov 10 01:09:54 2015 +0200 Committer: Ciprian Ciubotariu <[email protected]> Committed: Wed Nov 11 17:42:43 2015 +0200 ---------------------------------------------------------------------- .../camp/server/rest/CampRestResources.java | 10 +- .../rest/resource/ApidocRestResource.java | 2 +- usage/rest-api/src/main/webapp/WEB-INF/web.xml | 10 +- .../apache/brooklyn/rest/BrooklynRestApi.java | 2 - .../brooklyn/rest/filter/SwaggerFilter.java | 94 ++++++ .../brooklyn/rest/resources/ApidocResource.java | 7 +- .../rest-server/src/main/webapp/WEB-INF/web.xml | 13 +- .../brooklyn/rest/BrooklynRestApiLauncher.java | 3 +- .../rest/resources/ApiDocResourceTest.java | 138 --------- .../rest/resources/ApidocResourceTest.java | 135 +++++++++ .../rest/testing/BrooklynRestApiTest.java | 8 + .../rest/util/NullServletConfigProvider.java | 5 + utils/rest-swagger/pom.xml | 16 +- .../rest/apidoc/ApiListingResource.java | 259 ++++++++++++++++ .../brooklyn/rest/apidoc/ApidocEndpoint.java | 54 ---- .../apidoc/ApidocHelpMessageBodyWriter.java | 28 -- .../brooklyn/rest/apidoc/ApidocResource.java | 294 ------------------- .../apache/brooklyn/rest/apidoc/ApidocRoot.java | 47 --- .../rest/apidoc/RestApiResourceScanner.java | 79 +++++ utils/swagger-annotations/pom.xml | 44 +++ .../brooklyn/swagger/annotations/Apidoc.java | 33 +++ 21 files changed, 697 insertions(+), 584 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5c625626/camp/camp-server/src/main/java/org/apache/brooklyn/camp/server/rest/CampRestResources.java ---------------------------------------------------------------------- diff --git a/camp/camp-server/src/main/java/org/apache/brooklyn/camp/server/rest/CampRestResources.java b/camp/camp-server/src/main/java/org/apache/brooklyn/camp/server/rest/CampRestResources.java index 2d3030c..0c99377 100644 --- a/camp/camp-server/src/main/java/org/apache/brooklyn/camp/server/rest/CampRestResources.java +++ b/camp/camp-server/src/main/java/org/apache/brooklyn/camp/server/rest/CampRestResources.java @@ -30,15 +30,15 @@ import org.apache.brooklyn.camp.server.rest.resource.AssemblyTemplateRestResourc import org.apache.brooklyn.camp.server.rest.resource.PlatformComponentRestResource; import org.apache.brooklyn.camp.server.rest.resource.PlatformComponentTemplateRestResource; import org.apache.brooklyn.camp.server.rest.resource.PlatformRestResource; -import org.apache.brooklyn.rest.apidoc.ApidocHelpMessageBodyWriter; import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider; import com.google.common.collect.Iterables; +import io.swagger.jaxrs.listing.SwaggerSerializers; public class CampRestResources { public static Iterable<AbstractCampRestResource> getCampRestResources() { - List<AbstractCampRestResource> resources = new ArrayList<AbstractCampRestResource>(); + List<AbstractCampRestResource> resources = new ArrayList<>(); resources.add(new PlatformRestResource()); resources.add(new AssemblyTemplateRestResource()); resources.add(new PlatformComponentTemplateRestResource()); @@ -50,14 +50,14 @@ public class CampRestResources { } public static Iterable<Object> getApidocResources() { - List<Object> resources = new ArrayList<Object>(); - resources.add(new ApidocHelpMessageBodyWriter()); + List<Object> resources = new ArrayList<>(); resources.add(new ApidocRestResource()); return resources; } public static Iterable<Object> getMiscResources() { - List<Object> resources = new ArrayList<Object>(); + List<Object> resources = new ArrayList<>(); + resources.add(new SwaggerSerializers()); resources.add(new JacksonJsonProvider()); return resources; } http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5c625626/camp/camp-server/src/main/java/org/apache/brooklyn/camp/server/rest/resource/ApidocRestResource.java ---------------------------------------------------------------------- diff --git a/camp/camp-server/src/main/java/org/apache/brooklyn/camp/server/rest/resource/ApidocRestResource.java b/camp/camp-server/src/main/java/org/apache/brooklyn/camp/server/rest/resource/ApidocRestResource.java index e410f4c..850ae22 100644 --- a/camp/camp-server/src/main/java/org/apache/brooklyn/camp/server/rest/resource/ApidocRestResource.java +++ b/camp/camp-server/src/main/java/org/apache/brooklyn/camp/server/rest/resource/ApidocRestResource.java @@ -24,7 +24,7 @@ import javax.ws.rs.Path; @Path(ApidocRestResource.API_URI_PATH) @Api(value = "org.apache.brooklyn.camp.server.rest.resource.ApidocRestResource", description = "Web API Documentation") -public class ApidocRestResource extends org.apache.brooklyn.rest.apidoc.ApidocResource { +public class ApidocRestResource extends org.apache.brooklyn.rest.apidoc.ApiListingResource { public static final String API_URI_PATH = PlatformRestResource.CAMP_URI_PATH + "/apidoc"; http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5c625626/usage/rest-api/src/main/webapp/WEB-INF/web.xml ---------------------------------------------------------------------- diff --git a/usage/rest-api/src/main/webapp/WEB-INF/web.xml b/usage/rest-api/src/main/webapp/WEB-INF/web.xml index 672785e..06331bd 100644 --- a/usage/rest-api/src/main/webapp/WEB-INF/web.xml +++ b/usage/rest-api/src/main/webapp/WEB-INF/web.xml @@ -59,6 +59,14 @@ <filter-name>Brooklyn HA Master Filter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> + <filter> + <filter-name>Brooklyn Swagger Bootstrap</filter-name> + <filter-class>org.apache.brooklyn.rest.filter.SwaggerFilter</filter-class> + </filter> + <filter-mapping> + <filter-name>Brooklyn Swagger Bootstrap</filter-name> + <url-pattern>/*</url-pattern> + </filter-mapping> <!-- Brooklyn REST is usually run as a filter so static content can be placed in a webapp to which this is added; to run as a servlet directly, replace the filter tags @@ -75,7 +83,7 @@ <!-- load our REST API jersey resources --> <init-param> <param-name>com.sun.jersey.config.property.packages</param-name> - <param-value>org.apache.brooklyn.rest.resources;org.apache.brooklyn.rest.apidoc</param-value> + <param-value>io.swagger.jaxrs.listing;org.codehaus.jackson.jaxrs;org.apache.brooklyn.rest.resources;org.apache.brooklyn.rest.util</param-value> </init-param> <!-- install Jackson and turn on pojo/json serialization (could add org.codehaus.jackson.jaxrs http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5c625626/usage/rest-server/src/main/java/org/apache/brooklyn/rest/BrooklynRestApi.java ---------------------------------------------------------------------- diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/BrooklynRestApi.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/BrooklynRestApi.java index 3973e52..209eb94 100644 --- a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/BrooklynRestApi.java +++ b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/BrooklynRestApi.java @@ -21,7 +21,6 @@ package org.apache.brooklyn.rest; import java.util.ArrayList; import java.util.List; -import org.apache.brooklyn.rest.apidoc.ApidocHelpMessageBodyWriter; import org.apache.brooklyn.rest.resources.AbstractBrooklynRestResource; import org.apache.brooklyn.rest.resources.AccessResource; import org.apache.brooklyn.rest.resources.ActivityResource; @@ -70,7 +69,6 @@ public class BrooklynRestApi { public static Iterable<Object> getApidocResources() { List<Object> resources = new ArrayList<Object>(); - resources.add(new ApidocHelpMessageBodyWriter()); resources.add(new ApidocResource()); return resources; } http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5c625626/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/SwaggerFilter.java ---------------------------------------------------------------------- diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/SwaggerFilter.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/SwaggerFilter.java new file mode 100644 index 0000000..5159b6e --- /dev/null +++ b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/SwaggerFilter.java @@ -0,0 +1,94 @@ +/* + * Copyright 2015 The Apache Software Foundation. + * + * 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 org.apache.brooklyn.rest.filter; + +import io.swagger.config.ScannerFactory; +import io.swagger.models.Info; +import io.swagger.models.License; +import io.swagger.models.Swagger; +import java.io.IOException; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import org.apache.brooklyn.rest.apidoc.RestApiResourceScanner; + +/** + * Bootstraps swagger. + * + * Swagger was intended to run as a servlet. + * + * @author Ciprian Ciubotariu <[email protected]> + */ +public class SwaggerFilter implements Filter { + + static Info info = new Info() + .title("Brooklyn ApiDoc") + .version("TODO") // API version, not BROOKLYN_VERSION + // .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.") + // .termsOfService("http://swagger.io/terms/") + // .contact(new Contact() + // .email("[email protected]")) + .license(new License() + .name("Apache 2.0") + .url("http://www.apache.org/licenses/LICENSE-2.0.html")); + + @Override + public void init(FilterConfig filterConfig) throws ServletException { +// ReflectiveJaxrsScanner scanner = new ReflectiveJaxrsScanner(); +// scanner.setResourcePackage("org.apache.brooklyn.rest.api,org.apache.brooklyn.rest.apidoc,org.apache.brooklyn.rest.resources"); +// ScannerFactory.setScanner(scanner); + ScannerFactory.setScanner(new RestApiResourceScanner()); + + ServletContext context = filterConfig.getServletContext(); + Swagger swagger = new Swagger().info(info); +// swagger.externalDocs(new ExternalDocs("Find out more about Swagger", "http://swagger.io")); +// swagger.securityDefinition("api_key", new ApiKeyAuthDefinition("api_key", In.HEADER)); +// swagger.securityDefinition("petstore_auth", +// new OAuth2Definition() +// .implicit("http://petstore.swagger.io/api/oauth/dialog") +// .scope("read:pets", "read your pets") +// .scope("write:pets", "modify pets in your account")); +// swagger.tag(new Tag() +// .name("pet") +// .description("Everything about your Pets") +// .externalDocs(new ExternalDocs("Find out more", "http://swagger.io"))); +// swagger.tag(new Tag() +// .name("store") +// .description("Access to Petstore orders")); +// swagger.tag(new Tag() +// .name("user") +// .description("Operations about user") +// .externalDocs(new ExternalDocs("Find out more about our store", "http://swagger.io"))); + + context.setAttribute("swagger", swagger); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + chain.doFilter(request, response); + } + + @Override + public void destroy() { + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5c625626/usage/rest-server/src/main/java/org/apache/brooklyn/rest/resources/ApidocResource.java ---------------------------------------------------------------------- diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/resources/ApidocResource.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/resources/ApidocResource.java index e15ce80..e748516 100644 --- a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/resources/ApidocResource.java +++ b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/resources/ApidocResource.java @@ -21,8 +21,11 @@ package org.apache.brooklyn.rest.resources; import io.swagger.annotations.Api; import javax.ws.rs.Path; -@Api(value = "org.apache.brooklyn.rest.resources.ApidocRestResource", description = "API Documentation") +/** + * @author Ciprian Ciubotariu <[email protected]> + */ +@Api(value = "org.apache.brooklyn.rest.resources.ApidocResource", description = "API Documentation") @Path("/v1/apidoc") -public class ApidocResource extends org.apache.brooklyn.rest.apidoc.ApidocResource { +public class ApidocResource extends org.apache.brooklyn.rest.apidoc.ApiListingResource { } http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5c625626/usage/rest-server/src/main/webapp/WEB-INF/web.xml ---------------------------------------------------------------------- diff --git a/usage/rest-server/src/main/webapp/WEB-INF/web.xml b/usage/rest-server/src/main/webapp/WEB-INF/web.xml index 045eaab..1c44b02 100644 --- a/usage/rest-server/src/main/webapp/WEB-INF/web.xml +++ b/usage/rest-server/src/main/webapp/WEB-INF/web.xml @@ -58,7 +58,16 @@ <url-pattern>/*</url-pattern> </filter-mapping> - <!-- Brooklyn REST is usu run as a filter so static content can be placed in a webapp + <filter> + <filter-name>Brooklyn Swagger Bootstrap</filter-name> + <filter-class>org.apache.brooklyn.rest.filter.SwaggerFilter</filter-class> + </filter> + <filter-mapping> + <filter-name>Brooklyn Swagger Bootstrap</filter-name> + <url-pattern>/*</url-pattern> + </filter-mapping> + + <!-- Brooklyn REST is usually run as a filter so static content can be placed in a webapp to which this is added; to run as a servlet directly, replace the filter tags below (after the comment) with the servlet tags (commented out immediately below), (and do the same for the matching tags at the bottom) @@ -80,7 +89,7 @@ <init-param> <param-name>com.sun.jersey.config.property.classnames</param-name> <param-value> - org.apache.brooklyn.rest.apidoc.ApidocHelpMessageBodyWriter; + io.swagger.jaxrs.listing.SwaggerSerializers; org.apache.brooklyn.rest.util.FormMapProvider; org.codehaus.jackson.jaxrs.JacksonJsonProvider; org.apache.brooklyn.rest.resources.ActivityResource; http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5c625626/usage/rest-server/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncher.java ---------------------------------------------------------------------- diff --git a/usage/rest-server/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncher.java b/usage/rest-server/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncher.java index 221d1ad..a2d9113 100644 --- a/usage/rest-server/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncher.java +++ b/usage/rest-server/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncher.java @@ -71,6 +71,7 @@ import com.sun.jersey.api.core.DefaultResourceConfig; import com.sun.jersey.api.core.ResourceConfig; import com.sun.jersey.spi.container.servlet.ServletContainer; import org.eclipse.jetty.server.NetworkConnector; +import org.apache.brooklyn.rest.filter.SwaggerFilter; /** Convenience and demo for launching programmatically. Also used for automated tests. * <p> @@ -308,7 +309,7 @@ public class BrooklynRestApiLauncher { } public static void main(String[] args) throws Exception { - startRestResourcesViaFilter(); + startRestResourcesViaWebXml(); log.info("Press Ctrl-C to quit."); } http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5c625626/usage/rest-server/src/test/java/org/apache/brooklyn/rest/resources/ApiDocResourceTest.java ---------------------------------------------------------------------- diff --git a/usage/rest-server/src/test/java/org/apache/brooklyn/rest/resources/ApiDocResourceTest.java b/usage/rest-server/src/test/java/org/apache/brooklyn/rest/resources/ApiDocResourceTest.java deleted file mode 100644 index e5c1f23..0000000 --- a/usage/rest-server/src/test/java/org/apache/brooklyn/rest/resources/ApiDocResourceTest.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * 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.brooklyn.rest.resources; - -import static org.testng.Assert.assertEquals; - -import java.util.List; -import java.util.Map; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.testng.Assert; -import org.testng.annotations.Test; - -import org.apache.brooklyn.rest.BrooklynRestApi; -import org.apache.brooklyn.rest.apidoc.ApidocRoot; -import org.apache.brooklyn.rest.testing.BrooklynRestResourceTest; - -import com.google.common.collect.Iterables; -import com.google.common.collect.Lists; -import com.wordnik.swagger.core.DocumentationEndPoint; -import com.wordnik.swagger.core.DocumentationOperation; - -/** - * @author Adam Lowe - */ -@Test(singleThreaded = true) -public class ApiDocResourceTest extends BrooklynRestResourceTest { - - private static final Logger log = LoggerFactory.getLogger(ApiDocResourceTest.class); - - @Override - protected void addBrooklynResources() { - for (Object o : BrooklynRestApi.getApidocResources()) { - addResource(o); - } - super.addBrooklynResources(); - } - - @Test - public void testRootSerializesSensibly() throws Exception { - String data = client().resource("/v1/apidoc/").get(String.class); - log.info("apidoc gives: "+data); - // make sure no scala gets in - Assert.assertFalse(data.contains("$")); - Assert.assertFalse(data.contains("scala")); - } - - @Test - public void testCountRestResources() throws Exception { - ApidocRoot response = client().resource("/v1/apidoc/").get(ApidocRoot.class); - assertEquals(response.getApis().size(), 1 + Iterables.size(BrooklynRestApi.getBrooklynRestResources())); - } - - @Test - public void testEndpointSerializesSensibly() throws Exception { - String data = client().resource("/v1/apidoc/org.apache.brooklyn.rest.resources.ApidocResource").get(String.class); - log.info("apidoc endpoint resource gives: "+data); - // make sure no scala gets in - Assert.assertFalse(data.contains("$")); - Assert.assertFalse(data.contains("scala")); - } - - @Test - public void testApiDocDetails() throws Exception { - ApidocRoot response = client().resource("/v1/apidoc/org.apache.brooklyn.rest.resources.ApidocResource").get(ApidocRoot.class); - assertEquals(countOperations(response), 2); - } - - @Test - public void testEffectorDetails() throws Exception { - ApidocRoot response = client().resource("/v1/apidoc/org.apache.brooklyn.rest.resources.EffectorResource").get(ApidocRoot.class); - assertEquals(countOperations(response), 2); - } - - @Test - public void testEntityDetails() throws Exception { - ApidocRoot response = client().resource("/v1/apidoc/org.apache.brooklyn.rest.resources.EntityResource").get(ApidocRoot.class); - assertEquals(countOperations(response), 14); - } - - @Test - public void testCatalogDetails() throws Exception { - ApidocRoot response = client().resource("/v1/apidoc/org.apache.brooklyn.rest.resources.CatalogResource").get(ApidocRoot.class); - assertEquals(countOperations(response), 22, "ops="+getOperations(response)); - } - - @SuppressWarnings("rawtypes") - @Test - public void testAllAreLoadable() throws Exception { - // sometimes -- e.g. if an annotation refers to a class name with the wrong case -- the call returns a 500 and breaks apidoc; ensure we don't trigger that. - Map response = client().resource("/v1/apidoc/").get(Map.class); - // "Documenation" object does not include the links :( so traverse via map - log.debug("root doc response is: "+response); - List apis = (List)response.get("apis"); - for (Object api: apis) { - String link = (String) ((Map)api).get("link"); - try { - Map r2 = client().resource(link).get(Map.class); - log.debug("doc for "+link+" is: "+r2); - } catch (Exception e) { - log.error("Error in swagger/apidoc annotations, unparseable, at "+link+": "+e, e); - Assert.fail("Error in swagger/apidoc annotations, unparseable, at "+link+": "+e, e); - } - } - } - - /* Note in some cases we might have more than one Resource method per 'endpoint' - */ - private int countOperations(ApidocRoot doc) throws Exception { - return getOperations(doc).size(); - } - - private List<DocumentationOperation> getOperations(ApidocRoot doc) throws Exception { - List<DocumentationOperation> result = Lists.newArrayList(); - for (DocumentationEndPoint endpoint : doc.getApis()) { - result.addAll(endpoint.getOperations()); - } - return result; - } -} - http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5c625626/usage/rest-server/src/test/java/org/apache/brooklyn/rest/resources/ApidocResourceTest.java ---------------------------------------------------------------------- diff --git a/usage/rest-server/src/test/java/org/apache/brooklyn/rest/resources/ApidocResourceTest.java b/usage/rest-server/src/test/java/org/apache/brooklyn/rest/resources/ApidocResourceTest.java new file mode 100644 index 0000000..c98117f --- /dev/null +++ b/usage/rest-server/src/test/java/org/apache/brooklyn/rest/resources/ApidocResourceTest.java @@ -0,0 +1,135 @@ +/* + * 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.brooklyn.rest.resources; + +import static org.testng.Assert.assertEquals; + +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.Assert; +import org.testng.annotations.Test; + +import org.apache.brooklyn.rest.BrooklynRestApi; +import org.apache.brooklyn.rest.testing.BrooklynRestResourceTest; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; + +/** + * @author Adam Lowe + */ +@Test(singleThreaded = true) +public class ApidocResourceTest extends BrooklynRestResourceTest { + + private static final Logger log = LoggerFactory.getLogger(ApidocResourceTest.class); + + @Override + protected void addBrooklynResources() { + for (Object o : BrooklynRestApi.getApidocResources()) { + addResource(o); + } + super.addBrooklynResources(); + } + + @Test + public void testRootSerializesSensibly() throws Exception { + String data = client().resource("/v1/apidoc/swagger.json").get(String.class); + log.info("apidoc gives: "+data); + // make sure no scala gets in + Assert.assertFalse(data.contains("$")); + Assert.assertFalse(data.contains("scala")); + } + +// @Test +// public void testCountRestResources() throws Exception { +// ApidocRoot response = client().resource("/v1/apidoc/").get(ApidocRoot.class); +// assertEquals(response.getApis().size(), 1 + Iterables.size(BrooklynRestApi.getBrooklynRestResources())); +// } +// +// @Test +// public void testEndpointSerializesSensibly() throws Exception { +// String data = client().resource("/v1/apidoc/org.apache.brooklyn.rest.resources.ApidocResource").get(String.class); +// log.info("apidoc endpoint resource gives: "+data); +// // make sure no scala gets in +// Assert.assertFalse(data.contains("$")); +// Assert.assertFalse(data.contains("scala")); +// } +// +// @Test +// public void testApiDocDetails() throws Exception { +// ApidocRoot response = client().resource("/v1/apidoc/org.apache.brooklyn.rest.resources.ApidocResource").get(ApidocRoot.class); +// assertEquals(countOperations(response), 2); +// } +// +// @Test +// public void testEffectorDetails() throws Exception { +// ApidocRoot response = client().resource("/v1/apidoc/org.apache.brooklyn.rest.resources.EffectorResource").get(ApidocRoot.class); +// assertEquals(countOperations(response), 2); +// } +// +// @Test +// public void testEntityDetails() throws Exception { +// ApidocRoot response = client().resource("/v1/apidoc/org.apache.brooklyn.rest.resources.EntityResource").get(ApidocRoot.class); +// assertEquals(countOperations(response), 14); +// } +// +// @Test +// public void testCatalogDetails() throws Exception { +// ApidocRoot response = client().resource("/v1/apidoc/org.apache.brooklyn.rest.resources.CatalogResource").get(ApidocRoot.class); +// assertEquals(countOperations(response), 22, "ops="+getOperations(response)); +// } + + @SuppressWarnings("rawtypes") + @Test + public void testAllAreLoadable() throws Exception { + // sometimes -- e.g. if an annotation refers to a class name with the wrong case -- the call returns a 500 and breaks apidoc; ensure we don't trigger that. + Map response = client().resource("/v1/apidoc/swagger.json").get(Map.class); + // "Documenation" object does not include the links :( so traverse via map + log.debug("root doc response is: "+response); + List apis = (List)response.get("apis"); + for (Object api: apis) { + String link = (String) ((Map)api).get("link"); + try { + Map r2 = client().resource(link).get(Map.class); + log.debug("doc for "+link+" is: "+r2); + } catch (Exception e) { + log.error("Error in swagger/apidoc annotations, unparseable, at "+link+": "+e, e); + Assert.fail("Error in swagger/apidoc annotations, unparseable, at "+link+": "+e, e); + } + } + } + +// /* Note in some cases we might have more than one Resource method per 'endpoint' +// */ +// private int countOperations(ApidocRoot doc) throws Exception { +// return getOperations(doc).size(); +// } +// +// private List<DocumentationOperation> getOperations(ApidocRoot doc) throws Exception { +// List<DocumentationOperation> result = Lists.newArrayList(); +// for (DocumentationEndPoint endpoint : doc.getApis()) { +// result.addAll(endpoint.getOperations()); +// } +// return result; +// } +} + http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5c625626/usage/rest-server/src/test/java/org/apache/brooklyn/rest/testing/BrooklynRestApiTest.java ---------------------------------------------------------------------- diff --git a/usage/rest-server/src/test/java/org/apache/brooklyn/rest/testing/BrooklynRestApiTest.java b/usage/rest-server/src/test/java/org/apache/brooklyn/rest/testing/BrooklynRestApiTest.java index 2f50cfc..27115ad 100644 --- a/usage/rest-server/src/test/java/org/apache/brooklyn/rest/testing/BrooklynRestApiTest.java +++ b/usage/rest-server/src/test/java/org/apache/brooklyn/rest/testing/BrooklynRestApiTest.java @@ -36,9 +36,11 @@ import com.sun.jersey.api.core.DefaultResourceConfig; import com.sun.jersey.test.framework.AppDescriptor; import com.sun.jersey.test.framework.JerseyTest; import com.sun.jersey.test.framework.LowLevelAppDescriptor; +import com.sun.jersey.test.framework.WebAppDescriptor; import org.apache.brooklyn.rest.BrooklynRestApi; import org.apache.brooklyn.rest.BrooklynRestApiLauncherTest; +import org.apache.brooklyn.rest.filter.SwaggerFilter; import org.apache.brooklyn.rest.util.BrooklynRestResourceUtils; import org.apache.brooklyn.rest.util.NullHttpServletRequestProvider; import org.apache.brooklyn.rest.util.NullServletConfigProvider; @@ -157,6 +159,12 @@ public abstract class BrooklynRestApiTest { jerseyTest = new JerseyTest() { @Override protected AppDescriptor configure() { +// return new WebAppDescriptor.Builder( +// "io.swagger.jaxrs.listing", +// "org.apache.brooklyn.rest.util", +// "org.codehaus.jackson.jaxrs", +// "org.apache.brooklyn.rest.resources") +// .filterClass(SwaggerFilter.class).build(); return new LowLevelAppDescriptor.Builder(config).build(); } }; http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5c625626/usage/rest-server/src/test/java/org/apache/brooklyn/rest/util/NullServletConfigProvider.java ---------------------------------------------------------------------- diff --git a/usage/rest-server/src/test/java/org/apache/brooklyn/rest/util/NullServletConfigProvider.java b/usage/rest-server/src/test/java/org/apache/brooklyn/rest/util/NullServletConfigProvider.java index 06c60ea..106780d 100644 --- a/usage/rest-server/src/test/java/org/apache/brooklyn/rest/util/NullServletConfigProvider.java +++ b/usage/rest-server/src/test/java/org/apache/brooklyn/rest/util/NullServletConfigProvider.java @@ -26,6 +26,7 @@ import javax.ws.rs.ext.Provider; import com.sun.jersey.core.spi.component.ComponentContext; import com.sun.jersey.core.spi.component.ComponentScope; +import com.sun.jersey.spi.container.servlet.WebConfig; import com.sun.jersey.spi.inject.Injectable; import com.sun.jersey.spi.inject.InjectableProvider; @@ -37,6 +38,10 @@ public class NullServletConfigProvider implements InjectableProvider<Context, Ty return new Injectable<ServletContext>() { public ServletContext getValue() { return null; } }; + } else if (WebConfig.class == c) { + return new Injectable<ServletContext>() { + public ServletContext getValue() { return null; } + }; } else return null; } http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5c625626/utils/rest-swagger/pom.xml ---------------------------------------------------------------------- diff --git a/utils/rest-swagger/pom.xml b/utils/rest-swagger/pom.xml index 42edc4b..42e9767 100644 --- a/utils/rest-swagger/pom.xml +++ b/utils/rest-swagger/pom.xml @@ -40,11 +40,7 @@ <!-- ATTN: this moves jersey-server from 1.7 to 1.12 --> <dependency> <groupId>com.sun.jersey</groupId> - <artifactId>jersey-server</artifactId> - </dependency> - <dependency> - <groupId>com.sun.jersey</groupId> - <artifactId>jersey-core</artifactId> + <artifactId>jersey-servlet</artifactId> </dependency> <dependency> <groupId>com.google.guava</groupId> @@ -83,16 +79,18 @@ <artifactId>swagger-jaxrs</artifactId> <exclusions> <exclusion> - <groupId>javax.servlet</groupId> - <artifactId>servlet-api</artifactId> - </exclusion> - <exclusion> <groupId>com.sun.jersey</groupId> <artifactId>jersey-client</artifactId> </exclusion> </exclusions> </dependency> + <dependency> + <groupId>javax.servlet</groupId> + <artifactId>javax.servlet-api</artifactId> + </dependency> + + </dependencies> </project> http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5c625626/utils/rest-swagger/src/main/java/org/apache/brooklyn/rest/apidoc/ApiListingResource.java ---------------------------------------------------------------------- diff --git a/utils/rest-swagger/src/main/java/org/apache/brooklyn/rest/apidoc/ApiListingResource.java b/utils/rest-swagger/src/main/java/org/apache/brooklyn/rest/apidoc/ApiListingResource.java new file mode 100644 index 0000000..3edffe9 --- /dev/null +++ b/utils/rest-swagger/src/main/java/org/apache/brooklyn/rest/apidoc/ApiListingResource.java @@ -0,0 +1,259 @@ +/* + * Copyright 2015 The Apache Software Foundation. + * + * 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 org.apache.brooklyn.rest.apidoc; + +import com.sun.jersey.spi.container.servlet.WebConfig; +import io.swagger.annotations.ApiOperation; +import io.swagger.config.FilterFactory; +import io.swagger.config.Scanner; +import io.swagger.config.ScannerFactory; +import io.swagger.config.SwaggerConfig; +import io.swagger.core.filter.SpecFilter; +import io.swagger.core.filter.SwaggerSpecFilter; +import io.swagger.jaxrs.Reader; +import io.swagger.jaxrs.config.JaxrsScanner; +import io.swagger.jaxrs.config.ReaderConfigUtils; +import io.swagger.jaxrs.listing.SwaggerSerializers; +import io.swagger.models.Swagger; +import io.swagger.util.Yaml; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Cookie; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ApiListingResource usable within a jersey servlet filter. + * + * Taken from io.swagger:swagger-jaxrs, class + * io.swagger.jaxrs.listing.ApiListingResource, which can only be used within a + * servlet context. We are here using a filter, but jersey has a WebConfig class + * that can substitute ServletConfig and FilterConfig. + * + * @todo Remove when the rest-server is no longer running within a filter (e.g. + * as a standalone OSGi http service) + * + * @author Ciprian Ciubotariu <[email protected]> + */ +public class ApiListingResource { + + static Logger LOGGER = LoggerFactory.getLogger(ApiListingResource.class); + + @Context + ServletContext context; + + boolean initialized = false; + + private static class ServletConfigAdapter implements ServletConfig { + + private final WebConfig webConfig; + + private ServletConfigAdapter(WebConfig webConfig) { + this.webConfig = webConfig; + } + + @Override + public String getServletName() { + return webConfig.getName(); + } + + @Override + public ServletContext getServletContext() { + return webConfig.getServletContext(); + } + + @Override + public String getInitParameter(String name) { + return webConfig.getInitParameter(name); + } + + @Override + public Enumeration<String> getInitParameterNames() { + return webConfig.getInitParameterNames(); + } + + } + + protected synchronized Swagger scan(Application app, WebConfig sc) { + Swagger swagger = null; + Scanner scanner = ScannerFactory.getScanner(); + LOGGER.debug("using scanner " + scanner); + + if (scanner != null) { + SwaggerSerializers.setPrettyPrint(scanner.getPrettyPrint()); + swagger = (Swagger) context.getAttribute("swagger"); + + Set<Class<?>> classes; + if (scanner instanceof JaxrsScanner) { + JaxrsScanner jaxrsScanner = (JaxrsScanner) scanner; + classes = jaxrsScanner.classesFromContext(app, new ServletConfigAdapter(sc)); + } else { + classes = scanner.classes(); + } + if (classes != null) { + Reader reader = new Reader(swagger, ReaderConfigUtils.getReaderConfig(context)); + swagger = reader.read(classes); + if (scanner instanceof SwaggerConfig) { + swagger = ((SwaggerConfig) scanner).configure(swagger); + } else { + SwaggerConfig configurator = (SwaggerConfig) context.getAttribute("reader"); + if (configurator != null) { + LOGGER.debug("configuring swagger with " + configurator); + configurator.configure(swagger); + } else { + LOGGER.debug("no configurator"); + } + } + context.setAttribute("swagger", swagger); + } + } + initialized = true; + return swagger; + } + + private Swagger process( + Application app, + WebConfig sc, + HttpHeaders headers, + UriInfo uriInfo) { + Swagger swagger = (Swagger) context.getAttribute("swagger"); + if (!initialized) { + swagger = scan(app, sc); + } + if (swagger != null) { + SwaggerSpecFilter filterImpl = FilterFactory.getFilter(); + if (filterImpl != null) { + SpecFilter f = new SpecFilter(); + swagger = f.filter(swagger, filterImpl, getQueryParams(uriInfo.getQueryParameters()), getCookies(headers), + getHeaders(headers)); + } + } + return swagger; + } + + @GET + @Produces({MediaType.APPLICATION_JSON, "application/yaml"}) + @ApiOperation(value = "The swagger definition in either JSON or YAML", hidden = true) + @Path("/swagger.{type:json|yaml}") + public Response getListing( + @Context Application app, + @Context WebConfig sc, + @Context HttpHeaders headers, + @Context UriInfo uriInfo, + @PathParam("type") String type) { + if (StringUtils.isNotBlank(type) && type.trim().equalsIgnoreCase("yaml")) { + return getListingYaml(app, sc, headers, uriInfo); + } else { + return getListingJson(app, sc, headers, uriInfo); + } + } + + @GET + @Produces({MediaType.APPLICATION_JSON}) + @Path("/swagger") + @ApiOperation(value = "The swagger definition in JSON", hidden = true) + public Response getListingJson( + @Context Application app, + @Context WebConfig sc, + @Context HttpHeaders headers, + @Context UriInfo uriInfo) { + Swagger swagger = process(app, sc, headers, uriInfo); + + if (swagger != null) { + return Response.ok().entity(swagger).build(); + } else { + return Response.status(404).build(); + } + } + + @GET + @Produces("application/yaml") + @Path("/swagger") + @ApiOperation(value = "The swagger definition in YAML", hidden = true) + public Response getListingYaml( + @Context Application app, + @Context WebConfig sc, + @Context HttpHeaders headers, + @Context UriInfo uriInfo) { + Swagger swagger = process(app, sc, headers, uriInfo); + try { + if (swagger != null) { + String yaml = Yaml.mapper().writeValueAsString(swagger); + StringBuilder b = new StringBuilder(); + String[] parts = yaml.split("\n"); + for (String part : parts) { + b.append(part); + b.append("\n"); + } + return Response.ok().entity(b.toString()).type("application/yaml").build(); + } + } catch (Exception e) { + e.printStackTrace(); + } + return Response.status(404).build(); + } + + protected Map<String, List<String>> getQueryParams(MultivaluedMap<String, String> params) { + Map<String, List<String>> output = new HashMap<>(); + if (params != null) { + for (String key : params.keySet()) { + List<String> values = params.get(key); + output.put(key, values); + } + } + return output; + } + + protected Map<String, String> getCookies(HttpHeaders headers) { + Map<String, String> output = new HashMap<>(); + if (headers != null) { + for (String key : headers.getCookies().keySet()) { + Cookie cookie = headers.getCookies().get(key); + output.put(key, cookie.getValue()); + } + } + return output; + } + + protected Map<String, List<String>> getHeaders(HttpHeaders headers) { + Map<String, List<String>> output = new HashMap<>(); + if (headers != null) { + for (String key : headers.getRequestHeaders().keySet()) { + List<String> values = headers.getRequestHeaders().get(key); + output.put(key, values); + } + } + return output; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5c625626/utils/rest-swagger/src/main/java/org/apache/brooklyn/rest/apidoc/ApidocEndpoint.java ---------------------------------------------------------------------- diff --git a/utils/rest-swagger/src/main/java/org/apache/brooklyn/rest/apidoc/ApidocEndpoint.java b/utils/rest-swagger/src/main/java/org/apache/brooklyn/rest/apidoc/ApidocEndpoint.java deleted file mode 100644 index 9260c26..0000000 --- a/utils/rest-swagger/src/main/java/org/apache/brooklyn/rest/apidoc/ApidocEndpoint.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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.brooklyn.rest.apidoc; - -import java.util.Comparator; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; - -import com.wordnik.swagger.core.DocumentationEndPoint; - -@JsonIgnoreProperties({ - "com$wordnik$swagger$core$DocumentationEndPoint$$ops" -}) -public class ApidocEndpoint extends DocumentationEndPoint { - - public static final Comparator<ApidocEndpoint> COMPARATOR = new Comparator<ApidocEndpoint>() { - @Override - public int compare(ApidocEndpoint o1, ApidocEndpoint o2) { - if (o1.name==o2.name) return 0; - if (o1.name==null) return -1; - if (o2.name==null) return 1; - return o1.name.compareTo(o2.name); - } - }; - - public final String name; - public final String link; - - @JsonCreator - public ApidocEndpoint(@JsonProperty("name") String name, @JsonProperty("path") String path, @JsonProperty("description") String description, @JsonProperty("link") String link) { - super(path, description); - this.name = name; - this.link = link; - } - -} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5c625626/utils/rest-swagger/src/main/java/org/apache/brooklyn/rest/apidoc/ApidocHelpMessageBodyWriter.java ---------------------------------------------------------------------- diff --git a/utils/rest-swagger/src/main/java/org/apache/brooklyn/rest/apidoc/ApidocHelpMessageBodyWriter.java b/utils/rest-swagger/src/main/java/org/apache/brooklyn/rest/apidoc/ApidocHelpMessageBodyWriter.java deleted file mode 100644 index 12114e6..0000000 --- a/utils/rest-swagger/src/main/java/org/apache/brooklyn/rest/apidoc/ApidocHelpMessageBodyWriter.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.brooklyn.rest.apidoc; - -import javax.ws.rs.ext.Provider; - -import com.wordnik.swagger.jaxrs.ApiHelpMessageBodyWriter; - -/** subclassed for convenience */ -@Provider -public class ApidocHelpMessageBodyWriter extends ApiHelpMessageBodyWriter { -} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5c625626/utils/rest-swagger/src/main/java/org/apache/brooklyn/rest/apidoc/ApidocResource.java ---------------------------------------------------------------------- diff --git a/utils/rest-swagger/src/main/java/org/apache/brooklyn/rest/apidoc/ApidocResource.java b/utils/rest-swagger/src/main/java/org/apache/brooklyn/rest/apidoc/ApidocResource.java deleted file mode 100644 index 2c0330e..0000000 --- a/utils/rest-swagger/src/main/java/org/apache/brooklyn/rest/apidoc/ApidocResource.java +++ /dev/null @@ -1,294 +0,0 @@ -/* - * 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.brooklyn.rest.apidoc; - -import org.apache.brooklyn.swagger.annotations.Apidoc; -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriInfo; - -import com.google.common.collect.ImmutableList; -import com.sun.jersey.api.core.ResourceConfig; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.core.Documentation; -import io.swagger.core.DocumentationEndPoint; -import io.swagger.jaxrs.ConfigReader; -import io.swagger.jaxrs.HelpApi; -import io.swagger.jaxrs.JaxrsApiReader; -import io.swagger.jaxrs.JaxrsApiSpecParser; - -@Produces({"application/json"}) -/** create a concrete subclass for this annotated with the Path where - * this resource should live - * <p> - * like Swagger ApiListing (and based on that) but: - * supports singletons as well as classes; - * supports simpler Apidoc annotation (doesn't repeat path, in common case); - * doesn't support listingPath/Class that swagger does (but describes in under /apidoc/name.of.Class - * does not support auth filters - */ -abstract public class ApidocResource { - - static ConfigReader configReader; - static { - JaxrsApiReader.setFormatString(""); - } - - protected boolean isSupportedMediaType(String type) { - return "application/json".equals(type) || "application/xml".equals(type); - } - - protected boolean isIncludedForDocumentation(Class<?> resource) { - // TODO currently only support @Produces, not Contenty-type header, or Accept header (which original ApiListing does support) - Produces produces = getAnnotation(resource, Produces.class); - if (produces == null) return false; - for (String type: produces.value()) - if (isSupportedMediaType(type)) - return true; - return false; - } - - protected <A extends Annotation> A getAnnotation(Class<?> r, Class<A> annotationClass) { - A result = r.getAnnotation(annotationClass); - if (result == null) { - // first look at things directly on superclass (not inherited) - if (r.getSuperclass()!=null) - result = r.getSuperclass().getAnnotation(annotationClass); - } - if (result == null) { - // then look at interfaces here (not inherited) - // we look at superclasses next so don't have to here - for(Class<?> parentInterface : r.getInterfaces()) { - result = parentInterface.getAnnotation(annotationClass); - if (result != null) break; - } - } - if (result == null) { - // lastly take annotations on superclass and interfaces on superclass, recursively - // (so in short we prefer things lower down) - if (r.getSuperclass()!=null) - result = getAnnotation(r.getSuperclass(), annotationClass); - } - return result; - } - - protected String getLinkFor(String path, Class<?> resource) { - return getClass().getAnnotation(Path.class).value()+"/"+getLinkWordFor(resource); - } - - protected String getLinkWordFor(Class<?> resource) { - if (resource.getCanonicalName()!=null) - return resource.getCanonicalName(); - else - return Integer.toHexString(resource.hashCode()); - } - - protected Class<?> getResourceOfLink(ResourceConfig rc, String link) { - for (Class<?> r: getResourceClasses(rc)) { - if (getLinkWordFor(r).equals(link)) - return r; - } - return null; - } - - @GET - @ApiOperation(value = "Returns list of all available API resource endpoints", - response = ApidocRoot.class, - responseContainer = "List") - public Response getAllApis( - @Context ResourceConfig rc, - @Context HttpHeaders headers, - @Context UriInfo uriInfo) { - - String apiVersion = getConfigReader().getApiVersion(); - String swaggerVersion = getConfigReader().getSwaggerVersion(); - String basePath = getConfigReader().getBasePath(); - - Set<Class<?>> resources = getResourceClasses(rc); - - ApidocRoot allApiDoc = new ApidocRoot(); - - List<ApidocEndpoint> endpoints = new ArrayList<>(); - for (Class<?> resource : resources) { - if (!isIncludedForDocumentation(resource)) - continue; - - Apidoc apidoc = getAnnotation(resource, Apidoc.class); - Api apidocX = getAnnotation(resource, Api.class); - Path rsPath = getAnnotation(resource, Path.class); - - if (apidoc==null && apidocX == null) continue; - String path = rsPath.value(); - String name = null; - String description; - - if (apidoc!=null) { - name = apidoc.value(); - description = apidoc.description(); - } else { - path = apidocX.value(); - description = apidocX.description(); - } - - endpoints.add(new ApidocEndpoint(name, path, description, getLinkFor(path, resource))); - } - - Collections.sort(endpoints, ApidocEndpoint.COMPARATOR); - - for (ApidocEndpoint api: endpoints) { - if (!isApiAdded(allApiDoc, api)) { - allApiDoc.addApi(api); - } - } - allApiDoc.setSwaggerVersion(swaggerVersion); - allApiDoc.setBasePath(basePath); - allApiDoc.setApiVersion(apiVersion); - - return Response.ok().entity(allApiDoc).build(); - } - - protected Set<Class<?>> getResourceClasses(ResourceConfig rc) { - Set<Class<?>> resourceClasses = rc.getRootResourceClasses(); - Set<Object> resourceObjects = rc.getRootResourceSingletons(); - - Set<Class<?>> resources = new LinkedHashSet<Class<?>>(); - // @Path should always be set, right? unless something is oddd - for (Class<?> r: resourceClasses) - if (r.getAnnotation(Path.class)!=null) resources.add(r); - for (Object r: resourceObjects) { - if (getAnnotation(r.getClass(), Path.class)!=null) { - resources.add(r.getClass()); - } - } - return resources; - } - - private boolean isApiAdded(Documentation allApiDoc, DocumentationEndPoint endpoint) { - boolean isAdded = false; - if (allApiDoc.getApis() != null) { - for (DocumentationEndPoint addedApi : allApiDoc.getApis()) { - if (endpoint.getPath().equals(addedApi.getPath())) isAdded = true; - } - } - return isAdded; - } - - @GET - @Path("/{resource}") - @ApiOperation(value = "Returns detail on the given API resource endpoint", - response = DocumentationEndPoint.class, - responseContainer = "List") - public Response details( - @Context ResourceConfig rc, - @Context HttpHeaders headers, - @Context UriInfo uriInfo, - @PathParam("resource") String resource) { - Class<?> target = getResourceOfLink(rc, resource); - if (target==null) return Response.status(Response.Status.NOT_FOUND).build(); - - // roughly duplicates JavaHelp - String apiVersion = getConfigReader().getApiVersion(); - String swaggerVersion = getConfigReader().getSwaggerVersion(); - String basePath = getConfigReader().getBasePath(); - - String apiFilterClassName = getConfigReader().getApiFilterClassName(); - - Apidoc apidoc = getAnnotation(target, Apidoc.class); - Api apidocX = getAnnotation(target, Api.class); - Path rsPath = getAnnotation(target, Path.class); - - if ((apidoc==null && apidocX==null) || rsPath==null) - return Response.status(Response.Status.NOT_FOUND).build(); - - String apiPath = apidoc!=null ? rsPath.value() : apidocX.value(); - - HelpApi helpApi = new HelpApi(apiFilterClassName); - Documentation doc = read(target, apiVersion, swaggerVersion, basePath, apiPath); - Documentation docs = helpApi.filterDocs(doc, headers, uriInfo, apiPath, apiPath); - return Response.ok().entity(docs).build(); - } - - - - - // items below here simply override the swagger Jaxrs* classes/behaviour so we can use @Path/@Apidoc instead of @Api - - protected ConfigReader getConfigReader() { - if (configReader==null) configReader = new ConfigReader(null); - return configReader; - } - - static protected Map<Class<?>,Documentation> endpointsCache = new LinkedHashMap<Class<?>, Documentation>(); - - protected Documentation read(Class<?> target, String apiVersion, String swaggerVersion, String basePath, String apiPath) { - Documentation result = endpointsCache.get(target); - if (result!=null) return result; - JaxrsApiSpecParser parser = new ApidocJaxrsSpecParser(target, apiVersion, swaggerVersion, basePath, apiPath); - result = parser.parse(); - endpointsCache.put(target, result); - return result; - } - - @Api("ignored") - static class ApidocJaxrsSpecParser extends JaxrsApiSpecParser { - public ApidocJaxrsSpecParser(Class<?> target, String apiVersion, String swaggerVersion, String basePath, String apiPath) { - super(target, apiVersion, swaggerVersion, basePath, apiPath); - } - @Override - public Api apiEndpoint() { - // return an ignored item; all clients do is check it isn't null - return ApidocJaxrsSpecParser.class.getAnnotation(Api.class); - } - @Override - public Class<?> hostClass() { - // Overriding to make sure we have a look at the interfaces (Jersey jaxrs implementation doesn't bother) - // Note this means we require the @Path class annotation on the same class as all the method annotations - for (Class<?> tryMe : ImmutableList.<Class<?>>builder().add(super.hostClass()).add(super.hostClass().getInterfaces()).build()) { - if (tryMe.getAnnotation(Path.class) != null) { - return tryMe; - } - } - return super.hostClass(); - } - - public String getPath(Method method) { - Path cwsPath = hostClass().getAnnotation(Path.class); - Path mwsPath = method.getAnnotation(Path.class); - if (cwsPath==null) return null; - return cwsPath.value() + (mwsPath!=null ? mwsPath.value() : ""); - } - } - -} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5c625626/utils/rest-swagger/src/main/java/org/apache/brooklyn/rest/apidoc/ApidocRoot.java ---------------------------------------------------------------------- diff --git a/utils/rest-swagger/src/main/java/org/apache/brooklyn/rest/apidoc/ApidocRoot.java b/utils/rest-swagger/src/main/java/org/apache/brooklyn/rest/apidoc/ApidocRoot.java deleted file mode 100644 index e88f1ed..0000000 --- a/utils/rest-swagger/src/main/java/org/apache/brooklyn/rest/apidoc/ApidocRoot.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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.brooklyn.rest.apidoc; - -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; - -import io.swagger.core.Documentation; - -@JsonIgnoreProperties({ - "com$wordnik$swagger$core$Documentation$$apis", - "com$wordnik$swagger$core$Documentation$$models" -}) -public class ApidocRoot extends Documentation { - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @JsonProperty("apis") - public List<ApidocEndpoint> getApidocApis() { - return (List) getApis(); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @JsonSetter("apis") - public void setApidocApis(List<ApidocEndpoint> ep) { - super.setApis((List)ep); - } - -} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5c625626/utils/rest-swagger/src/main/java/org/apache/brooklyn/rest/apidoc/RestApiResourceScanner.java ---------------------------------------------------------------------- diff --git a/utils/rest-swagger/src/main/java/org/apache/brooklyn/rest/apidoc/RestApiResourceScanner.java b/utils/rest-swagger/src/main/java/org/apache/brooklyn/rest/apidoc/RestApiResourceScanner.java new file mode 100644 index 0000000..2f3c8c7 --- /dev/null +++ b/utils/rest-swagger/src/main/java/org/apache/brooklyn/rest/apidoc/RestApiResourceScanner.java @@ -0,0 +1,79 @@ +/* + * Copyright 2015 The Apache Software Foundation. + * + * 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 org.apache.brooklyn.rest.apidoc; + +import com.google.common.collect.Sets; +import io.swagger.annotations.Api; +import io.swagger.jaxrs.config.AbstractScanner; +import io.swagger.jaxrs.config.JaxrsScanner; +import java.util.HashSet; +import java.util.Set; +import javax.servlet.ServletConfig; +import javax.ws.rs.core.Application; +import org.apache.brooklyn.util.collections.MutableSet; + +/** + * Much like DefaultJaxrsScanner, but looks at annotations of ancestors as well. + * + * For instance, if a resource implementation exposes an annotated interface, + * that interface will be added as well. + * + * @author Ciprian Ciubotariu <[email protected]> + */ +public class RestApiResourceScanner extends AbstractScanner implements JaxrsScanner { + + private Set<Class<?>> apiClasses = null; + + + private void addAnnotatedClasses(Set<Class<?>> output, Set<Class<?>> classes) { + for (Class<?> clz : classes) { + if (clz.getAnnotation(Api.class) != null) { + output.add(clz); + } + addAnnotatedClasses(output, Sets.newHashSet(clz.getInterfaces())); + } + } + + private synchronized void buildApiClasses(Application app) { + if (apiClasses == null) { + apiClasses = new HashSet<>(); + if (app != null) { + Set<Class<?>> classes = app.getClasses(); + if (classes != null) { + addAnnotatedClasses(apiClasses, classes); + } + Set<Object> singletons = app.getSingletons(); + if (singletons != null) { + for (Object o : singletons) { + addAnnotatedClasses(apiClasses, (MutableSet<Class<?>>) MutableSet.of(o.getClass())); + } + } + } + } + } + + @Override + public Set<Class<?>> classesFromContext(Application app, ServletConfig sc) { + buildApiClasses(app); + return apiClasses; + } + + @Override + public Set<Class<?>> classes() { + return new HashSet<>(); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5c625626/utils/swagger-annotations/pom.xml ---------------------------------------------------------------------- diff --git a/utils/swagger-annotations/pom.xml b/utils/swagger-annotations/pom.xml new file mode 100644 index 0000000..36e5959 --- /dev/null +++ b/utils/swagger-annotations/pom.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + 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. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <artifactId>brooklyn-swagger-annotations</artifactId> + <name>Brooklyn Swagger Annotations</name> + + <description> + Swagger-like annotations developed for Brooklyn but not dependendent on Brooklyn + </description> + + <parent> + <groupId>org.apache.brooklyn</groupId> + <artifactId>brooklyn-parent</artifactId> + <version>0.9.0-SNAPSHOT</version> <!-- BROOKLYN_VERSION --> + <relativePath>../../parent/pom.xml</relativePath> + </parent> + + <dependencies> + <dependency> + <groupId>com.wordnik</groupId> + <artifactId>swagger-core_2.9.1</artifactId> + </dependency> + </dependencies> + +</project> http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5c625626/utils/swagger-annotations/src/main/java/org/apache/brooklyn/swagger/annotations/Apidoc.java ---------------------------------------------------------------------- diff --git a/utils/swagger-annotations/src/main/java/org/apache/brooklyn/swagger/annotations/Apidoc.java b/utils/swagger-annotations/src/main/java/org/apache/brooklyn/swagger/annotations/Apidoc.java new file mode 100644 index 0000000..a9263c2 --- /dev/null +++ b/utils/swagger-annotations/src/main/java/org/apache/brooklyn/swagger/annotations/Apidoc.java @@ -0,0 +1,33 @@ +/* + * 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.brooklyn.swagger.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** like Swagger Api annotation (and treated similarly) but doesn't require path to be repeated, and supports a name */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Apidoc { + String value(); + String description() default ""; + // ? what is 'open' in @Api +}
