├── .gitignore ├── src ├── main │ ├── resources │ │ ├── META-INF │ │ │ └── services │ │ │ │ └── javax.annotation.processing.Processor │ │ └── org │ │ │ └── versly │ │ │ └── rest │ │ │ └── wsdoc │ │ │ ├── RamlDocumentation.ftl │ │ │ └── RestDocumentation.ftl │ └── java │ │ └── org │ │ └── versly │ │ └── rest │ │ └── wsdoc │ │ ├── ReturnType.java │ │ ├── DocumentationTraits.java │ │ ├── DocumentationRestApi.java │ │ ├── DocumentationScope.java │ │ ├── AuthorizationScope.java │ │ ├── impl │ │ ├── JsonType.java │ │ ├── JsonArray.java │ │ ├── JsonRecursiveObject.java │ │ ├── JsonDict.java │ │ ├── JsonObject.java │ │ ├── Utils.java │ │ ├── SpringMVCRestImplementationSupport.java │ │ ├── JaxRSRestImplementationSupport.java │ │ ├── JsonPrimitive.java │ │ ├── SpringMVC43RestImplementationSupport.java │ │ └── RestDocumentation.java │ │ ├── RestApiMountPoint.java │ │ └── RestDocAssembler.java └── test │ ├── java │ └── org │ │ └── versly │ │ └── rest │ │ └── wsdoc │ │ ├── model │ │ ├── genericdomain │ │ │ ├── Enum1.java │ │ │ ├── Parent.java │ │ │ ├── Child.java │ │ │ ├── WithEnumSet.java │ │ │ ├── DefaultParent.java │ │ │ └── Grandparent.java │ │ ├── ParameterizedTypeReferrer.java │ │ ├── WildcardTypeReferrer.java │ │ ├── ValueWithOptional.java │ │ └── GenericTypeContainer.java │ │ ├── JaxRSRestAnnotationProcessorTest.java │ │ └── SpringMVCRestAnnotationProcessorTest.java │ └── resources │ └── org │ └── versly │ └── rest │ └── wsdoc │ ├── jaxrs │ ├── NoPath.java │ ├── ClassPathOnly.java │ ├── UriFirstParameterValidation.java │ ├── ApiLevelDocs.java │ ├── ApiLevelTemplateDocs.java │ ├── AllMethods.java │ ├── UriParameterNormalization.java │ ├── PostWithRequestBody.java │ ├── AuthorizationScopes.java │ ├── NonRecursiveMultiUse.java │ ├── GenericResponse.java │ ├── SnowReportController.java │ ├── TraitsAnnotations.java │ ├── MultiApiLevelDocs.java │ ├── PublicationScopes.java │ └── RestDocEndpoint.java │ └── springmvc │ ├── UriFirstParameterValidation.java │ ├── genericdomain │ ├── WildcardController.java │ ├── AsyncController.java │ ├── ChildController.java │ └── ParentController.java │ ├── ApiLevelDocs.java │ ├── ApiLevelTemplateDocs.java │ ├── UriParameterNormalization.java │ ├── AllMethods.java │ ├── EnumSetController.java │ ├── AuthorizationScopes.java │ ├── NonRecursiveMultiUse.java │ ├── GenericResponse.java │ ├── SnowReportController.java │ ├── TraitsAnnotations.java │ ├── MultiApiLevelDocs.java │ ├── PublicationScopes.java │ ├── Spring43ComposedAnnotationController.java │ └── RestDocEndpoint.java ├── .github └── workflows │ └── maven.yml ├── pom.xml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | bin 3 | test-output 4 | *.iml 5 | *.ipr 6 | *.iws 7 | .idea 8 | .classpath 9 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/javax.annotation.processing.Processor: -------------------------------------------------------------------------------- 1 | org.versly.rest.wsdoc.AnnotationProcessor 2 | -------------------------------------------------------------------------------- /src/test/java/org/versly/rest/wsdoc/model/genericdomain/Enum1.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc.model.genericdomain; 2 | 3 | public enum Enum1 { 4 | TEST1, TEST2 5 | } -------------------------------------------------------------------------------- /src/test/java/org/versly/rest/wsdoc/model/genericdomain/Parent.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc.model.genericdomain; 2 | 3 | public interface Parent { 4 | 5 | T getParentField(); 6 | } 7 | -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/jaxrs/NoPath.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc.jaxrs; 2 | 3 | import javax.ws.rs.GET; 4 | 5 | public class NoPath { 6 | 7 | @GET 8 | public void noPathGet() { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/test/java/org/versly/rest/wsdoc/model/ParameterizedTypeReferrer.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc.model; 2 | 3 | public class ParameterizedTypeReferrer { 4 | public GenericTypeContainer getGenericTypeContainer() { 5 | return null; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/test/java/org/versly/rest/wsdoc/model/WildcardTypeReferrer.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc.model; 2 | 3 | import java.util.concurrent.Callable; 4 | 5 | public class WildcardTypeReferrer { 6 | public Callable getWildcardType() { 7 | return null; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/jaxrs/ClassPathOnly.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc.jaxrs; 2 | 3 | import javax.ws.rs.GET; 4 | import javax.ws.rs.Path; 5 | 6 | @Path("classPathOnly") 7 | public class ClassPathOnly { 8 | 9 | @GET 10 | public void classPathOnlyGet() { 11 | } 12 | } -------------------------------------------------------------------------------- /src/test/java/org/versly/rest/wsdoc/model/ValueWithOptional.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc.model; 2 | 3 | import java.util.Optional; 4 | 5 | public class ValueWithOptional { 6 | /** 7 | * this field may be omitted! 8 | */ 9 | public Optional getOptionalfield() { 10 | return null; 11 | } 12 | 13 | public void setOptionalfield(Optional other) { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/org/versly/rest/wsdoc/model/genericdomain/Child.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc.model.genericdomain; 2 | 3 | public class Child extends DefaultParent { 4 | 5 | private String childField; 6 | 7 | public String getChildField() { 8 | return childField; 9 | } 10 | 11 | public void setChildField(String childField) { 12 | this.childField = childField; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/org/versly/rest/wsdoc/ReturnType.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Target; 5 | 6 | /** 7 | * Documents the expected return type for wrapped responses. 8 | * Example: @ReturnType(User.class) public Response getUser() {...} 9 | */ 10 | @Target(ElementType.METHOD) 11 | public @interface ReturnType { 12 | Class value(); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/org/versly/rest/wsdoc/DocumentationTraits.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Target; 5 | 6 | @Target({ ElementType.TYPE, ElementType.METHOD }) 7 | public @interface DocumentationTraits { 8 | 9 | public static final String STABLE = "stable"; 10 | public static final String DEPRECATED = "deprecated"; 11 | public static final String EXPERIMENTAL = "experimental"; 12 | 13 | String[] value() default {}; 14 | } 15 | -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/springmvc/UriFirstParameterValidation.java: -------------------------------------------------------------------------------- 1 | import org.springframework.web.bind.annotation.*; 2 | 3 | import java.net.URI; 4 | import java.util.UUID; 5 | 6 | @RequestMapping("${base-service.server.api-path:}/api/v1/") 7 | public class UriFirstParameterValidation { 8 | /** 9 | * Some description of the group. 10 | * @param id The participant's parent identifier. 11 | */ 12 | @RequestMapping(value = "/group/{id}/participant", method = RequestMethod.GET) 13 | public void getParticipant(@PathVariable("id") String id) { 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/java/org/versly/rest/wsdoc/DocumentationRestApi.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Target; 5 | 6 | @Target(ElementType.TYPE) 7 | public @interface DocumentationRestApi { 8 | String ID_TEMPLATE = "@WSDOC_ID@"; 9 | String MOUNT_TEMPLATE = "@WSDOC_MOUNT@"; 10 | String TITLE_TEMPLATE = "@WSDOC_TITLE@"; 11 | String VERSION_TEMPLATE = "@WSDOC_VERSION@"; 12 | 13 | String id() default "(default)"; 14 | String mount() default ""; 15 | String title() default ""; 16 | String version() default ""; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/org/versly/rest/wsdoc/DocumentationScope.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Target; 5 | 6 | /** 7 | * Define a scope for an endpoint or its containing class. The documentation of endpoints may be filtered 8 | * based on scope. 9 | * 10 | * @author aisac 11 | * 12 | */ 13 | @Target({ ElementType.TYPE, ElementType.METHOD }) 14 | public @interface DocumentationScope { 15 | 16 | public static final String PUBLIC = "public"; 17 | public static final String PRIVATE = "private"; 18 | 19 | String[] value() default {}; 20 | } 21 | -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/springmvc/genericdomain/WildcardController.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc.springmvc.genericdomain; 2 | 3 | import org.springframework.web.bind.annotation.RequestMapping; 4 | import org.springframework.web.bind.annotation.RequestMethod; 5 | import org.versly.rest.wsdoc.model.WildcardTypeReferrer; 6 | 7 | public class WildcardController { 8 | 9 | /** 10 | * Retrieves a field that has a wildcard in its nested fields 11 | */ 12 | @RequestMapping(value = "/foo", method = RequestMethod.GET) 13 | public WildcardTypeReferrer getFoo() { 14 | return null; 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/jaxrs/UriFirstParameterValidation.java: -------------------------------------------------------------------------------- 1 | import org.springframework.web.bind.annotation.*; 2 | 3 | 4 | import java.net.URI; 5 | import java.util.UUID; 6 | import javax.ws.rs.GET; 7 | import javax.ws.rs.POST; 8 | import javax.ws.rs.Path; 9 | import javax.ws.rs.PathParam; 10 | 11 | @Path("${base-service.server.api-path:}/api/v1/") 12 | public class UriFirstParameterValidation { 13 | /** 14 | * Some description of the group. 15 | * @param id The participant's parent identifier. 16 | */ 17 | @GET 18 | @Path("/group/{id}/participant") 19 | public void getParticipant(@PathVariable("id") String id) { 20 | } 21 | } -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/jaxrs/ApiLevelDocs.java: -------------------------------------------------------------------------------- 1 | import org.versly.rest.wsdoc.DocumentationScope; 2 | import org.versly.rest.wsdoc.DocumentationRestApi; 3 | 4 | import javax.ws.rs.Path; 5 | import javax.ws.rs.GET; 6 | 7 | /** 8 | * Some documentation of the API itself. 9 | */ 10 | @DocumentationRestApi(id = "UltimateApi", title = "The Ultimate REST API", version = "v1", mount = "/ultimate/api/v1") 11 | @DocumentationScope("public") 12 | public class ApiLevelDocs { 13 | 14 | /** 15 | * Some description of the widgets. 16 | * @param id The widget identifier. 17 | */ 18 | @GET 19 | @Path("/widgets") 20 | public void getWidget() { 21 | } 22 | } -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/springmvc/ApiLevelDocs.java: -------------------------------------------------------------------------------- 1 | import org.springframework.web.bind.annotation.*; 2 | import org.versly.rest.wsdoc.DocumentationScope; 3 | import org.versly.rest.wsdoc.DocumentationRestApi; 4 | 5 | /** 6 | * Some documentation of the API itself. 7 | */ 8 | @DocumentationRestApi(id = "UltimateApi", title = "The Ultimate REST API", version = "v1", mount = "/ultimate/api/v1") 9 | @DocumentationScope("public") 10 | public class ApiLevelDocs { 11 | 12 | /** 13 | * Some description of the widgets. 14 | * @param id The widget identifier. 15 | */ 16 | @RequestMapping(value = "/widgets", method = RequestMethod.GET) 17 | public void getWidget() { 18 | } 19 | } -------------------------------------------------------------------------------- /src/test/java/org/versly/rest/wsdoc/model/genericdomain/WithEnumSet.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc.model.genericdomain; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.EnumSet; 5 | 6 | public class WithEnumSet { 7 | private EnumSet myEnumSet = EnumSet.noneOf(Enum1.class); 8 | private Enum1 myEnum; 9 | 10 | public EnumSet getMyEnumSet() { 11 | return myEnumSet; 12 | } 13 | 14 | public void setMyEnumSet(EnumSet myEnumSet) { 15 | this.myEnumSet = myEnumSet; 16 | } 17 | 18 | public Enum1 getMyEnum() { 19 | return myEnum; 20 | } 21 | 22 | public void setMyEnum(Enum1 myEnum) { 23 | this.myEnum = myEnum; 24 | } 25 | } -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/springmvc/genericdomain/AsyncController.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc.springmvc.genericdomain; 2 | 3 | import org.springframework.web.bind.annotation.RequestMapping; 4 | import org.springframework.web.bind.annotation.RequestMethod; 5 | import org.springframework.web.context.request.async.WebAsyncTask; 6 | 7 | import org.versly.rest.wsdoc.model.genericdomain.Child; 8 | 9 | public class AsyncController { 10 | 11 | /** 12 | * An async controller method. Should be documented just as a child return. 13 | */ 14 | @RequestMapping(value = "/child", method = RequestMethod.GET) 15 | public WebAsyncTask getChild() { 16 | return null; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/versly/rest/wsdoc/AuthorizationScope.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * This annotation documents Oauth2 authorization scopes, each of which satisfy the authorization scope criterion 10 | * for access to the annotated REST endpoint(s). It may be applied at either the class or method level. The 11 | * if applied at both levels, the effect is to recognize the union of scopes from both levels. 12 | */ 13 | @Retention(RetentionPolicy.RUNTIME) 14 | @Target({ ElementType.TYPE, ElementType.METHOD }) 15 | public @interface AuthorizationScope { 16 | String[] value() default {}; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/org/versly/rest/wsdoc/impl/JsonType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 TaskDock, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.versly.rest.wsdoc.impl; 18 | 19 | public interface JsonType { 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/test/java/org/versly/rest/wsdoc/model/genericdomain/DefaultParent.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc.model.genericdomain; 2 | 3 | 4 | public class DefaultParent extends Grandparent implements Parent { 5 | 6 | private T parentField; 7 | 8 | private Grandparent grandparent; 9 | 10 | @Override 11 | public T getParentField() { 12 | return parentField; 13 | } 14 | 15 | public void setParentField(T parentField) { 16 | this.parentField = parentField; 17 | } 18 | 19 | public Grandparent getGrandparent() { 20 | return grandparent; 21 | } 22 | 23 | public void setGrandparent(Grandparent grandparent) { 24 | this.grandparent = grandparent; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/org/versly/rest/wsdoc/model/genericdomain/Grandparent.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc.model.genericdomain; 2 | 3 | /** 4 | * User: Ryan Walls 5 | * Date: 1/11/13 6 | */ 7 | public class Grandparent { 8 | 9 | private S firstGrandparentField; 10 | private T secondGrandparentField; 11 | 12 | public S getFirstGrandparentField() { 13 | return firstGrandparentField; 14 | } 15 | 16 | public void setFirstGrandparentField(S firstGrandparentField) { 17 | this.firstGrandparentField = firstGrandparentField; 18 | } 19 | 20 | public T getSecondGrandparentField() { 21 | return secondGrandparentField; 22 | } 23 | 24 | public void setSecondGrandparentField(T secondGrandparentField) { 25 | this.secondGrandparentField = secondGrandparentField; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/jaxrs/ApiLevelTemplateDocs.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc.jaxrs; 2 | 3 | import org.versly.rest.wsdoc.DocumentationScope; 4 | import org.versly.rest.wsdoc.DocumentationRestApi; 5 | 6 | import javax.ws.rs.Path; 7 | import javax.ws.rs.GET; 8 | 9 | /** 10 | * Some documentation of the API itself. 11 | */ 12 | @DocumentationRestApi( 13 | id = DocumentationRestApi.ID_TEMPLATE, 14 | title = "The Ultimate REST API", 15 | version = "v1", 16 | mount = DocumentationRestApi.MOUNT_TEMPLATE) 17 | @DocumentationScope("public") 18 | public class ApiLevelTemplateDocs { 19 | 20 | /** 21 | * Some description of the widgets. 22 | * @param id The widget identifier. 23 | */ 24 | @GET 25 | @Path("/widgets") 26 | public void getWidget() { 27 | } 28 | } -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/springmvc/ApiLevelTemplateDocs.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc.springmvc; 2 | 3 | import org.springframework.web.bind.annotation.*; 4 | import org.versly.rest.wsdoc.DocumentationScope; 5 | import org.versly.rest.wsdoc.DocumentationRestApi; 6 | 7 | /** 8 | * Some documentation of the API itself. 9 | */ 10 | @DocumentationRestApi( 11 | id = DocumentationRestApi.ID_TEMPLATE, 12 | title = "The Ultimate REST API", 13 | version = "v1", 14 | mount = DocumentationRestApi.MOUNT_TEMPLATE) 15 | @DocumentationScope("public") 16 | public class ApiLevelTemplateDocs { 17 | 18 | /** 19 | * Some description of the widgets. 20 | * @param id The widget identifier. 21 | */ 22 | @RequestMapping(value = "/widgets", method = RequestMethod.GET) 23 | public void getWidget() { 24 | } 25 | } -------------------------------------------------------------------------------- /src/test/java/org/versly/rest/wsdoc/model/GenericTypeContainer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 TaskDock, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.versly.rest.wsdoc.model; 18 | 19 | import java.util.List; 20 | 21 | public class GenericTypeContainer { 22 | 23 | public List getContainedList() { 24 | return null; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/jaxrs/AllMethods.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc.jaxrs; 2 | 3 | import javax.ws.rs.Path; 4 | import javax.ws.rs.GET; 5 | import javax.ws.rs.HEAD; 6 | import javax.ws.rs.OPTIONS; 7 | import javax.ws.rs.POST; 8 | import javax.ws.rs.PUT; 9 | import javax.ws.rs.DELETE; 10 | 11 | public class AllMethods { 12 | 13 | @GET 14 | @Path("allMethodsGet") 15 | public void allMethodsGet() { 16 | } 17 | 18 | @POST 19 | @Path("allMethodsPost") 20 | public void allMethodsPost() { 21 | } 22 | 23 | @PUT 24 | @Path("allMethodsPut") 25 | public void allMethodsPut() { 26 | } 27 | 28 | @DELETE 29 | @Path("allMethodsDelete") 30 | public void allMethodsDelete() { 31 | } 32 | 33 | @HEAD 34 | @Path("allMethodsHead") 35 | public void allMethodsHead() { 36 | } 37 | 38 | @OPTIONS 39 | @Path("allMethodsOptions") 40 | public void allMethodsOptions() { 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Java CI with Maven 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build-jdk8: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up JDK 1.8 18 | uses: actions/setup-java@v1 19 | with: 20 | java-version: 1.8 21 | - name: "[mvn -B clean verify -U]" 22 | run: mvn -B clean verify -U 23 | build-jdk11: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v2 27 | - name: Set up JDK 11 28 | uses: actions/setup-java@v1.4.3 29 | with: 30 | java-version: '11.0.2+9' 31 | - name: "[mvn -B clean verify -U]" 32 | run: mvn -B clean verify -U 33 | 34 | -------------------------------------------------------------------------------- /src/main/java/org/versly/rest/wsdoc/impl/JsonArray.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 TaskDock, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.versly.rest.wsdoc.impl; 18 | 19 | import java.io.Serializable; 20 | 21 | public class JsonArray implements JsonType, Serializable { 22 | private JsonType elementType; 23 | 24 | public JsonArray(JsonType elementType) { 25 | this.elementType = elementType; 26 | } 27 | 28 | public JsonType getElementType() { 29 | return elementType; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/jaxrs/UriParameterNormalization.java: -------------------------------------------------------------------------------- 1 | import org.springframework.web.bind.annotation.*; 2 | 3 | 4 | import java.net.URI; 5 | import java.util.UUID; 6 | import javax.ws.rs.GET; 7 | import javax.ws.rs.POST; 8 | import javax.ws.rs.Path; 9 | import javax.ws.rs.PathParam; 10 | 11 | public class UriParameterNormalization { 12 | 13 | /** 14 | * Some description of the widgets. 15 | * @param id The widget identifier documented in GET. 16 | */ 17 | @GET 18 | @Path("/widgets/{id}") 19 | public void getWidget(@PathParam("id") String id) { 20 | } 21 | 22 | /** 23 | * Some description of the widgets. 24 | * @param id The widget identifier documented in POST. 25 | */ 26 | @POST 27 | @Path("/widgets/{id}") 28 | public void createWidget(@PathParam("id") String id) { 29 | } 30 | 31 | /** 32 | * Some description of the gadgets. 33 | * @param id The gadget's parent identifier. 34 | */ 35 | @GET 36 | @Path("/widgets/{id}/gadgets") 37 | public void getGadgets(@PathVariable("id") String id) { 38 | } 39 | } -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/springmvc/UriParameterNormalization.java: -------------------------------------------------------------------------------- 1 | import org.springframework.web.bind.annotation.*; 2 | 3 | import java.net.URI; 4 | import java.util.UUID; 5 | 6 | public class UriParameterNormalization { 7 | 8 | /** 9 | * Some description of the widgets. 10 | * @param id The widget identifier documented in GET. 11 | */ 12 | @RequestMapping(value = "/widgets/{id}", method = RequestMethod.GET) 13 | public void getWidget(@PathVariable("id") String id) { 14 | } 15 | 16 | /** 17 | * Some description of the widgets. 18 | * @param id The widget identifier documented in POST. 19 | */ 20 | @RequestMapping(value = "/widgets/{id}", method = RequestMethod.POST) 21 | public void createWidget(@PathVariable("id") String id) { 22 | } 23 | 24 | /** 25 | * Some description of the gadgets. 26 | * @param id The gadget's parent identifier. 27 | */ 28 | @RequestMapping(value = "/widgets/{id}/gadgets", method = RequestMethod.GET) 29 | public void getGadgets(@PathVariable("id") String id) { 30 | } 31 | } -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/jaxrs/PostWithRequestBody.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 TaskDock, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.versly.rest.wsdoc.jaxrs; 18 | 19 | import javax.ws.rs.POST; 20 | import javax.ws.rs.Path; 21 | 22 | public class PostWithRequestBody { 23 | 24 | @POST 25 | @Path("stringBody") 26 | public void stringBody(String body) { 27 | } 28 | 29 | @POST 30 | @Path("complexBody") 31 | public void complexBody(ComplexBody body) { 32 | } 33 | 34 | public class ComplexBody { 35 | public String foo; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/versly/rest/wsdoc/impl/JsonRecursiveObject.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 TaskDock, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.versly.rest.wsdoc.impl; 18 | 19 | import java.io.Serializable; 20 | 21 | public class JsonRecursiveObject implements JsonType, Serializable { 22 | private String recursedObjectTypeName; 23 | 24 | public JsonRecursiveObject(String recursedObjectTypeName) { 25 | this.recursedObjectTypeName = recursedObjectTypeName; 26 | } 27 | 28 | public String getRecursedObjectTypeName() { 29 | return recursedObjectTypeName; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/springmvc/AllMethods.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc.springmvc; 2 | 3 | import org.springframework.web.bind.annotation.RequestMapping; 4 | import org.springframework.web.bind.annotation.RequestMethod; 5 | 6 | public class AllMethods { 7 | 8 | @RequestMapping(value = "/allMethodsGet", method = RequestMethod.GET) 9 | public void allMethodsGet() { 10 | } 11 | 12 | @RequestMapping(value = "/allMethodsPost", method = RequestMethod.POST) 13 | public void allMethodsPost() { 14 | } 15 | 16 | @RequestMapping(value = "/allMethodsPut", method = RequestMethod.PUT) 17 | public void allMethodsPut() { 18 | } 19 | 20 | @RequestMapping(value = "/allMethodsDelete", method = RequestMethod.DELETE) 21 | public void allMethodsDelete() { 22 | } 23 | 24 | @RequestMapping(value = "/allMethodsHead", method = RequestMethod.HEAD) 25 | public void allMethodsHead() { 26 | } 27 | 28 | @RequestMapping(value = "/allMethodsOptions", method = RequestMethod.OPTIONS) 29 | public void allMethodsOptions() { 30 | } 31 | 32 | @RequestMapping(value = "/allMethodsPatch", method = RequestMethod.PATCH) 33 | public void allMethodsPatch() { 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/springmvc/genericdomain/ChildController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 TaskDock, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.versly.rest.wsdoc.springmvc.genericdomain; 18 | 19 | import org.springframework.web.bind.annotation.RequestMapping; 20 | import org.springframework.web.bind.annotation.RequestMethod; 21 | import org.versly.rest.wsdoc.model.genericdomain.Child; 22 | 23 | public class ChildController { 24 | 25 | /** 26 | * Retrieves the child 27 | */ 28 | @RequestMapping(value = "/child", method = RequestMethod.GET) 29 | public Child getChild() { 30 | return null; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/versly/rest/wsdoc/impl/JsonDict.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 TaskDock, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.versly.rest.wsdoc.impl; 18 | 19 | import java.io.Serializable; 20 | 21 | public class JsonDict implements JsonType, Serializable { 22 | 23 | private JsonType keyType; 24 | private JsonType valType; 25 | 26 | public JsonDict(JsonType keyType, JsonType valType) { 27 | this.keyType = keyType; 28 | this.valType = valType; 29 | } 30 | 31 | public JsonType getKeyType() { 32 | return keyType; 33 | } 34 | 35 | public JsonType getValueType() { 36 | return valType; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/springmvc/genericdomain/ParentController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 TaskDock, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.versly.rest.wsdoc.springmvc.genericdomain; 18 | 19 | import org.springframework.web.bind.annotation.RequestMapping; 20 | import org.springframework.web.bind.annotation.RequestMethod; 21 | import org.versly.rest.wsdoc.model.genericdomain.Parent; 22 | 23 | public class ParentController { 24 | 25 | /** 26 | * Retrieves the parent 27 | */ 28 | @RequestMapping(value = "/parent", method = RequestMethod.GET) 29 | public Parent getParent() { 30 | return null; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/jaxrs/AuthorizationScopes.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc.jaxrs; 2 | 3 | import javax.ws.rs.Path; 4 | import javax.ws.rs.GET; 5 | import javax.ws.rs.POST; 6 | import org.versly.rest.wsdoc.AuthorizationScope; 7 | import org.versly.rest.wsdoc.RestApiMountPoint; 8 | 9 | public class AuthorizationScopes { 10 | 11 | /** 12 | * A controller with no defined authorization scopes 13 | */ 14 | @RestApiMountPoint("/default") 15 | @Path("/api/v1") 16 | public static class DefaultController { 17 | 18 | @GET 19 | @Path("/default") 20 | public void pub() { 21 | } 22 | } 23 | 24 | /** 25 | * A controller with three authorization scopes, two at class level, one at method level. 26 | */ 27 | @RestApiMountPoint("/twoscopes") 28 | @Path("/api/v1") 29 | @AuthorizationScope( { "two_scope_service:read", "two_scope_service:admin" } ) 30 | public static class TwoScopeController { 31 | 32 | @AuthorizationScope("two_scope_service:write") 33 | @POST 34 | @Path("/twoscope") 35 | public void get() { 36 | } 37 | 38 | @GET 39 | @Path("/twoscope") 40 | public void post() { 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/org/versly/rest/wsdoc/RestApiMountPoint.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 TaskDock, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.versly.rest.wsdoc; 18 | 19 | import java.lang.annotation.ElementType; 20 | import java.lang.annotation.Target; 21 | 22 | /** 23 | *

Documents the expected REST URL path to which the {@link org.springframework.web.bind.annotation.RequestMapping}s 24 | * in this class will be mounted, presumably by a servlet configuration etc. This should not include the hostname.

25 | *
26 | *

For example: @RestApiMountPoint("/api/v1/users/").

27 | */ 28 | @Target(ElementType.TYPE) 29 | public @interface RestApiMountPoint { 30 | String value(); 31 | } 32 | -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/springmvc/EnumSetController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 TaskDock, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.versly.rest.wsdoc.springmvc; 18 | 19 | import org.springframework.web.bind.annotation.PathVariable; 20 | import org.springframework.web.bind.annotation.RequestMapping; 21 | import org.springframework.web.bind.annotation.RequestMethod; 22 | import org.springframework.web.bind.annotation.ResponseBody; 23 | import org.versly.rest.wsdoc.model.genericdomain.WithEnumSet; 24 | 25 | import java.util.List; 26 | 27 | public class EnumSetController { 28 | 29 | @RequestMapping(value = "/enumset", method = RequestMethod.GET) 30 | @ResponseBody 31 | public List getEnumSet() { 32 | return null; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/jaxrs/NonRecursiveMultiUse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 TaskDock, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.versly.rest.wsdoc.jaxrs; 18 | 19 | import javax.ws.rs.GET; 20 | import javax.ws.rs.Path; 21 | 22 | public class NonRecursiveMultiUse { 23 | 24 | @GET 25 | @Path("foo") 26 | public NonRecursiveParent foo() { 27 | return null; 28 | } 29 | 30 | public interface NonRecursiveParent { 31 | public NonRecursiveMiddle getMiddle1(); 32 | public NonRecursiveMiddle getMiddle2(); 33 | } 34 | 35 | public interface NonRecursiveMiddle { 36 | public NonRecursiveInnard getInnard(); 37 | } 38 | 39 | public interface NonRecursiveInnard { 40 | public String getString(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/springmvc/AuthorizationScopes.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc.springmvc; 2 | 3 | import org.springframework.web.bind.annotation.RequestMapping; 4 | import org.springframework.web.bind.annotation.RequestMethod; 5 | import org.versly.rest.wsdoc.AuthorizationScope; 6 | import org.versly.rest.wsdoc.RestApiMountPoint; 7 | 8 | public class AuthorizationScopes { 9 | 10 | /** 11 | * A controller with no defined authorization scopes 12 | */ 13 | @RestApiMountPoint("/default") 14 | @RequestMapping("/api/v1") 15 | public static class DefaultController { 16 | 17 | @RequestMapping(value = "/default", method = RequestMethod.GET) 18 | public void pub() { 19 | } 20 | } 21 | 22 | /** 23 | * A controller with three authorization scopes, two at class level, one at method level. 24 | */ 25 | @RestApiMountPoint("/twoscopes") 26 | @RequestMapping("/api/v1") 27 | @AuthorizationScope( { "two_scope_service:read", "two_scope_service:admin" } ) 28 | public static class TwoScopeController { 29 | 30 | @AuthorizationScope("two_scope_service:write") 31 | @RequestMapping(value = "/twoscope", method = RequestMethod.POST) 32 | public void post() { 33 | } 34 | 35 | @RequestMapping(value = "/twoscope", method = RequestMethod.GET) 36 | public void get() { 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/jaxrs/GenericResponse.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc.jaxrs; 2 | 3 | import java.util.UUID; 4 | 5 | import javax.ws.rs.DELETE; 6 | import javax.ws.rs.GET; 7 | import javax.ws.rs.HEAD; 8 | import javax.ws.rs.POST; 9 | import javax.ws.rs.PUT; 10 | import javax.ws.rs.Path; 11 | import javax.ws.rs.core.Response; 12 | 13 | import org.versly.rest.wsdoc.ReturnType; 14 | import org.versly.rest.wsdoc.model.genericdomain.DefaultParent; 15 | 16 | public class GenericResponse { 17 | 18 | @GET 19 | @Path("genericResponseGet") 20 | @ReturnType(UUID.class) 21 | public Response genericResponseGet() { 22 | return Response.ok(UUID.randomUUID()).build(); 23 | } 24 | 25 | @POST 26 | @Path("genericResponsePost") 27 | @ReturnType(String.class) 28 | public String genericResponsePost() { 29 | return UUID.randomUUID().toString(); 30 | } 31 | 32 | @PUT 33 | @Path("genericResponsePut") 34 | @ReturnType(UUID.class) 35 | public UUID genericResponsePut() { 36 | return UUID.randomUUID(); 37 | } 38 | 39 | @DELETE 40 | @Path("genericResponseDelete") 41 | @ReturnType(Response.class) 42 | public Response genericResponseDelete() { 43 | return Response.noContent().build(); 44 | } 45 | 46 | @HEAD 47 | @Path("genericResponseHead") 48 | @ReturnType(DefaultParent.class) 49 | public Response genericResponseHead() { 50 | return null; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/springmvc/NonRecursiveMultiUse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 TaskDock, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.versly.rest.wsdoc.springmvc; 18 | 19 | import org.springframework.web.bind.annotation.RequestMapping; 20 | import org.springframework.web.bind.annotation.RequestMethod; 21 | 22 | public class NonRecursiveMultiUse { 23 | 24 | @RequestMapping(value = "/foo", method = RequestMethod.GET) 25 | public NonRecursiveParent foo() { 26 | return null; 27 | } 28 | 29 | public interface NonRecursiveParent { 30 | public NonRecursiveMiddle getMiddle1(); 31 | public NonRecursiveMiddle getMiddle2(); 32 | } 33 | 34 | public interface NonRecursiveMiddle { 35 | public NonRecursiveInnard getInnard(); 36 | } 37 | 38 | public interface NonRecursiveInnard { 39 | public String getString(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/jaxrs/SnowReportController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 TaskDock, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.versly.rest.wsdoc.jaxrs; 18 | 19 | import javax.ws.rs.GET; 20 | import javax.ws.rs.Path; 21 | import javax.ws.rs.PathParam; 22 | 23 | public class SnowReportController { 24 | 25 | /** 26 | * Retrieves the current snow report for the specified mountain. 27 | */ 28 | @GET 29 | @Path("/snow-report/{mountainId}") 30 | public SnowReport getReportForMountain( 31 | @PathParam("mountainId") String mountainId) { 32 | return null; 33 | } 34 | 35 | 36 | public interface SnowReport { 37 | public String getMountainName(); 38 | public double getTemperatureInFahrenheit(); 39 | public double getStormSnowfall(); 40 | public double getOvernightSnowfall(); 41 | public SkyReport getConditions(); 42 | } 43 | 44 | public enum SkyReport { 45 | CLEAR, OVERCAST, SNOWING 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/springmvc/GenericResponse.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc.springmvc; 2 | 3 | import java.util.UUID; 4 | 5 | import javax.ws.rs.HEAD; 6 | import javax.ws.rs.Path; 7 | import javax.ws.rs.core.Response; 8 | 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RequestMethod; 11 | import org.versly.rest.wsdoc.ReturnType; 12 | import org.versly.rest.wsdoc.model.genericdomain.DefaultParent; 13 | 14 | public class GenericResponse { 15 | 16 | @RequestMapping(value = "/genericResponseGet", method = RequestMethod.GET) 17 | @ReturnType(UUID.class) 18 | public Response genericResponseGet() { 19 | return Response.ok(UUID.randomUUID()).build(); 20 | } 21 | 22 | @RequestMapping(value = "/genericResponsePost", method = RequestMethod.POST) 23 | @ReturnType(String.class) 24 | public String genericResponsePost() { 25 | return UUID.randomUUID().toString(); 26 | } 27 | 28 | @RequestMapping(value = "/genericResponsePut", method = RequestMethod.PUT) 29 | @ReturnType(UUID.class) 30 | public UUID genericResponsePut() { 31 | return UUID.randomUUID(); 32 | } 33 | 34 | @RequestMapping(value = "/genericResponseDelete", method = RequestMethod.DELETE) 35 | @ReturnType(Response.class) 36 | public Response genericResponseDelete() { 37 | return Response.noContent().build(); 38 | } 39 | 40 | @RequestMapping(value = "/genericResponseHead", method = RequestMethod.HEAD) 41 | @ReturnType(DefaultParent.class) 42 | public Response genericResponseHead() { 43 | return Response.noContent().build(); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/springmvc/SnowReportController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 TaskDock, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.versly.rest.wsdoc.springmvc; 18 | 19 | import org.springframework.web.bind.annotation.PathVariable; 20 | import org.springframework.web.bind.annotation.RequestMapping; 21 | import org.springframework.web.bind.annotation.RequestMethod; 22 | 23 | public class SnowReportController { 24 | 25 | /** 26 | * Retrieves the current snow report for the specified mountain. 27 | */ 28 | @RequestMapping(value = "/snow-report/{mountainId}", method = RequestMethod.GET) 29 | public SnowReport getReportForMountain( 30 | @PathVariable("mountainId") String mountainId) { 31 | return null; 32 | } 33 | 34 | 35 | public interface SnowReport { 36 | public String getMountainName(); 37 | public double getTemperatureInFahrenheit(); 38 | public double getStormSnowfall(); 39 | public double getOvernightSnowfall(); 40 | public SkyReport getConditions(); 41 | } 42 | 43 | public enum SkyReport { 44 | CLEAR, OVERCAST, SNOWING 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/jaxrs/TraitsAnnotations.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc.jaxrs; 2 | 3 | import org.versly.rest.wsdoc.DocumentationTraits; 4 | 5 | import javax.swing.text.Document; 6 | import javax.ws.rs.GET; 7 | import javax.ws.rs.Path; 8 | 9 | public class TraitsAnnotations { 10 | 11 | /** 12 | * A controller with all stable methods. 13 | */ 14 | public static class StableController { 15 | 16 | @GET 17 | @Path("/stable1") 18 | public void stable() { 19 | } 20 | } 21 | 22 | /** 23 | * A controller with all deprecated (implicit) methods. 24 | */ 25 | @DocumentationTraits(DocumentationTraits.DEPRECATED) 26 | public static class DeprecatedController { 27 | 28 | @GET 29 | @Path("/deprecated2") 30 | public void deprecated() { 31 | } 32 | } 33 | 34 | /** 35 | * A controller with some stable, deprecated, experimental, and deprecated experimental methods. 36 | */ 37 | public static class MixedController { 38 | 39 | @GET 40 | @Path("/stable3") 41 | public void stable() { 42 | } 43 | 44 | @DocumentationTraits(DocumentationTraits.DEPRECATED) 45 | @GET 46 | @Path("/deprecated3") 47 | public void deprecated() { 48 | } 49 | 50 | @DocumentationTraits(DocumentationTraits.EXPERIMENTAL) 51 | @GET 52 | @Path("/experimental3") 53 | public void experimental() { 54 | } 55 | 56 | @DocumentationTraits({ DocumentationTraits.EXPERIMENTAL, DocumentationTraits.DEPRECATED }) 57 | @GET 58 | @Path("/experimentaldeprecated3") 59 | public void experimentaldeprecated() { 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/springmvc/TraitsAnnotations.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This endpoint has private publication scope. 3 | */ 4 | package org.versly.rest.wsdoc.springmvc; 5 | 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RequestMethod; 8 | import org.versly.rest.wsdoc.DocumentationTraits; 9 | 10 | public class TraitsAnnotations { 11 | 12 | /** 13 | * A controller with all stable methods. 14 | */ 15 | public static class StableController { 16 | 17 | @RequestMapping(value = "/stable1", method = RequestMethod.GET) 18 | public void stable() { 19 | } 20 | } 21 | 22 | /** 23 | * A controller with all deprecated (implicit) methods. 24 | */ 25 | @DocumentationTraits(DocumentationTraits.DEPRECATED) 26 | public static class DeprecatedController { 27 | 28 | @RequestMapping(value = "/deprecated2", method = RequestMethod.GET) 29 | public void deprecated() { 30 | } 31 | } 32 | 33 | /** 34 | * A controller with some stable, deprecated, experimental, and deprecated experimental methods. 35 | */ 36 | public static class MixedController { 37 | 38 | @RequestMapping(value = "/stable3", method = RequestMethod.GET) 39 | public void stable() { 40 | } 41 | 42 | @DocumentationTraits(DocumentationTraits.DEPRECATED) 43 | @RequestMapping(value = "/deprecated3", method = RequestMethod.GET) 44 | public void deprecated() { 45 | } 46 | 47 | @DocumentationTraits(DocumentationTraits.EXPERIMENTAL) 48 | @RequestMapping(value = "/experimental3", method = RequestMethod.GET) 49 | public void experimental() { 50 | } 51 | 52 | @DocumentationTraits({ DocumentationTraits.DEPRECATED, DocumentationTraits.EXPERIMENTAL }) 53 | @RequestMapping(value = "/experimentaldeprecated3", method = RequestMethod.GET) 54 | public void experimentaldeprecated() { 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/jaxrs/MultiApiLevelDocs.java: -------------------------------------------------------------------------------- 1 | import org.versly.rest.wsdoc.DocumentationScope; 2 | import org.versly.rest.wsdoc.DocumentationRestApi; 3 | 4 | import javax.ws.rs.GET; 5 | import javax.ws.rs.Path; 6 | 7 | public class MultiApiLevelDocs { 8 | 9 | /** 10 | * This is the header documentation text for RestApi1. 11 | */ 12 | @DocumentationRestApi(id = "RestApi1", title = "The RestApi1 API", version = "v1", mount = "/restapi1/api/v1") 13 | @DocumentationScope("public") 14 | public class RestApi1 { 15 | 16 | /** 17 | * Returns the contents of the widgets collecion. 18 | * 19 | * @param id The widget identifier. 20 | */ 21 | @GET 22 | @Path("/widgets") 23 | public void getWidgets() { 24 | } 25 | } 26 | 27 | 28 | /** 29 | * This is the header documentation text for RestApi2. This API actually spans 30 | * multiple controller classes, RestApi2_A and RestApi2_B. This former declares 31 | * the title, version, and provides API level documentation, the latter just 32 | * indicates it has the same identifier as the former and it's resources will 33 | * be merged in. 34 | */ 35 | @DocumentationRestApi(id = "RestApi2", title = "The RestApi2 API", version = "v1", mount = "/restapi2/api/v1") 36 | @DocumentationScope("public") 37 | public class RestApi2_A { 38 | 39 | /** 40 | * Returns the contents of the gadgets collecion. 41 | * 42 | * @param id The gadget identifier. 43 | */ 44 | @GET 45 | @Path("/gadgets") 46 | public void getGadgets() { 47 | } 48 | } 49 | 50 | 51 | @DocumentationRestApi(id = "RestApi2", mount = "/restapi2/api/v1") 52 | @DocumentationScope("public") 53 | 54 | public class RestApi2_B { 55 | 56 | /** 57 | * Some description of the whirlygigs. 58 | * 59 | * @param id The whirlygig identifier. 60 | */ 61 | @GET 62 | @Path("/whirlygigs") 63 | public void getWhirlygigs() { 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /src/main/java/org/versly/rest/wsdoc/impl/JsonObject.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 TaskDock, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.versly.rest.wsdoc.impl; 18 | 19 | import java.io.Serializable; 20 | import java.util.*; 21 | 22 | public class JsonObject implements JsonType, Serializable { 23 | 24 | private List _fields = new ArrayList(); 25 | 26 | public JsonField addField(String fieldName, T value) { 27 | JsonField field = new JsonField(fieldName, value); 28 | if (_fields.contains(field)) 29 | throw new IllegalStateException("field " + fieldName + " is already defined"); 30 | _fields.add(field); 31 | return field; 32 | } 33 | 34 | public Collection getFields() { 35 | return _fields; 36 | } 37 | 38 | public class JsonField implements Serializable { 39 | 40 | private String fieldName; 41 | private T fieldType; 42 | private String commentText; 43 | 44 | public JsonField(String fieldName, T value) { 45 | this.fieldName = fieldName; 46 | this.fieldType = value; 47 | } 48 | 49 | public String getFieldName() { 50 | return fieldName; 51 | } 52 | 53 | public T getFieldType() { 54 | return fieldType; 55 | } 56 | 57 | public void setCommentText(String commentText) { 58 | this.commentText = commentText; 59 | } 60 | 61 | public String getCommentText() { 62 | return commentText; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/springmvc/MultiApiLevelDocs.java: -------------------------------------------------------------------------------- 1 | import org.springframework.web.bind.annotation.*; 2 | import org.versly.rest.wsdoc.DocumentationScope; 3 | import org.versly.rest.wsdoc.DocumentationRestApi; 4 | 5 | 6 | public class MultiApiLevelDocs { 7 | 8 | /** 9 | * This is the header documentation text for RestApi1. 10 | */ 11 | @DocumentationRestApi(id = "RestApi1", title = "The RestApi1 API", version = "v1", mount = "/restapi1/api/v1") 12 | @DocumentationScope("public") 13 | public class RestApi1 { 14 | 15 | /** 16 | * Returns the contents of the widgets collecion. 17 | * 18 | * @param id The widget identifier. 19 | */ 20 | @RequestMapping(value = "/widgets", method = RequestMethod.GET) 21 | public void getWidgets() { 22 | } 23 | } 24 | 25 | 26 | /** 27 | * This is the header documentation text for RestApi2. This API actually spans 28 | * multiple controller classes, RestApi2_A and RestApi2_B. This former declares 29 | * the title, version, and provides API level documentation, the latter just 30 | * indicates it has the same identifier as the former and it's resources will 31 | * be merged in. 32 | */ 33 | @DocumentationRestApi(id = "RestApi2", title = "The RestApi2 API", version = "v1", mount = "/restapi2/api/v1") 34 | @DocumentationScope("public") 35 | public class RestApi2_A { 36 | 37 | /** 38 | * Returns the contents of the gadgets collecion. 39 | * 40 | * @param id The gadget identifier. 41 | */ 42 | @RequestMapping(value = "/gadgets", method = RequestMethod.GET) 43 | public void getGadgets() { 44 | } 45 | } 46 | 47 | 48 | @DocumentationRestApi(id = "RestApi2", mount = "/restapi2/api/v1") 49 | @DocumentationScope("public") 50 | public class RestApi2_B { 51 | 52 | /** 53 | * Some description of the whirlygigs. 54 | * 55 | * @param id The whirlygig identifier. 56 | */ 57 | @RequestMapping(value = "/whirlygigs", method = RequestMethod.GET) 58 | public void getWhirlygigs() { 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /src/main/java/org/versly/rest/wsdoc/impl/Utils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 TaskDock, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.versly.rest.wsdoc.impl; 18 | 19 | import org.versly.rest.wsdoc.DocumentationRestApi; 20 | 21 | import java.util.HashMap; 22 | import java.util.Map; 23 | 24 | public class Utils { 25 | public static final String SERIALIZED_RESOURCE_LOCATION = "org.versly.rest.wsdoc.web-service-api.ser"; 26 | private static Map templateStrings = new HashMap(); 27 | 28 | public static String joinPaths(String lhs, String rhs) { 29 | // Mike Rawlins 2013-03-15 Don't append slash if right hand side is empty 30 | if (rhs == null || rhs.length() == 0) { 31 | return lhs; 32 | } 33 | 34 | while (lhs.endsWith("/")) 35 | lhs = lhs.substring(0, lhs.length() - 1); 36 | 37 | while (rhs.startsWith("/")) 38 | rhs = rhs.substring(1); 39 | 40 | // By default JAX-RS and Spring URI template matching ignore the presence of trailing slashes. 41 | while (rhs.endsWith("/")) 42 | rhs = rhs.substring(0, rhs.length() - 1); 43 | 44 | return lhs + "/" + rhs; 45 | } 46 | 47 | public static void addTemplateValue(String key, String value) { 48 | templateStrings.put(key, value); 49 | } 50 | 51 | private static String getTemplateValue(String key) { 52 | String value = templateStrings.get(key); 53 | return value != null ? value : ""; 54 | } 55 | 56 | public static String fillTemplate(String value) { 57 | if (value == null) return value; 58 | return value.replace(DocumentationRestApi.MOUNT_TEMPLATE, 59 | getTemplateValue(DocumentationRestApi.MOUNT_TEMPLATE)) 60 | .replace(DocumentationRestApi.ID_TEMPLATE, 61 | getTemplateValue(DocumentationRestApi.ID_TEMPLATE)) 62 | .replace(DocumentationRestApi.TITLE_TEMPLATE, 63 | getTemplateValue(DocumentationRestApi.TITLE_TEMPLATE)) 64 | .replace(DocumentationRestApi.VERSION_TEMPLATE, 65 | getTemplateValue(DocumentationRestApi.VERSION_TEMPLATE)); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/jaxrs/PublicationScopes.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This endpoint has private publication scope. 3 | */ 4 | 5 | import javax.ws.rs.GET; 6 | import javax.ws.rs.Path; 7 | import org.versly.rest.wsdoc.DocumentationScope; 8 | import org.versly.rest.wsdoc.RestApiMountPoint; 9 | 10 | public class PublicationScopes { 11 | 12 | /** 13 | * A controller with default publication scopes 14 | */ 15 | @RestApiMountPoint("/default") 16 | @Path("/api/v1") 17 | public static class DefaultController { 18 | 19 | @GET 20 | @Path("/public1") 21 | public void pub() { 22 | } 23 | } 24 | 25 | /** 26 | * A controller with some public and some private scopes 27 | */ 28 | @RestApiMountPoint("/mixed") 29 | @Path("/api/v1") 30 | public static class MixedController { 31 | 32 | @DocumentationScope(DocumentationScope.PUBLIC) 33 | @GET 34 | @Path("/public2") 35 | public void pub() { 36 | } 37 | 38 | @DocumentationScope(DocumentationScope.PRIVATE) 39 | @GET 40 | @Path("/private2") 41 | public void priv() { 42 | } 43 | } 44 | 45 | /** 46 | * A controller with all private scopes. 47 | */ 48 | @RestApiMountPoint("/private") 49 | @Path("/api/v1") 50 | public static class PrivateController { 51 | 52 | @DocumentationScope(DocumentationScope.PRIVATE) 53 | @GET 54 | @Path("/private3") 55 | public void priv() { 56 | } 57 | } 58 | 59 | /** 60 | * A controller that is private at the class level and containing one public method. 61 | */ 62 | @DocumentationScope(DocumentationScope.PRIVATE) 63 | @RestApiMountPoint("/classpriv") 64 | @Path("/api/v1") 65 | public static class ClassPrivateController { 66 | 67 | @GET 68 | @Path("/private4") 69 | public void priv() { 70 | } 71 | 72 | @DocumentationScope(DocumentationScope.PUBLIC) 73 | @GET 74 | @Path("/pubpriv4") 75 | public void pub() { 76 | } 77 | } 78 | 79 | /** 80 | * A controller that is private at the class level and containing one public method. 81 | */ 82 | @DocumentationScope(DocumentationScope.PUBLIC) 83 | @RestApiMountPoint("/classpriv") 84 | @Path("/api/v1") 85 | public static class ClassPublicController { 86 | 87 | @GET 88 | @Path("/public5/foo") 89 | public void pub1() { 90 | } 91 | 92 | @DocumentationScope(DocumentationScope.PRIVATE) 93 | @GET 94 | @Path("/pubpriv5/bar") 95 | public void pub2() { 96 | } 97 | } 98 | 99 | /** 100 | * A controller that is "experimental" scope only. 101 | */ 102 | @DocumentationScope("experimental") 103 | @RestApiMountPoint("/newshakystuff") 104 | public static class ExperimentalController { 105 | 106 | @GET 107 | @Path("/foo") 108 | public void foo() { 109 | } 110 | 111 | @GET 112 | @Path("/bar") 113 | public void bar() { 114 | } 115 | } 116 | 117 | } 118 | 119 | -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/springmvc/PublicationScopes.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This endpoint has private publication scope. 3 | */ 4 | 5 | import org.springframework.web.bind.annotation.RequestMapping; 6 | import org.springframework.web.bind.annotation.RequestMethod; 7 | import org.versly.rest.wsdoc.DocumentationScope; 8 | import org.versly.rest.wsdoc.RestApiMountPoint; 9 | 10 | public class PublicationScopes { 11 | 12 | /** 13 | * A controller with default publication scopes 14 | */ 15 | @RestApiMountPoint("/default") 16 | @RequestMapping("/api/v1") 17 | public static class DefaultController { 18 | 19 | @RequestMapping(value = "/public1", method = RequestMethod.GET) 20 | public void pub() { 21 | } 22 | } 23 | 24 | /** 25 | * A controller with some public and some private scopes 26 | */ 27 | @RestApiMountPoint("/mixed") 28 | @RequestMapping("/api/v1") 29 | public static class MixedController { 30 | 31 | @DocumentationScope(DocumentationScope.PUBLIC) 32 | @RequestMapping(value = "/public2", method = RequestMethod.GET) 33 | public void pub() { 34 | } 35 | 36 | @DocumentationScope(DocumentationScope.PRIVATE) 37 | @RequestMapping(value = "/private2", method = RequestMethod.GET) 38 | public void priv() { 39 | } 40 | } 41 | 42 | /** 43 | * A controller with all private scopes. 44 | */ 45 | @RestApiMountPoint("/private") 46 | @RequestMapping("/api/v1") 47 | public static class PrivateController { 48 | 49 | @DocumentationScope(DocumentationScope.PRIVATE) 50 | @RequestMapping(value = "/private3", method = RequestMethod.GET) 51 | public void priv() { 52 | } 53 | } 54 | 55 | /** 56 | * A controller that is private at the class level and containing one public method. 57 | */ 58 | @DocumentationScope(DocumentationScope.PRIVATE) 59 | @RestApiMountPoint("/classpriv") 60 | @RequestMapping("/api/v1") 61 | public static class ClassPrivateController { 62 | 63 | @RequestMapping(value = "/private4", method = RequestMethod.GET) 64 | public void priv() { 65 | } 66 | 67 | @DocumentationScope(DocumentationScope.PUBLIC) 68 | @RequestMapping(value = "/pubpriv4", method = RequestMethod.GET) 69 | public void pub() { 70 | } 71 | } 72 | 73 | /** 74 | * A controller that is private at the class level and containing one public method. 75 | */ 76 | @DocumentationScope(DocumentationScope.PUBLIC) 77 | @RestApiMountPoint("/classpriv") 78 | @RequestMapping("/api/v1") 79 | public static class ClassPublicController { 80 | 81 | @RequestMapping(value = "/public5/foo", method = RequestMethod.GET) 82 | public void pub1() { 83 | } 84 | 85 | @DocumentationScope(DocumentationScope.PRIVATE) 86 | @RequestMapping(value = "/pubpriv5/bar", method = RequestMethod.GET) 87 | public void pub2() { 88 | } 89 | } 90 | 91 | /** 92 | * A controller that is "experimental" scope only. 93 | */ 94 | @DocumentationScope("experimental") 95 | @RestApiMountPoint("/newshakystuff") 96 | public static class ExperimentalController { 97 | 98 | @RequestMapping(value = "/foo", method = RequestMethod.GET) 99 | public void foo() { 100 | } 101 | 102 | @RequestMapping(value = "/bar", method = RequestMethod.GET) 103 | public void bar() { 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/springmvc/Spring43ComposedAnnotationController.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc.springmvc; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import java.util.UUID; 6 | 7 | import javax.servlet.http.HttpServletRequest; 8 | 9 | import org.springframework.web.bind.annotation.DeleteMapping; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.PatchMapping; 12 | import org.springframework.web.bind.annotation.PathVariable; 13 | import org.springframework.web.bind.annotation.PostMapping; 14 | import org.springframework.web.bind.annotation.PutMapping; 15 | import org.springframework.web.bind.annotation.RequestBody; 16 | import org.springframework.web.bind.annotation.RequestParam; 17 | 18 | /** 19 | * Tests the new method-level composed variants of the @{@link org.springframework.web.bind.annotation.RequestMapping} annotations introduced in 20 | * Spring Framework 4.3. 21 | *

22 | * This tests the following annotations: 23 | *

    24 | *
  • {@link GetMapping}
  • 25 | *
  • {@link PostMapping}
  • 26 | *
  • {@link PutMapping}
  • 27 | *
  • {@link DeleteMapping}
  • 28 | *
  • {@link PatchMapping}
  • 29 | *
30 | * 31 | * @author Sidharth Mishra 32 | * @see https://docs.spring.io/spring-framework/docs/4.3.1.RELEASE/spring-framework-reference/htmlsingle/#mvc-ann-requestmapping-composed 33 | */ 34 | public class Spring43ComposedAnnotationController { 35 | 36 | /** 37 | * Some description for this GET method. 38 | * 39 | * @param id The ID. 40 | * @param pageNbr The page number. 41 | * @return some map from string to string. 42 | */ 43 | @GetMapping("/theGetMethod") 44 | public Map theGetMethod(@RequestParam("id") UUID id, @RequestParam("pageNbr") int pageNbr) { 45 | 46 | return new HashMap<>(); 47 | } 48 | 49 | /** 50 | * Some description for this POST method. 51 | * 52 | * @param theReqBody the request-body. 53 | * @return the ID of item created. 54 | */ 55 | @PostMapping("/thePostMethod") 56 | public UUID thePostMethod(@RequestBody Map theReqBody) { 57 | 58 | return null; 59 | } 60 | 61 | /** 62 | * Some description for this PUT method. 63 | * 64 | * @param id The ID of the item to update. 65 | * @param theReqBody the request-body containing data to update. 66 | * @param httpServletRequest the raw {@link HttpServletRequest} instance for more contextual data. 67 | * @return the updated item's ID. 68 | */ 69 | @PutMapping("/thePutMethod/{id}") 70 | public UUID thePutMethod(@PathVariable("id") UUID id, @RequestBody Map theReqBody, HttpServletRequest httpServletRequest) { 71 | 72 | return null; 73 | } 74 | 75 | /** 76 | * Some description for this DELETE method. 77 | * 78 | * @param id The ID of the item to delete. 79 | */ 80 | @DeleteMapping("/theDeleteMethod/{id}") 81 | public void theDeleteMethod(@PathVariable("id") UUID id) { 82 | 83 | } 84 | 85 | /** 86 | * Some description for this PATCH method. 87 | * 88 | * @param id The ID of the item to patch. 89 | * @param theReqBody The request-body containing data to path. 90 | * @return the ID of the item that was successfully patched. 91 | */ 92 | @PatchMapping("/thePatchMethod/{id}") 93 | public UUID thePatchMethod(@PathVariable("id") UUID id, @RequestBody Map theReqBody) { 94 | 95 | return null; 96 | } 97 | } -------------------------------------------------------------------------------- /src/test/java/org/versly/rest/wsdoc/JaxRSRestAnnotationProcessorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 TaskDock, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.versly.rest.wsdoc; 18 | 19 | import freemarker.template.TemplateException; 20 | import org.testng.AssertJUnit; 21 | import org.testng.annotations.BeforeClass; 22 | import org.testng.annotations.Test; 23 | 24 | import java.io.File; 25 | import java.io.IOException; 26 | import java.net.URISyntaxException; 27 | 28 | public class JaxRSRestAnnotationProcessorTest extends AbstractRestAnnotationProcessorTest { 29 | 30 | @BeforeClass 31 | public void setUp() throws IOException, ClassNotFoundException, URISyntaxException, TemplateException { 32 | super.setUp(); 33 | } 34 | 35 | @Test 36 | public void assertClassPathOnly() { 37 | for (String format : _outputFormats) { 38 | processResource("ClassPathOnly.java", format, "all"); 39 | AssertJUnit.assertTrue( 40 | "expected 'classPathOnly' in doc string; got: \n" + defaultApiOutput, 41 | defaultApiOutput.contains("classPathOnly")); 42 | if (format.equals("html")) { 43 | AssertJUnit.assertTrue( 44 | "expected 'classPathOnly_GET' in doc string; got: \n" + defaultApiOutput, 45 | defaultApiOutput.contains("classPathOnly_GET")); 46 | } 47 | } 48 | } 49 | 50 | @Test 51 | public void assertNoPath() { 52 | for (String format : _outputFormats) { 53 | processResource("NoPath.java", format, "all"); 54 | if (format.equals("html")) { 55 | AssertJUnit.assertTrue( 56 | "expected '_GET' in doc string; got: \n" + defaultApiOutput, 57 | defaultApiOutput.contains("_GET")); 58 | } 59 | if (format.equals("raml")) { 60 | AssertJUnit.assertTrue( 61 | "expected 'baseUri: /' in doc string; got: \n" + defaultApiOutput, 62 | defaultApiOutput.contains("baseUri: /")); 63 | AssertJUnit.assertTrue( 64 | "expected 'get:' in doc string; got: \n" + defaultApiOutput, 65 | defaultApiOutput.contains("get:")); 66 | } 67 | } 68 | } 69 | 70 | @Test 71 | public void assertRequestBody() { 72 | processResource("PostWithRequestBody.java", "html", "all"); 73 | 74 | AssertJUnit.assertTrue("expected two Request Body sections; got: \n" + defaultApiOutput, 75 | defaultApiOutput.split("Request Body").length == 3); 76 | } 77 | 78 | public static void main(String[] args) throws IOException, URISyntaxException { 79 | File dir = new File(args[0]); 80 | for (int i = 1; i < args.length; i++) { 81 | runAnnotationProcessor(dir, 82 | args[i].substring(0, args[i].lastIndexOf('/')), 83 | args[i].substring(args[i].lastIndexOf('/'))); 84 | } 85 | } 86 | 87 | @Override 88 | protected String getPackageToTest() { 89 | return "jaxrs"; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/org/versly/rest/wsdoc/impl/SpringMVCRestImplementationSupport.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc.impl; 2 | 3 | import org.springframework.web.bind.annotation.PathVariable; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | import org.springframework.web.bind.annotation.RequestParam; 6 | import org.versly.rest.wsdoc.AnnotationProcessor; 7 | 8 | import java.lang.annotation.Annotation; 9 | import java.util.Arrays; 10 | import java.util.HashSet; 11 | import java.util.Set; 12 | 13 | import javax.lang.model.element.ExecutableElement; 14 | import javax.lang.model.element.TypeElement; 15 | import javax.lang.model.element.VariableElement; 16 | 17 | public class SpringMVCRestImplementationSupport implements AnnotationProcessor.RestImplementationSupport { 18 | 19 | @Override 20 | public Class getMappingAnnotationType() { 21 | return RequestMapping.class; 22 | } 23 | 24 | @Override 25 | public Set> getExtendedMappingAnnotationTypes() { 26 | return new HashSet>(Arrays.asList(RequestMapping.class)); 27 | } 28 | 29 | @Override 30 | public String[] getRequestPaths(ExecutableElement executableElement, TypeElement contextClass) { 31 | RequestMapping anno = executableElement.getAnnotation(RequestMapping.class); 32 | return requestPathsForAnnotation(anno); 33 | } 34 | 35 | @Override 36 | public String[] getRequestPaths(TypeElement cls) { 37 | RequestMapping clsAnno = cls.getAnnotation(RequestMapping.class); 38 | return requestPathsForAnnotation(clsAnno); 39 | } 40 | 41 | private String[] requestPathsForAnnotation(RequestMapping clsAnno) { 42 | if (clsAnno == null) 43 | return new String[0]; 44 | else 45 | return clsAnno.value(); 46 | } 47 | 48 | @Override 49 | public String getRequestMethod(ExecutableElement executableElement, TypeElement contextClass) { 50 | RequestMapping anno = executableElement.getAnnotation(RequestMapping.class); 51 | if (anno.method().length != 1) 52 | throw new IllegalStateException(String.format( 53 | "The RequestMapping annotation for %s.%s is not parseable. Exactly one request method (GET/POST/etc) is required.", 54 | contextClass.getQualifiedName(), executableElement.getSimpleName())); 55 | else 56 | return anno.method()[0].name(); 57 | } 58 | 59 | @Override 60 | public String getPathVariable(VariableElement var) { 61 | PathVariable pathVar = var.getAnnotation(PathVariable.class); 62 | return pathVar == null ? null : pathVar.value(); 63 | } 64 | 65 | @Override 66 | public String getRequestParam(VariableElement var) { 67 | RequestParam reqParam = var.getAnnotation(RequestParam.class); 68 | return reqParam == null ? null : reqParam.value(); 69 | } 70 | 71 | /** 72 | * Return whether this variable is an un-annotated POJO. 73 | * This catches the case where a model is bound to directly. We explicitly exclude spring classes so that we don't look at the 74 | * magical spring bound parameters like Errors or ModelMap 75 | */ 76 | @Override 77 | public String getPojoRequestParam(VariableElement var) { 78 | if (getRequestParam(var) == null && getPathVariable(var) == null && !var.asType().toString().startsWith("org.springframework")) { 79 | return ""; // No annotations to parse 80 | } else { 81 | return null; 82 | } 83 | 84 | } 85 | 86 | 87 | @Override 88 | public boolean isRequestBody(VariableElement var) { 89 | return var.getAnnotation(org.springframework.web.bind.annotation.RequestBody.class) != null; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/org/versly/rest/wsdoc/impl/JaxRSRestImplementationSupport.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc.impl; 2 | 3 | import org.versly.rest.wsdoc.AnnotationProcessor; 4 | 5 | import java.lang.annotation.Annotation; 6 | import java.util.ArrayList; 7 | import java.util.Arrays; 8 | import java.util.HashSet; 9 | import java.util.List; 10 | import java.util.Set; 11 | 12 | import javax.lang.model.element.ExecutableElement; 13 | import javax.lang.model.element.TypeElement; 14 | import javax.lang.model.element.VariableElement; 15 | import javax.ws.rs.DELETE; 16 | import javax.ws.rs.GET; 17 | import javax.ws.rs.HEAD; 18 | import javax.ws.rs.OPTIONS; 19 | import javax.ws.rs.POST; 20 | import javax.ws.rs.PUT; 21 | import javax.ws.rs.Path; 22 | import javax.ws.rs.PathParam; 23 | import javax.ws.rs.QueryParam; 24 | 25 | public class JaxRSRestImplementationSupport implements AnnotationProcessor.RestImplementationSupport { 26 | @Override 27 | public Class getMappingAnnotationType() { 28 | return Path.class; 29 | } 30 | 31 | @Override 32 | public Set> getExtendedMappingAnnotationTypes() { 33 | return new HashSet>(Arrays.asList(Path.class, GET.class, PUT.class, POST.class, DELETE.class, HEAD.class, OPTIONS.class)); 34 | } 35 | 36 | @Override 37 | public String[] getRequestPaths(ExecutableElement executableElement, TypeElement contextClass) { 38 | Path anno = executableElement.getAnnotation(Path.class); 39 | if (anno == null) { 40 | for (Class a : getExtendedMappingAnnotationTypes()) { 41 | if (executableElement.getAnnotation(a) != null) { 42 | return new String[] { "" }; 43 | } 44 | } 45 | throw new IllegalStateException(String.format( 46 | "The Path annotation for %s.%s is not parseable. Exactly one value is required.", 47 | contextClass.getQualifiedName(), executableElement.getSimpleName())); 48 | } else 49 | return new String[] { anno.value() }; 50 | } 51 | 52 | @Override 53 | public String[] getRequestPaths(TypeElement cls) { 54 | Path clsAnno = cls.getAnnotation(Path.class); 55 | if (clsAnno == null) 56 | return new String[0]; 57 | else 58 | return new String[] { clsAnno.value() }; 59 | } 60 | 61 | @Override 62 | public String getRequestMethod(ExecutableElement executableElement, TypeElement contextClass) { 63 | List methods = new ArrayList(); 64 | 65 | gatherMethod(executableElement, methods, GET.class); 66 | gatherMethod(executableElement, methods, PUT.class); 67 | gatherMethod(executableElement, methods, POST.class); 68 | gatherMethod(executableElement, methods, DELETE.class); 69 | gatherMethod(executableElement, methods, HEAD.class); 70 | gatherMethod(executableElement, methods, OPTIONS.class); 71 | 72 | if (methods.size() != 1) 73 | throw new IllegalStateException(String.format( 74 | "The method annotation for %s.%s is not parseable. Exactly one request method (GET/POST/PUT/DELETE/HEAD/OPTIONS) is required. Found: %s", 75 | contextClass.getQualifiedName(), executableElement.getSimpleName(), methods)); 76 | 77 | return methods.get(0); 78 | } 79 | 80 | private void gatherMethod(ExecutableElement executableElement, List methods, Class anno) { 81 | if (executableElement.getAnnotation(anno) != null) { 82 | methods.add(anno.getSimpleName()); 83 | } 84 | } 85 | 86 | @Override 87 | public String getPathVariable(VariableElement var) { 88 | PathParam param = var.getAnnotation(PathParam.class); 89 | return param == null ? null : param.value(); 90 | } 91 | 92 | @Override 93 | public String getRequestParam(VariableElement var) { 94 | QueryParam param = var.getAnnotation(QueryParam.class); 95 | return param == null ? null : param.value(); 96 | } 97 | 98 | @Override 99 | public String getPojoRequestParam(VariableElement var) { 100 | return null; // Not sure if this concept exists with JaxRS 101 | } 102 | 103 | @Override 104 | public boolean isRequestBody(VariableElement var) { 105 | return var.getAnnotationMirrors().size() == 0; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/org/versly/rest/wsdoc/impl/JsonPrimitive.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 TaskDock, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.versly.rest.wsdoc.impl; 18 | 19 | import java.io.Serializable; 20 | import java.math.BigDecimal; 21 | import java.math.BigInteger; 22 | import java.net.URI; 23 | import java.net.URL; 24 | import java.sql.Timestamp; 25 | import java.util.Date; 26 | import java.util.HashMap; 27 | import java.util.List; 28 | import java.util.Map; 29 | import java.util.UUID; 30 | 31 | import org.joda.time.DateTime; 32 | import org.joda.time.LocalDate; 33 | 34 | public class JsonPrimitive implements JsonType, Serializable { 35 | 36 | private static final Map _primitiveTypeNamesByJavaTypeName = new HashMap(); 37 | 38 | static { 39 | _primitiveTypeNamesByJavaTypeName.put(Object.class.getName(), "object"); 40 | _primitiveTypeNamesByJavaTypeName.put(String.class.getName(), "string"); 41 | 42 | _primitiveTypeNamesByJavaTypeName.put(boolean.class.getName(), "boolean"); 43 | _primitiveTypeNamesByJavaTypeName.put(Boolean.class.getName(), "boolean"); 44 | _primitiveTypeNamesByJavaTypeName.put(byte.class.getName(), "byte"); 45 | _primitiveTypeNamesByJavaTypeName.put(Byte.class.getName(), "byte"); 46 | _primitiveTypeNamesByJavaTypeName.put(char.class.getName(), "char"); 47 | _primitiveTypeNamesByJavaTypeName.put(Character.class.getName(), "char"); 48 | _primitiveTypeNamesByJavaTypeName.put(double.class.getName(), "double"); 49 | _primitiveTypeNamesByJavaTypeName.put(Double.class.getName(), "double"); 50 | _primitiveTypeNamesByJavaTypeName.put(BigDecimal.class.getName(), "double"); 51 | _primitiveTypeNamesByJavaTypeName.put(float.class.getName(), "float"); 52 | _primitiveTypeNamesByJavaTypeName.put(Float.class.getName(), "float"); 53 | _primitiveTypeNamesByJavaTypeName.put(int.class.getName(), "integer"); 54 | _primitiveTypeNamesByJavaTypeName.put(Integer.class.getName(), "integer"); 55 | _primitiveTypeNamesByJavaTypeName.put(BigInteger.class.getName(), "integer"); 56 | _primitiveTypeNamesByJavaTypeName.put(long.class.getName(), "long"); 57 | _primitiveTypeNamesByJavaTypeName.put(Long.class.getName(), "long"); 58 | _primitiveTypeNamesByJavaTypeName.put(short.class.getName(), "short"); 59 | _primitiveTypeNamesByJavaTypeName.put(Short.class.getName(), "short"); 60 | _primitiveTypeNamesByJavaTypeName.put(void.class.getName(), "void"); 61 | _primitiveTypeNamesByJavaTypeName.put(Void.class.getName(), "void"); 62 | 63 | _primitiveTypeNamesByJavaTypeName.put(URL.class.getName(), "url"); 64 | _primitiveTypeNamesByJavaTypeName.put(URI.class.getName(), "url"); 65 | _primitiveTypeNamesByJavaTypeName.put(UUID.class.getName(), "uuid"); 66 | 67 | _primitiveTypeNamesByJavaTypeName.put(Date.class.getName(), "timestamp"); 68 | _primitiveTypeNamesByJavaTypeName.put(java.sql.Date.class.getName(), "date"); 69 | _primitiveTypeNamesByJavaTypeName.put(java.sql.Time.class.getName(), "time"); 70 | _primitiveTypeNamesByJavaTypeName.put(Timestamp.class.getName(), "timestamp"); 71 | _primitiveTypeNamesByJavaTypeName.put(DateTime.class.getName(), "datetime"); 72 | _primitiveTypeNamesByJavaTypeName.put(LocalDate.class.getName(), "date"); 73 | } 74 | 75 | private String typeName; 76 | private List restrictions; 77 | 78 | public JsonPrimitive(String typeName) { 79 | if (isPrimitive(typeName)) 80 | this.typeName = _primitiveTypeNamesByJavaTypeName.get(typeName); 81 | else 82 | this.typeName = typeName; 83 | } 84 | 85 | /** 86 | * Whether or not the passed-in type name should be considered a primitive for the purposes 87 | * of the generated documentation (i.e., a terminal node in the rendered output). 88 | */ 89 | public static boolean isPrimitive(String typeName) { 90 | return _primitiveTypeNamesByJavaTypeName.containsKey(typeName); 91 | } 92 | 93 | public String getTypeName() { 94 | return typeName; 95 | } 96 | 97 | public void setRestrictions(List restrictions) { 98 | this.restrictions = restrictions; 99 | } 100 | 101 | public List getRestrictions() { 102 | return restrictions; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/jaxrs/RestDocEndpoint.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 TaskDock, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.versly.rest.wsdoc.jaxrs; 18 | 19 | import org.versly.rest.wsdoc.RestApiMountPoint; 20 | import org.versly.rest.wsdoc.model.ParameterizedTypeReferrer; 21 | 22 | import javax.servlet.http.HttpServletRequest; 23 | import javax.ws.rs.*; 24 | import javax.ws.rs.core.Context; 25 | import java.util.Date; 26 | import java.util.List; 27 | import java.util.Map; 28 | import java.util.UUID; 29 | 30 | @RestApiMountPoint("/mount") 31 | @Path("/api/v1") 32 | public class RestDocEndpoint { 33 | 34 | /** 35 | * This is an exciting JavaDoc comment 36 | */ 37 | @GET 38 | @Path("foo/{dateParam}/{intParam}") 39 | public Map> methodWithJavadocAndInterestingArgs( 40 | @Context HttpServletRequest req, 41 | @PathParam("dateParam") Date dp, 42 | @PathParam("intParam") int intParam) { 43 | return null; 44 | } 45 | 46 | @GET 47 | @Path("voidreturn1") 48 | public void methodWithVoidReturn(@Context HttpServletRequest req) { 49 | } 50 | 51 | @GET 52 | @Path("voidreturn2") 53 | public void methodWithVoidReturnAndParams(@Context HttpServletRequest req, 54 | @QueryParam("param0") int p0, 55 | @QueryParam("param1") int param1) { 56 | } 57 | 58 | @GET 59 | @Path("uuidReturn") 60 | public UUID uuidReturn() { 61 | return null; 62 | } 63 | 64 | @POST 65 | @Path("recursiveParam") 66 | public void recursiveParam(@Context HttpServletRequest req, 67 | @QueryParam("recursive") ValueWithRecursion recursive) 68 | { 69 | } 70 | 71 | @GET 72 | @Path("recursiveReturn") 73 | public ValueWithRecursion recursiveReturn(@Context HttpServletRequest req) 74 | { 75 | return null; 76 | } 77 | 78 | @GET 79 | @Path("recursiveListReturn") 80 | public ValueWithListRecursion recursiveListReturn(HttpServletRequest req) 81 | { 82 | return null; 83 | } 84 | 85 | @GET 86 | @Path("endpointWithParameterizedType") 87 | public ParameterizedTypeReferrer endpointWithParameterizedType() { 88 | return null; 89 | } 90 | 91 | @POST 92 | @Path("recursiveListRequestBody") 93 | public void recursiveListReturn(ValueWithListRecursion body) 94 | { 95 | } 96 | 97 | public class ExcitingReturnValue { 98 | /** 99 | * The exciting return value's date! 100 | */ 101 | public Date getDate() { 102 | return null; 103 | } 104 | 105 | public void setDate(Date date) { // here to exercise a setter bug 106 | } 107 | } 108 | 109 | public class ValueWithRecursion { 110 | public ValueWithRecursion getOther() { 111 | return null; 112 | } 113 | 114 | public void setOther(ValueWithRecursion other) { 115 | } 116 | } 117 | 118 | public class ValueWithListRecursion { 119 | 120 | public String getStringValue() { 121 | return null; 122 | } 123 | 124 | public void setStringValue(String val) { 125 | } 126 | 127 | public List getOthers() { 128 | return null; 129 | } 130 | 131 | public void setOthers(List others) { 132 | } 133 | } 134 | 135 | @GET 136 | @Path("things/{id}") 137 | public void getThing(@Context HttpServletRequest req, 138 | @PathParam("id") String id) { 139 | } 140 | 141 | @POST 142 | @Path("things/{id}") 143 | public void deleteThing(@Context HttpServletRequest req, 144 | @PathParam("id") String id) { 145 | } 146 | 147 | // this following widges/{id1}/gizmos endpoints are for verifying issue #29 148 | 149 | /** 150 | * This endpoint creates things. 151 | * @param id1 The widget identifier. 152 | */ 153 | @POST 154 | @Path("widgets/{id1}/gizmos") 155 | public void createThing(@PathParam("id1") String id1) { 156 | } 157 | 158 | /** 159 | * This endpoint gets things. 160 | * @param id1 The widget identifier. 161 | * @param id2 The gizmo identifier. 162 | */ 163 | @GET 164 | @Path("widgets/{id1}/gizmos/{id2}") 165 | public void getThing(@PathParam("id1") String id1, @PathParam("id2") String id2) { 166 | } 167 | 168 | /** 169 | * This endpoint Deletes things. 170 | * @param id1 The widget identifier. 171 | * @param id2 The gizmo identifier. 172 | */ 173 | @DELETE 174 | @Path("widgets/{id1}/gizmos/{id2}") 175 | public void deleteThing(@PathParam("id1") String id1, @PathParam("id2") String id2) { 176 | } 177 | 178 | enum Colors { 179 | RED, 180 | GREEN, 181 | BLUE 182 | }; 183 | 184 | /** 185 | * This endpoint tests query parameters that are enums. 186 | * 187 | * @param color Whirlygig color of interest. 188 | */ 189 | @GET 190 | @Path("/whirlygigs") 191 | public void getWhirlygig(@QueryParam("color") Colors color) { 192 | } 193 | 194 | /** 195 | * This endpoint tests uri path parameters that are enums. 196 | * 197 | * @param color The color of interest. 198 | */ 199 | @GET 200 | @Path("/colors/{color}") 201 | public void getColor(@PathParam("color") Colors color) { 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/main/resources/org/versly/rest/wsdoc/RamlDocumentation.ftl: -------------------------------------------------------------------------------- 1 | <#-- @ftlvariable name="api" type="org.versly.rest.wsdoc.impl.RestDocumentation.RestApi" --> 2 | #%RAML 0.8 3 | --- 4 | <#if api.apiTitle??> 5 | title: ${api.apiTitle} 6 | <#else> 7 | title: 8 | 9 | <#if api.apiVersion??> 10 | version: ${api.apiVersion} 11 | 12 | <#if api.mount??> 13 | baseUri: ${api.mount} 14 | 15 | <#if api.apiDocumentation??> 16 | documentation: 17 | - title: Overview 18 | content: | 19 | ${api.indentedApiDocumentationText(10)} 20 | 21 | 22 | securitySchemes: 23 | - oauth_2_0: 24 | type: OAuth 2.0 25 | settings: 26 | accessTokenUri: TBD 27 | authorizationUri: TBD 28 | 29 | <#if api.getTraits()?size gt 0> 30 | traits: 31 | ${api.indentedApiTraits(4)} 32 | 33 | 34 | <#list api.resources as resource> 35 | <#if !resource.parent??> 36 | <@write_resource resource=resource depth=0/> 37 | 38 | 39 | 40 | 41 | <#-- 42 | -- write out a RAML resource path. 43 | --> 44 | <#macro write_resource resource depth> 45 | <#if (depth > 0)><#list 1..depth as i> ${resource.pathLeaf}: 46 | <@write_uri_parameters resource=resource depth=depth+4 /> 47 | <@write_resource_parts resource=resource depth=depth+4/> 48 | 49 | 50 | 51 | 52 | <#-- 53 | -- write out methods of the RAML resource and then the sub-resources 54 | --> 55 | <#macro write_resource_parts resource depth> 56 | <#list resource.requestMethodDocs as methodDoc> 57 | <@write_method methodDoc=methodDoc depth=depth/> 58 | 59 | 60 | <#list resource.children as child> 61 | <@write_resource resource=child depth=depth/> 62 | 63 | 64 | 65 | 66 | 67 | <#-- 68 | -- write out the method name, description, parameters, body, and response for the given method 69 | --> 70 | <#macro write_method methodDoc depth> 71 | <#list 1..depth as i> ${methodDoc.requestMethod?lower_case}: 72 | <#if methodDoc.commentText??> 73 | <@write_description methodDoc=methodDoc depth=depth+4/> 74 | 75 | 76 | <#if methodDoc.authScopesAsString??> 77 | <@write_auth_scopes methodDoc=methodDoc depth=depth+4/> 78 | 79 | 80 | <@write_traits methodDoc=methodDoc depth=depth+4/> 81 | 82 | <@write_parameters methodDoc=methodDoc depth=depth+4/> 83 | 84 | <#if methodDoc.requestSchema??> 85 | <@write_body schema=methodDoc.requestSchema example=methodDoc.requestExample depth=depth+4/> 86 | 87 | 88 | <@write_response methodDoc=methodDoc depth=depth+4/> 89 | 90 | 91 | <#-- 92 | -- write out method description 93 | --> 94 | <#macro write_description methodDoc depth> 95 | <#list 1..depth as i> description: | 96 | ${methodDoc.indentedCommentText(depth+4)} 97 | 98 | 99 | <#-- 100 | -- write out method auth scopes 101 | --> 102 | <#macro write_auth_scopes methodDoc depth> 103 | <#list 1..depth as i> securedBy: [ oauth_2_0: { scopes: ${methodDoc.authScopesAsString} } ] 104 | 105 | 106 | <#-- 107 | -- write out method traits 108 | --> 109 | <#macro write_traits methodDoc depth> 110 | <#list 1..depth as i> is: ${methodDoc.traitsAsString} 111 | 112 | 113 | <#-- 114 | -- write out all URI parameters for a method 115 | --> 116 | <#macro write_uri_parameters resource depth> 117 | <#list 1..depth as i> uriParameters: 118 | <#assign fields=resource.resourceUrlSubstitutions.fields> 119 | <#list fields?keys as key> 120 | <@write_parameter fields=fields key=key depth=depth+4/> 121 | 122 | 123 | 124 | <#-- 125 | -- write out all query parameters for a method 126 | --> 127 | <#macro write_parameters methodDoc depth> 128 | <#list 1..depth as i> queryParameters: 129 | <#assign fields=methodDoc.urlParameters.fields> 130 | <#list fields?keys as key> 131 | <@write_parameter fields=fields key=key depth=depth+4/> 132 | 133 | 134 | 135 | 136 | <#-- 137 | -- write out a single query parameter name and type 138 | --> 139 | <#macro write_parameter fields key depth> 140 | <#list 1..depth as i> ${key}: 141 | <@write_parameter_info field=fields[key] depth=depth+4/> 142 | 143 | 144 | 145 | <#-- 146 | -- write out a single url parameter type 147 | --> 148 | <#macro write_parameter_info field depth> 149 | <#list 1..depth as i> description: | 150 | <#list 1..depth as i> ${field.fieldDescription!} 151 | <#if field.fieldType.class.name == "org.versly.rest.wsdoc.impl.JsonPrimitive"> 152 | <#list 1..depth as i> type: <@write_raml_type field.fieldType.typeName/> 153 | <#if field.fieldType.restrictions??><#t> 154 | <#list 1..depth as i> enum: [ <#list field.fieldType.restrictions as restricton>${restricton}<#if restricton_has_next>, ] 155 | <#else> 156 | <#list 1..depth as i> type: string 157 | 158 | 159 | 160 | 161 | <#-- 162 | -- write out request or response body 163 | --> 164 | <#macro write_body schema example depth> 165 | <#list 1..depth as i> body: 166 | <@write_body_media_type schema=schema example=example depth=depth+4/> 167 | 168 | 169 | 170 | <#-- 171 | -- write out media type of request body 172 | -- (TODO: allow for more interesting media types) 173 | --> 174 | <#macro write_body_media_type schema example depth> 175 | <#list 1..depth as i> application/json: 176 | <@write_body_schema schema=schema depth=depth+4/> 177 | <@write_body_example example=example depth=depth+4/> 178 | 179 | 180 | 181 | <#-- 182 | -- write out all url parameters for a method 183 | --> 184 | <#macro write_body_schema schema depth> 185 | <#list 1..depth as i> schema: | 186 | <#list 1..depth as i> ${schema?trim} 187 | 188 | 189 | <#macro write_body_example example depth> 190 | <#list 1..depth as i> example: | 191 | <#list 1..depth as i> ${example?trim} 192 | 193 | 194 | <#-- 195 | -- write out response for a method 196 | --> 197 | <#macro write_response methodDoc depth> 198 | <#list 1..depth as i> responses: 199 | <@write_response_code methodDoc=methodDoc depth=depth+4/> 200 | 201 | 202 | 203 | <#-- 204 | -- write out response code for a method 205 | --> 206 | <#macro write_response_code methodDoc depth> 207 | <#list 1..depth as i> 200: 208 | <#if methodDoc.responseSchema??> 209 | <@write_body schema=methodDoc.responseSchema example=methodDoc.responseExample depth=depth+4/> 210 | 211 | 212 | 213 | 214 | <#-- 215 | -- RAML has a limited set of types for URI template parameters 216 | --> 217 | <#macro write_raml_type type> 218 | <#if type == "float" || type == "double">number 219 | <#elseif type == "integer" || type == "long" || type == "short" || type == "byte" >integer 220 | <#elseif type == "date" || type == "timestamp" || type == "time">date 221 | <#elseif type == "boolean">boolean 222 | <#else>string 223 | 224 | -------------------------------------------------------------------------------- /src/test/java/org/versly/rest/wsdoc/SpringMVCRestAnnotationProcessorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 TaskDock, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.versly.rest.wsdoc; 18 | 19 | import freemarker.template.TemplateException; 20 | import org.testng.Assert; 21 | import org.testng.AssertJUnit; 22 | import org.testng.annotations.BeforeClass; 23 | import org.testng.annotations.Test; 24 | 25 | import java.io.File; 26 | import java.io.IOException; 27 | import java.net.URISyntaxException; 28 | 29 | public class SpringMVCRestAnnotationProcessorTest extends AbstractRestAnnotationProcessorTest { 30 | 31 | @BeforeClass 32 | public void setUp() throws IOException, ClassNotFoundException, URISyntaxException, TemplateException { 33 | super.setUp(); 34 | } 35 | 36 | @Override 37 | protected String getPackageToTest() { 38 | return "springmvc"; 39 | } 40 | 41 | @Test 42 | public void assertMultipart() { 43 | processResource("RestDocEndpoint.java", "html", "all"); 44 | AssertJUnit.assertTrue("expected multipart info docs; got: \n" + defaultApiOutput, 45 | defaultApiOutput.contains("Note: this endpoint expects a multipart")); 46 | } 47 | 48 | @Test 49 | public void processControllerThatReturnsDomainObjectWithGenericParentsExpectsSuccess() { 50 | processResource("genericdomain/ChildController.java", "html", "all"); 51 | AssertJUnit.assertTrue("expected firstGrandparentField and secondGrandparentField in docs; got: \n" + defaultApiOutput, 52 | defaultApiOutput.contains(">firstGrandparentField<") && defaultApiOutput.contains(">secondGrandparentField<") 53 | && defaultApiOutput.contains(">parentField<") && defaultApiOutput.contains(">childField<") 54 | ); 55 | } 56 | 57 | @Test 58 | public void processControllerThatReturnsGenericDomainObjectExpectsSuccess() { 59 | processResource("genericdomain/ParentController.java", "html", "all"); 60 | AssertJUnit.assertTrue("expected parentField in docs; got: \n" + defaultApiOutput, 61 | defaultApiOutput.contains(">parentField<")); 62 | } 63 | 64 | @Test 65 | public void assertQueryParams() { 66 | processResource("RestDocEndpoint.java", "html", "all"); 67 | AssertJUnit.assertTrue("expected queryParam1 and queryParam2 in docs; got: \n" + defaultApiOutput, 68 | defaultApiOutput.contains(">queryParamVal1<") && defaultApiOutput.contains(">queryParamVal2<")); 69 | } 70 | 71 | @Test 72 | public void processControllerThatReturnsEnumSetExpectsSuccess() { 73 | processResource("EnumSetController.java", "html", "all"); 74 | AssertJUnit.assertTrue("expected enumsets in docs; got: \n" + defaultApiOutput, 75 | defaultApiOutput.contains(">myEnumSet<") && defaultApiOutput.contains(">myEnum<") && defaultApiOutput.contains(">one of [ TEST1, TEST2 ]<")); 76 | } 77 | 78 | @Test 79 | public void multipleBindingsForOneEndpoint() { 80 | processResource("RestDocEndpoint.java", "html", "all"); 81 | AssertJUnit.assertTrue("expected multiple-bindings-a and multiple-bindings-b in docs; got: \n" + defaultApiOutput, 82 | defaultApiOutput.contains("multiple-bindings-a<") && defaultApiOutput.contains("multiple-bindings-b<")); 83 | } 84 | 85 | @Test 86 | public void assertOptionalIsNotTraversedInto() { 87 | processResource("RestDocEndpoint.java", "raml", "all"); 88 | AssertJUnit.assertFalse( 89 | "'present' field (member field of Optional class) should not be in results", 90 | defaultApiOutput.contains("present")); 91 | AssertJUnit.assertFalse( 92 | "'Optional<' should not be in results", 93 | defaultApiOutput.contains("Optional<")); 94 | AssertJUnit.assertTrue(defaultApiOutput, 95 | defaultApiOutput.contains("optionalfield")); 96 | } 97 | 98 | @Test 99 | public void assertAllMethods() { 100 | super.assertAllMethods(); 101 | for (String format : _outputFormats) { 102 | processResource("AllMethods.java", format, "all"); 103 | AssertJUnit.assertTrue( 104 | "expected 'allMethodsPatch' in doc string; got: \n" + defaultApiOutput, 105 | defaultApiOutput.contains("allMethodsPatch")); 106 | } 107 | } 108 | 109 | @Test 110 | public void assertWildcardTypes() { 111 | processResource("genericdomain/WildcardController.java", "html", "all"); 112 | AssertJUnit.assertTrue("expected 'wildcardType' in docs; got: \n" + defaultApiOutput, 113 | defaultApiOutput.contains(">wildcardType<")); 114 | } 115 | 116 | @Test 117 | public void assertAsync() { 118 | processResource("genericdomain/AsyncController.java", "html", "all"); 119 | AssertJUnit.assertFalse("expected no AsyncWebTask fields in docs; got: \n" + defaultApiOutput, 120 | defaultApiOutput.contains(">callable<")); 121 | AssertJUnit.assertTrue("expected 'childField' in docs; got: \n" + defaultApiOutput, 122 | defaultApiOutput.contains(">childField<")); 123 | } 124 | 125 | @Test 126 | public void assertSpring43ComposedAnnotationController() { 127 | 128 | super.assertAllMethods(); 129 | 130 | for (String format : _outputFormats) { 131 | 132 | processResource("Spring43ComposedAnnotationController.java", format, "all"); 133 | 134 | AssertJUnit.assertTrue( 135 | "expected 'theGetMethod' in doc string; got: \n" + defaultApiOutput, 136 | defaultApiOutput.contains("theGetMethod")); 137 | AssertJUnit.assertTrue( 138 | "expected 'thePostMethod' in doc string; got: \n" + defaultApiOutput, 139 | defaultApiOutput.contains("thePostMethod")); 140 | AssertJUnit.assertTrue( 141 | "expected 'thePutMethod' in doc string; got: \n" + defaultApiOutput, 142 | defaultApiOutput.contains("thePutMethod")); 143 | AssertJUnit.assertTrue( 144 | "expected 'theDeleteMethod' in doc string; got: \n" + defaultApiOutput, 145 | defaultApiOutput.contains("theDeleteMethod")); 146 | AssertJUnit.assertTrue( 147 | "expected 'thePatchMethod' in doc string; got: \n" + defaultApiOutput, 148 | defaultApiOutput.contains("thePatchMethod")); 149 | } 150 | } 151 | 152 | public static void main(String[] args) throws IOException, URISyntaxException { 153 | File dir = new File(args[0]); 154 | for (int i = 1; i < args.length; i++) { 155 | runAnnotationProcessor(dir, 156 | args[i].substring(0, args[i].lastIndexOf('/')), 157 | args[i].substring(args[i].lastIndexOf('/'))); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/test/resources/org/versly/rest/wsdoc/springmvc/RestDocEndpoint.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 TaskDock, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.versly.rest.wsdoc.springmvc; 18 | 19 | import java.util.*; 20 | import javax.servlet.http.HttpServletRequest; 21 | 22 | import org.springframework.web.bind.annotation.*; 23 | import org.springframework.web.multipart.MultipartHttpServletRequest; 24 | import org.versly.rest.wsdoc.RestApiMountPoint; 25 | import org.versly.rest.wsdoc.model.ParameterizedTypeReferrer; 26 | import org.versly.rest.wsdoc.model.ValueWithOptional; 27 | 28 | @RestApiMountPoint("/mount") 29 | @RequestMapping("/api/v1") 30 | public class RestDocEndpoint { 31 | 32 | /** 33 | * This is an exciting JavaDoc comment 34 | */ 35 | @RequestMapping(value = "foo/{dateParam}/{intParam}", method = RequestMethod.GET) 36 | public Map> methodWithJavadocAndInterestingArgs(HttpServletRequest req, 37 | @PathVariable("dateParam") java.util.Date dp, 38 | @PathVariable int intParam) { 39 | return null; 40 | } 41 | 42 | @RequestMapping(value = "voidreturn1", method = RequestMethod.GET) 43 | public void methodWithVoidReturn(HttpServletRequest req) { 44 | } 45 | 46 | /** 47 | * Returns void 48 | * @param req Nothing interesting 49 | * @param p0 The integer request parameter 0 50 | * @param param1 The integer request parameter 1 51 | */ 52 | @RequestMapping(value = "voidreturn2", method = RequestMethod.GET, params = { "param0", "param1" }) 53 | public void methodWithVoidReturnAndParams(HttpServletRequest req, 54 | @RequestParam("param0") int p0, 55 | @RequestParam int param1) { 56 | } 57 | 58 | @RequestMapping(value = "pojoQueryParams", method = RequestMethod.GET) 59 | public void methodWithPojoArgs(PojoValue pojoQueryParams) { 60 | } 61 | 62 | @RequestMapping(value = "multipart", method = RequestMethod.GET) 63 | public void methodWithVoidReturn(MultipartHttpServletRequest req) { 64 | } 65 | 66 | @RequestMapping(value = "uuidReturn", method = RequestMethod.GET) 67 | public UUID uuidReturn(MultipartHttpServletRequest req) { 68 | return null; 69 | } 70 | 71 | @RequestMapping(value="optional", method = RequestMethod.POST) 72 | public void optionalFieldInRequestBody(HttpServletRequest req, 73 | @RequestBody ValueWithOptional optional) 74 | { 75 | } 76 | 77 | @RequestMapping(value="recursiveParam", method = RequestMethod.POST) 78 | public void recursiveParam(HttpServletRequest req, 79 | @RequestParam("recursive") ValueWithRecursion recursive) 80 | { 81 | } 82 | 83 | @RequestMapping(value="recursiveReturn", method = RequestMethod.GET) 84 | public ValueWithRecursion recursiveReturn(HttpServletRequest req) 85 | { 86 | return null; 87 | } 88 | 89 | @RequestMapping(value="recursiveListReturn", method = RequestMethod.GET) 90 | public @ResponseBody ValueWithListRecursion recursiveListReturn(HttpServletRequest req) 91 | { 92 | return null; 93 | } 94 | 95 | @RequestMapping(value="endpointWithParameterizedType", method = RequestMethod.GET) 96 | public ParameterizedTypeReferrer endpointWithParameterizedType() { 97 | return null; 98 | } 99 | 100 | @RequestMapping(value = { "multiple-bindings-a", "multiple-bindings-b" }, method = RequestMethod.GET) 101 | public void multipleBindings() { 102 | } 103 | 104 | public class PojoValue { 105 | 106 | public String getQueryParamVal1() { 107 | return null; 108 | } 109 | 110 | public int getQueryParamVal2() { 111 | return 1; 112 | } 113 | 114 | /** 115 | * The first query param 116 | */ 117 | public void setQueryParamVal1(String str) { 118 | } 119 | 120 | public void setQueryParamVal2(int i) { 121 | } 122 | } 123 | 124 | public class ExcitingReturnValue { 125 | /** 126 | * The exciting return value's date! 127 | */ 128 | public Date getDate() { 129 | return null; 130 | } 131 | 132 | public void setDate(Date date) { // here to exercise a setter bug 133 | } 134 | } 135 | 136 | public class ValueWithRecursion { 137 | public ValueWithRecursion getOther() { 138 | return null; 139 | } 140 | 141 | public void setOther(ValueWithRecursion other) { 142 | } 143 | } 144 | 145 | public class ValueWithListRecursion { 146 | 147 | public String getStringValue() { 148 | return null; 149 | } 150 | 151 | public void setStringValue(String val) { 152 | } 153 | 154 | public List getOthers() { 155 | return null; 156 | } 157 | 158 | public void setOthers(List others) { 159 | } 160 | } 161 | 162 | // this following widges/{id1}/gizmos endpoints are for verifying issue #29 163 | 164 | /** 165 | * This endpoint creates things. 166 | * @param id1 The widget identifier. 167 | */ 168 | @RequestMapping(value = "widgets/{id1}/gizmos", method = RequestMethod.POST) 169 | public void createThing(@PathVariable("id1") String id1) { 170 | } 171 | 172 | /** 173 | * This endpoint gets things. 174 | * @param id1 The widget identifier. 175 | * @param id2 The gizmo identifier. 176 | */ 177 | @RequestMapping(value = "widgets/{id1}/gizmos/{id2}", method = RequestMethod.GET) 178 | public void getThing(@PathVariable("id1") String id1, @PathVariable("id2") String id2) { 179 | } 180 | 181 | /** 182 | * This endpoint Deletes things. 183 | * @param id1 The widget identifier. 184 | * @param id2 The gizmo identifier. 185 | */ 186 | @RequestMapping(value = "widgets/{id1}/gizmos/{id2}", method = RequestMethod.DELETE) 187 | public void deleteThing(@PathVariable("id1") String id1, @PathVariable("id2") String id2) { 188 | } 189 | 190 | enum Colors { 191 | RED, 192 | GREEN, 193 | BLUE 194 | }; 195 | 196 | /** 197 | * This endpoint tests query parameters that are enums. 198 | * 199 | * @param color Whirlygig color of interest. 200 | */ 201 | @RequestMapping(value = "/whirlygigs", method = RequestMethod.GET) 202 | public void getWhirlygig(@RequestParam("color") Colors color) { 203 | } 204 | 205 | /** 206 | * This endpoint tests uri path parameters that are enums. 207 | * 208 | * @param color The color of interest. 209 | */ 210 | @RequestMapping(value = "/colors/{color}", method = RequestMethod.GET) 211 | public void getColor(@PathVariable("color") Colors color) { 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/main/java/org/versly/rest/wsdoc/impl/SpringMVC43RestImplementationSupport.java: -------------------------------------------------------------------------------- 1 | package org.versly.rest.wsdoc.impl; 2 | 3 | import static org.springframework.web.bind.annotation.RequestMethod.DELETE; 4 | import static org.springframework.web.bind.annotation.RequestMethod.GET; 5 | import static org.springframework.web.bind.annotation.RequestMethod.PATCH; 6 | import static org.springframework.web.bind.annotation.RequestMethod.POST; 7 | import static org.springframework.web.bind.annotation.RequestMethod.PUT; 8 | 9 | import java.lang.annotation.Annotation; 10 | import java.util.Arrays; 11 | import java.util.Collections; 12 | import java.util.HashSet; 13 | import java.util.Objects; 14 | import java.util.Optional; 15 | import java.util.Set; 16 | import java.util.function.Function; 17 | import java.util.function.Supplier; 18 | 19 | import javax.lang.model.element.ExecutableElement; 20 | import javax.lang.model.element.TypeElement; 21 | 22 | import org.springframework.web.bind.annotation.DeleteMapping; 23 | import org.springframework.web.bind.annotation.GetMapping; 24 | import org.springframework.web.bind.annotation.PatchMapping; 25 | import org.springframework.web.bind.annotation.PostMapping; 26 | import org.springframework.web.bind.annotation.PutMapping; 27 | import org.springframework.web.bind.annotation.RequestMapping; 28 | 29 | /** 30 | * Implements support for method-level composed-variants for 31 | * {@link org.springframework.web.bind.annotation.RequestMapping} annotation introduced since Spring Framework 4.3. 32 | * 33 | * @author Sidharth Mishra 34 | * @see mvc-ann-requestmapping-composed 35 | */ 36 | public class SpringMVC43RestImplementationSupport extends SpringMVCRestImplementationSupport { 37 | 38 | /** The new method-level composed annotations introduced since Spring Framework 4.3. */ 39 | private Set> getSpring43ComposedAnnotationTypes() { 40 | 41 | return new HashSet<>(Arrays.asList(GetMapping.class, PostMapping.class, PutMapping.class, PatchMapping.class, DeleteMapping.class)); 42 | } 43 | 44 | /** 45 | * Gets the path-value for the supported annotations. 46 | * 47 | * @param annotation the method-level composed annotations from Spring Framework 4.3+ 48 | * @return the request-paths if the annotation is supported, else {@link Optional#empty()}. 49 | */ 50 | private Optional getRequestPathsFromAnnotation(Annotation annotation) { 51 | 52 | if (annotation instanceof GetMapping) { 53 | 54 | return Optional.of(((GetMapping) annotation).value()); 55 | } else if (annotation instanceof PostMapping) { 56 | 57 | return Optional.of(((PostMapping) annotation).value()); 58 | } else if (annotation instanceof PutMapping) { 59 | 60 | return Optional.of(((PutMapping) annotation).value()); 61 | } else if (annotation instanceof PatchMapping) { 62 | 63 | return Optional.of(((PatchMapping) annotation).value()); 64 | } else if (annotation instanceof DeleteMapping) { 65 | 66 | return Optional.of(((DeleteMapping) annotation).value()); 67 | } 68 | 69 | return Optional.empty(); 70 | } 71 | 72 | /** 73 | * Gets the HTTP-method name for the supported annotation. 74 | * 75 | * @param annotation the spring-mvc annotation, 76 | * @return the HTTP-method name for the annotation if supported, else {@link Optional#empty()}. 77 | */ 78 | private Optional getRequestMethod(Annotation annotation) { 79 | 80 | if (annotation instanceof GetMapping) { 81 | 82 | return Optional.of(GET.name()); 83 | } else if (annotation instanceof PostMapping) { 84 | 85 | return Optional.of(POST.name()); 86 | } else if (annotation instanceof PutMapping) { 87 | 88 | return Optional.of(PUT.name()); 89 | } else if (annotation instanceof PatchMapping) { 90 | 91 | return Optional.of(PATCH.name()); 92 | } else if (annotation instanceof DeleteMapping) { 93 | 94 | return Optional.of(DELETE.name()); 95 | } 96 | 97 | return Optional.empty(); 98 | } 99 | 100 | /** 101 | * Gets the request-paths for the supported Spring MVC method-level annotations. 102 | * Starts with the newer method-level composed annotations introduced since Spring Framework 4.3. If it cannot find any new annotations, delegates the control to super-class 103 | * for handling {@link RequestMapping} annotation. 104 | * 105 | * @param annotationProvider a function that searches for the annotation of the annotationClass' type on the element under process. 106 | * @param altSupplier the supplier to use in-case the annotation is {@link RequestMapping} –– to be delegated to the super-class, 107 | * @return the request-paths from supported annotations, else an empty-string-array. 108 | */ 109 | private String[] getRequestPaths(Function, Annotation> annotationProvider, Supplier altSupplier) { 110 | 111 | for (Class annotationClass : this.getSpring43ComposedAnnotationTypes()) { 112 | 113 | Annotation annotation = annotationProvider.apply(annotationClass); 114 | 115 | if (Objects.nonNull(annotation)) { 116 | 117 | Optional requestPaths = getRequestPathsFromAnnotation(annotation); 118 | 119 | if (requestPaths.isPresent()) { 120 | 121 | return requestPaths.get(); 122 | } 123 | } 124 | } 125 | // Delegate the request-path computation for the RequestMapping annotated methods to super-class 126 | // 127 | return altSupplier.get(); 128 | } 129 | 130 | /** Supports {@link RequestMapping} and the new method-level composed annotations introduced in Spring Framework 4.3. */ 131 | @Override 132 | public Set> getExtendedMappingAnnotationTypes() { 133 | 134 | Set> supportedAnnotationTypes = new HashSet<>(Collections.singletonList(RequestMapping.class)); 135 | 136 | supportedAnnotationTypes.addAll(this.getSpring43ComposedAnnotationTypes()); 137 | 138 | return supportedAnnotationTypes; 139 | } 140 | 141 | @Override 142 | public String[] getRequestPaths(ExecutableElement executableElement, TypeElement contextClass) { 143 | 144 | return getRequestPaths(executableElement::getAnnotation, () -> super.getRequestPaths(executableElement, contextClass)); 145 | } 146 | 147 | @Override 148 | public String[] getRequestPaths(TypeElement cls) { 149 | 150 | return getRequestPaths(cls::getAnnotation, () -> super.getRequestPaths(cls)); 151 | } 152 | 153 | @Override 154 | public String getRequestMethod(ExecutableElement executableElement, TypeElement contextClass) { 155 | 156 | for (Class annotationClass : this.getSpring43ComposedAnnotationTypes()) { 157 | 158 | Optional requestMethod = getRequestMethod(executableElement.getAnnotation(annotationClass)); 159 | 160 | if (requestMethod.isPresent()) { 161 | 162 | return requestMethod.get(); 163 | } 164 | } 165 | // Delegate to super-class to handle the RequestMapping annotation 166 | // 167 | return super.getRequestMethod(executableElement, contextClass); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/main/resources/org/versly/rest/wsdoc/RestDocumentation.ftl: -------------------------------------------------------------------------------- 1 | 16 | 17 | <#-- @ftlvariable name="docs" type="java.util.List" --> 18 | 19 | 20 | 21 | REST Endpoint Documentation 22 | 50 | 51 | 52 | 53 |
Overview
54 | <#list api.resources as resource> 55 |
56 | ${resource.path} 57 |
58 | <#list resource.requestMethodDocs?sort_by("requestMethod") as methodDoc> 59 | ${methodDoc.requestMethod} 60 | 61 |
62 |
63 | 64 | 65 | <#list api.resources as resource> 66 | <#list resource.requestMethodDocs as methodDoc> 67 | 68 |
69 |
70 | ${methodDoc.requestMethod} 71 | ${resource.path}<#t> 72 | <#-- append any URL parameters to the end of the main banner --> 73 | <#assign params=methodDoc.urlParameters.fields><#t> 74 | <#if (params?keys?size > 0)><#t> 75 | ?<#list params?keys as key><#t> 76 | ${key}<#t> 77 | =<#t> 78 | <@render_json params[key].fieldType/><#t> 79 | <#if key_has_next>&<#t> 80 | <#t> 81 | <#t> 82 |
83 | 84 | <#if (methodDoc.commentText??)> 85 |
${methodDoc.commentText}
86 | 87 | 88 | <#if methodDoc.multipartRequest> 89 |
90 | Note: this endpoint expects a multipart request body. 91 |
92 | 93 | 94 | <#assign subs=methodDoc.urlSubstitutions.fields> 95 | <#if (subs?keys?size > 0)> 96 |
97 | 98 | 99 | 100 | 101 | <#list subs?keys as key> 102 | 103 | 104 | 105 | 106 | 107 | 108 |
URL Substitution KeyExpected TypeDescription
${key}<@render_json subs[key].fieldType/>${subs[key].fieldDescription!}
109 |
110 | 111 | 112 | <#assign queryParams=methodDoc.urlParameters.fields> 113 | <#if (queryParams?keys?size > 0)> 114 |
115 | 116 | 117 | 118 | 119 | <#list queryParams?keys as key> 120 | 121 | 122 | 123 | 124 | 125 | 126 |
Query ParameterExpected TypeDescription
${key}<@render_json queryParams[key].fieldType/>${queryParams[key].fieldDescription!}
127 |
128 | 129 | 130 | 131 | <#if (methodDoc.requestBody??)> 132 |
133 |
Request Body
134 |
<@render_json methodDoc.requestBody/>
135 |
136 | 137 | 138 | <#if (methodDoc.responseBody??)> 139 |
140 |
Response Body
141 |
<@render_json methodDoc.responseBody/>
142 |
143 | 144 |
145 | 146 | 147 | 148 | 149 | 150 | <#macro render_json json> 151 | <#if json.class.name == "org.versly.rest.wsdoc.impl.JsonPrimitive"> 152 | <@render_json_primitive json/> 153 | <#elseif json.class.name == "org.versly.rest.wsdoc.impl.JsonObject"> 154 | <@render_json_object json/> 155 | <#elseif json.class.name == "org.versly.rest.wsdoc.impl.JsonRecursiveObject"> 156 | <@render_json_recursive_object json/> 157 | <#elseif json.class.name == "org.versly.rest.wsdoc.impl.JsonArray"> 158 | <@render_json_array json/> 159 | <#elseif json.class.name == "org.versly.rest.wsdoc.impl.JsonDict"> 160 | <@render_json_dict json/> 161 | 162 | 163 | 164 | <#macro render_json_array json> 165 | <#-- @ftlvariable name="json" type="org.versly.rest.wsdoc.impl.JsonArray" --> 166 | [ 167 | <@render_json json.elementType /> 168 | ] 169 | 170 | 171 | <#macro render_json_dict json> 172 | <#-- @ftlvariable name="json" type="org.versly.rest.wsdoc.impl.JsonDict" --> 173 | [ 174 | <@render_json json.keyType/> 175 | -> 176 | <@render_json json.valueType/> 177 | ] 178 | 179 | 180 | <#macro render_json_primitive json> 181 | <#-- @ftlvariable name="json" type="org.versly.rest.wsdoc.impl.JsonPrimitive" --> 182 | ${json.typeName}<#t> 183 | <#if json.restrictions??><#t> 184 |
<#t> 185 | one of [ <#list json.restrictions as restricton><#t> 186 | ${restricton}<#if restricton_has_next>, <#t> 187 | ]
<#t> 188 | <#t> 189 | 190 | 191 | <#macro render_json_object json> 192 | <#-- @ftlvariable name="json" type="org.versly.rest.wsdoc.impl.JsonObject" --> 193 | { 194 |
195 | <#list json.fields as field> 196 |
197 | ${field.fieldName} 198 | <@render_json field.fieldType/> 199 | <#if field.commentText??> 200 |
${field.commentText}
201 | 202 |
203 | 204 |
<#t> 205 |
<#t> 206 | } 207 | 208 | 209 | <#macro render_json_recursive_object json> 210 | <#-- @ftlvariable name="json" type="org.versly.rest.wsdoc.impl.JsonRecursiveObject" --> 211 | ${json.recursedObjectTypeName} ↺ 212 | 213 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 19 | 20 | 4.0.0 21 | Web Service Documentation Generator 22 | Generates HTML documentation from Spring Web Services REST endpoints 23 | 24 | org.versly 25 | versly-wsdoc 26 | jar 27 | 1.2-SNAPSHOT 28 | http://github.com/versly/wsdoc 29 | 30 | 31 | 32 | Apache License, Version 2.0 33 | https://www.apache.org/licenses/LICENSE-2.0.txt 34 | 35 | 36 | 37 | 38 | 39 | pcl 40 | Patrick Linskey 41 | pcl@apache.org 42 | 43 | 44 | 45 | 46 | https://github.com/versly/wsdoc 47 | 48 | 49 | 50 | 51 | mulesoft-repo 52 | https://repository-master.mulesoft.org/nexus/content/repositories/public 53 | 54 | 55 | 56 | 57 | 58 | 59 | org.springframework 60 | spring-webmvc 61 | 5.0.9.RELEASE 62 | 63 | 64 | 65 | 66 | javax.ws.rs 67 | javax.ws.rs-api 68 | 2.1.1 69 | 70 | 71 | 72 | com.beust 73 | jcommander 74 | 1.4 75 | 76 | 77 | 78 | 79 | joda-time 80 | joda-time 81 | 2.2 82 | 83 | 84 | org.freemarker 85 | freemarker 86 | 2.3.16 87 | 88 | 89 | javax.servlet 90 | servlet-api 91 | 2.5 92 | compile 93 | 94 | 95 | com.fasterxml.jackson.core 96 | jackson-core 97 | 2.4.2 98 | 99 | 100 | com.fasterxml.jackson.core 101 | jackson-annotations 102 | 2.4.1 103 | 104 | 105 | com.fasterxml.jackson.datatype 106 | jackson-datatype-joda 107 | 2.4.2 108 | 109 | 110 | com.fasterxml.jackson.core 111 | jackson-databind 112 | 2.4.2 113 | 114 | 115 | com.fasterxml.jackson.datatype 116 | jackson-datatype-jdk8 117 | 2.4.3 118 | 119 | 120 | org.testng 121 | testng 122 | 6.8.8 123 | test 124 | 125 | 126 | snakeyaml 127 | org.yaml 128 | 129 | 130 | 131 | 132 | com.fasterxml.jackson.module 133 | jackson-module-jsonSchema 134 | 2.4.2 135 | 136 | 137 | org.apache.commons 138 | commons-lang3 139 | 3.3.2 140 | 141 | 142 | 143 | 144 | org.raml 145 | raml-parser 146 | 0.9-SNAPSHOT 147 | test 148 | 149 | 150 | org.yaml 151 | snakeyaml 152 | 1.13 153 | 154 | 155 | 156 | 157 | 158 | 159 | services 160 | 161 | 162 | 163 | src/main/resources 164 | 165 | META-INF/services/* 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | release 174 | 175 | 176 | 177 | org.sonatype.plugins 178 | nexus-staging-maven-plugin 179 | 1.6.7 180 | true 181 | 182 | ossrh 183 | https://oss.sonatype.org/ 184 | true 185 | 186 | 187 | 188 | 189 | org.apache.maven.plugins 190 | maven-source-plugin 191 | 2.2.1 192 | 193 | 194 | attach-sources 195 | 196 | jar-no-fork 197 | 198 | 199 | 200 | 201 | 202 | 203 | org.apache.maven.plugins 204 | maven-javadoc-plugin 205 | 2.9.1 206 | 207 | 208 | attach-javadocs 209 | 210 | jar 211 | 212 | 213 | 214 | 215 | 216 | 217 | org.apache.maven.plugins 218 | maven-gpg-plugin 219 | 1.5 220 | 221 | 222 | sign-artifacts 223 | verify 224 | 225 | sign 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | src/main/resources 240 | 241 | META-INF/services/* 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | org.apache.maven.plugins 250 | maven-compiler-plugin 251 | 3.3 252 | 253 | -proc:none 254 | 1.8 255 | 1.8 256 | 257 | 258 | 259 | 260 | org.apache.maven.plugins 261 | maven-jar-plugin 262 | 2.4 263 | 264 | 265 | 266 | test-jar 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | ossrh 277 | https://oss.sonatype.org/content/repositories/snapshots 278 | 279 | 280 | 281 | 282 | -------------------------------------------------------------------------------- /src/main/java/org/versly/rest/wsdoc/RestDocAssembler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 TaskDock, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.versly.rest.wsdoc; 18 | 19 | import com.beust.jcommander.JCommander; 20 | import com.beust.jcommander.Parameter; 21 | import com.beust.jcommander.internal.Lists; 22 | import freemarker.template.Configuration; 23 | import freemarker.template.DefaultObjectWrapper; 24 | import freemarker.template.Template; 25 | import freemarker.template.TemplateException; 26 | import org.versly.rest.wsdoc.impl.RestDocumentation; 27 | import org.versly.rest.wsdoc.impl.Utils; 28 | 29 | import java.io.*; 30 | import java.util.*; 31 | import java.util.jar.JarFile; 32 | import java.util.regex.Pattern; 33 | import java.util.zip.ZipEntry; 34 | 35 | public class RestDocAssembler { 36 | private final String _outputFileName; 37 | private final String _outputTemplate; 38 | 39 | public static void main(String... args) 40 | throws IOException, ClassNotFoundException, TemplateException { 41 | Arguments arguments = new Arguments(); 42 | new JCommander(arguments, args); 43 | Utils.addTemplateValue(DocumentationRestApi.MOUNT_TEMPLATE, arguments.mountTemplateValue); 44 | Utils.addTemplateValue(DocumentationRestApi.ID_TEMPLATE, arguments.idTemplateValue); 45 | Utils.addTemplateValue(DocumentationRestApi.TITLE_TEMPLATE, arguments.titleTemplateValue); 46 | Utils.addTemplateValue(DocumentationRestApi.VERSION_TEMPLATE, arguments.versionTemplateValue); 47 | 48 | List docs = new LinkedList(); 49 | for (String input : arguments.inputs) { 50 | File inputFile = new File(input); 51 | if (inputFile.isDirectory()) { 52 | System.err.println("adding web service docs from classes directory " + input); 53 | File resourceFile = new File(inputFile, Utils.SERIALIZED_RESOURCE_LOCATION); 54 | docs.add(RestDocumentation.fromStream(new FileInputStream(resourceFile))); // TODO resource management 55 | } else if (input.toLowerCase().endsWith(".war")) { 56 | System.err.println("adding web service docs from WAR " + input); 57 | JarFile jar = new JarFile(input); 58 | ZipEntry e = jar.getEntry("WEB-INF/classes/" + Utils.SERIALIZED_RESOURCE_LOCATION); 59 | docs.add(RestDocumentation.fromStream(jar.getInputStream(e))); 60 | jar.close(); 61 | } else { 62 | System.err.println("adding web service docs from serialized input " + input); 63 | docs.add(RestDocumentation.fromStream(new FileInputStream(inputFile))); // TODO resource management 64 | } 65 | } 66 | 67 | if (docs.size() > 0) { 68 | List excludePatterns = new ArrayList(); 69 | for (String pattern : arguments.excludes) 70 | excludePatterns.add(Pattern.compile(pattern)); 71 | new RestDocAssembler(arguments.outputFileName, arguments.outputFormat) 72 | .writeDocumentation(docs, excludePatterns, arguments.scope); 73 | } 74 | } 75 | 76 | public RestDocAssembler(String outputFileName, String outputFormat) { 77 | 78 | _outputFileName = outputFileName; 79 | if (outputFormat.equalsIgnoreCase("raml")) 80 | { 81 | _outputTemplate = "RamlDocumentation.ftl"; 82 | } 83 | else 84 | { 85 | _outputTemplate = "RestDocumentation.ftl"; 86 | } 87 | } 88 | 89 | public RestDocAssembler(String outputFileName) { 90 | this(outputFileName, "html"); 91 | } 92 | 93 | /** 94 | * combine APIs from the REST docs into one map, merging those APIs with matching identifiers 95 | */ 96 | private Collection mergeApis(List docs) { 97 | Map aggregatedApis = new LinkedHashMap(); 98 | for (RestDocumentation doc : docs) { 99 | for (RestDocumentation.RestApi api : doc.getApis()) { 100 | if (!aggregatedApis.containsKey(api.getIdentifier())) { 101 | aggregatedApis.put(api.getIdentifier(), api); 102 | } 103 | else { 104 | aggregatedApis.get(api.getIdentifier()).merge(api); 105 | } 106 | } 107 | } 108 | return aggregatedApis.values(); 109 | } 110 | 111 | /** 112 | * Filter out APIs based on user provided exclude patterns and selected scope (scope of 'all' implies no filtering). 113 | */ 114 | private Collection filterApis( 115 | Collection apis, Iterable excludePatterns, String scope) { 116 | 117 | // filter doc objects by client provided exclude patterns 118 | Collection filteredApis = null; 119 | if (excludePatterns != null) { 120 | filteredApis = new LinkedList(); 121 | for (RestDocumentation.RestApi api : apis) 122 | filteredApis.add(api.filter(excludePatterns)); 123 | } else { 124 | filteredApis = apis; 125 | } 126 | 127 | // use command-line --scope value to filter the generated documentation 128 | if (!scope.equals("all")) { 129 | HashSet requestedScopes = new HashSet(Arrays.asList(new String[]{scope})); 130 | 131 | // ugly old-style iterating because we need to be able to remove elements as we go 132 | Iterator apiIter = filteredApis.iterator(); 133 | while (apiIter.hasNext()) { 134 | RestDocumentation.RestApi api = apiIter.next(); 135 | Iterator resIter = api.getResources().iterator(); 136 | while (resIter.hasNext()) { 137 | RestDocumentation.RestApi.Resource resource = resIter.next(); 138 | Iterator methIter = resource.getRequestMethodDocs().iterator(); 139 | while (methIter.hasNext()) { 140 | HashSet scopes = methIter.next().getDocScopes(); 141 | scopes.retainAll(requestedScopes); 142 | if (scopes.isEmpty()) { 143 | methIter.remove(); 144 | } 145 | } 146 | if (resource.getRequestMethodDocs().isEmpty()) { 147 | resIter.remove(); 148 | } 149 | } 150 | if (api.getResources().isEmpty()) { 151 | apiIter.remove(); 152 | } 153 | } 154 | } 155 | 156 | return filteredApis; 157 | } 158 | 159 | /** 160 | * derive the common base URI for all resources in each API and declare that the API mount point. 161 | */ 162 | private void deriveBaseURIs(Collection apis) { 163 | for (RestDocumentation.RestApi api : apis) { 164 | String basePath = null; 165 | for (RestDocumentation.RestApi.Resource resource : api.getResources()) { 166 | String resourcePath = resource.getPath(); 167 | if (null != resourcePath) { 168 | if (null == basePath) { 169 | basePath = resourcePath; 170 | } else { 171 | String[] baseParts = basePath.split("/"); 172 | String[] resourceParts = resourcePath.split("/"); 173 | basePath = ""; 174 | int smallerLength = Math.min(baseParts.length, resourceParts.length); 175 | for (int i = 0; i < smallerLength && baseParts[i].equals(resourceParts[i]); ++i) { 176 | if (baseParts[i].length() > 0) { 177 | basePath += "/" + baseParts[i]; 178 | } 179 | } 180 | } 181 | } 182 | } 183 | api.setMount(basePath); 184 | } 185 | } 186 | 187 | List writeDocumentation(List docs, Iterable excludePatterns, String scope) 188 | throws IOException, ClassNotFoundException, TemplateException { 189 | List filesWritten = new ArrayList(); 190 | 191 | // combine APIs from the REST docs into one map, merging those with matching identifiers 192 | Collection apis = mergeApis(docs); 193 | 194 | // filter out APIs based on exclude patterns and selected publishing scope 195 | apis = filterApis(apis, excludePatterns, scope); 196 | 197 | // derive the common base URI for all resources of each API and declare that the API mount 198 | deriveBaseURIs(apis); 199 | 200 | Configuration conf = new Configuration(); 201 | conf.setClassForTemplateLoading(RestDocAssembler.class, ""); 202 | conf.setObjectWrapper(new DefaultObjectWrapper()); 203 | Writer out = null; 204 | try { 205 | for (RestDocumentation.RestApi api : apis) { 206 | Template template = conf.getTemplate(_outputTemplate); 207 | Map root = new HashMap(); 208 | root.put("api", api); 209 | String fileName = getOutputFileName(api); 210 | filesWritten.add(fileName); 211 | File file = new File(fileName); 212 | out = new FileWriter(file); 213 | template.process(root, out); 214 | out.flush(); 215 | System.err.printf("Wrote REST docs to %s\n", file.getAbsolutePath()); 216 | } 217 | } finally { 218 | if (out != null) { 219 | try { 220 | out.close(); 221 | } catch (IOException ignored) { 222 | // ignored 223 | } 224 | } 225 | } 226 | return filesWritten; 227 | } 228 | 229 | String getOutputFileName(RestDocumentation.RestApi api) { 230 | String outputFileName = _outputFileName; 231 | if (!api.getIdentifier().equals(RestDocumentation.RestApi.DEFAULT_IDENTIFIER)) { 232 | StringBuilder constructedName = new StringBuilder(_outputFileName); 233 | int identifierIndex = constructedName.lastIndexOf("."); 234 | if (identifierIndex < 0) { 235 | identifierIndex = constructedName.length(); 236 | } 237 | constructedName.insert(identifierIndex, "-" + api.getIdentifier()); 238 | outputFileName = constructedName.toString(); 239 | } 240 | return outputFileName; 241 | } 242 | 243 | static class Arguments { 244 | @Parameter 245 | List inputs = Lists.newArrayList(); 246 | 247 | @Parameter(names = { "-o", "--out" }, description = "File to write HTML documentation to") 248 | String outputFileName = "web-service-api.html"; 249 | 250 | @Parameter(names = { "--exclude" }, description = "Endpoint pattern to exclude from the generated docs") 251 | List excludes = Lists.newArrayList(); 252 | 253 | @Parameter(names = { "-f", "--format" }, description = "Format for output: html or raml") 254 | String outputFormat = "html"; 255 | 256 | @Parameter(names = { "-s", "--scope" }, description = "Publication scope for output (e.g. public, private, etc) or \"all\"") 257 | String scope = "all"; 258 | 259 | @Parameter(names = { "--template-mount" }, description = "Mount point to use when filling templates.") 260 | String mountTemplateValue = ""; 261 | 262 | @Parameter(names = { "--template-id" }, description = "Id to use when filling templates.") 263 | String idTemplateValue = ""; 264 | 265 | @Parameter(names = { "--template-title" }, description = "Title to use when filling templates.") 266 | String titleTemplateValue = ""; 267 | 268 | @Parameter(names = { "--template-version" }, description = "Version to use when filling templates.") 269 | String versionTemplateValue = ""; 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Web Service Documentation Generator ## 2 | 3 | Automatically generate up-to-date documentation for your REST API. 4 | 5 | 1. [Installation](#installation) 6 | 2. [Running](#running) 7 | 3. [Samples](#samples) 8 | 4. [Limitations](#limitations) 9 | 5. [Configuration](#configuration) 10 | 6. [Use with maven](#maven) 11 | 7. [License (ASL v2)](#license) 12 | 13 |
14 | #### Installation 15 | 16 | Currently, wsdoc is available in source format only. To install, you'll need mvn and Java 1.8 (or later) and whatnot: 17 | 18 | git clone git@github.com:versly/wsdoc.git 19 | cd wsdoc 20 | mvn install 21 | 22 | Once you've done this, the wsdoc jar will be available in your local Maven repository, probably at ~/.m2/repository/org/versly/versly-wsdoc/1.1-SNAPSHOT/versly-wsdoc-1.1-SNAPSHOT.jar 23 | 24 | Note, the current supported source version is Java 11. If you intend to use wsdoc to document code that does not utilize newer types, a one-line change in AnnotationProcessor.java can be made. Specifically, locate this line in AnnotationProcessor.java: 25 | 26 | @SupportedSourceVersion(SourceVersion.RELEASE_11) 27 | 28 | and change it to something like: 29 | 30 | @SupportedSourceVersion(SourceVersion.RELEASE_X) 31 | 32 | where "RELEASE_X" reflects the version of Java your build environment uses (e.g. "RELEASE_7"). 33 | 34 | 35 | #### Running wsdoc 36 | 37 | With version 1.1-SNAPSHOT, wsdoc now requires a Java 11 runtime at annotations processing time. This does not impose any requirements, however, on the source version or target runtime of the processed Java code. 38 | 39 | Often, a single REST API is implemented across a number of web archives. As a result, wsdoc is designed to run in two passes: a data-gathering pass (implemented via Java annotation processor) and an output-assembly pass (implemented as a standalone Java program): 40 | 41 | 1\. Generate the wsdoc interim data. This will create a file called org.versly.rest.wsdoc.web-service-api.ser in your build output directory. This file should be included as a resource in your web archive (at WEB-INF/classes/org.versly.rest.wsdoc.web-service-api.ser) 42 | 43 | javac -processor org.versly.rest.wsdoc.AnnotationProcessor *.java 44 | 45 | 2\. (Optional) Perform the rest of your WAR assembly. 46 | 47 | 3\. Generate the HTML output, given all your web archives or output from the annotation processor: 48 | 49 | java org.versly.rest.wsdoc.RestDocAssembler *.war *.ser 50 | 51 | 4\. Enjoy the output at web-service-api.html 52 | 53 | Note, with release 1.1-SNAPSHOT, wsdoc requires a Java 8 runtime at annotations processing time. This will not impose any requirements, however, on the source version or target runtime of the processed Java code. 54 | 55 | 56 | #### Sample Input and Output 57 | 58 | * Input: https://github.com/versly/wsdoc/blob/gh-pages/sample/SnowReportController.java 59 | 60 | * Output: http://versly.github.com/wsdoc/sample/web-service-api.html 61 | 62 | 63 | #### Limitations 64 | 65 | * wsdoc is currently limited to REST endpoints identified via [Spring 4.3 web bind annotations](https://docs.spring.io/spring-framework/docs/4.3.0.RELEASE/spring-framework-reference/htmlsingle/#mvc-ann-requestmapping) or [JaxRs endpoints](https://github.com/eclipse-ee4j/jaxrs-api) (@RequestMapping, @GetMapping, @PostMapping, @PutMapping, @PatchMapping, @DeleteMapping, and @Path). 66 | 67 | * wsdoc needs access to your sources to extract JavaDoc comments. If you package your DTOs in a separate compilation unit than your controllers using a build tool like mvn, the sources for those compilation units might not be available. So, wsdoc will not find the comments and will therefore not include them in the generated output. This can be resolved by providing additional source locations to apt. 68 | 69 | * We've made a bunch of JSON-related assumptions about how you want your DTOs to be represented. None of the Jackson annotations (except @JsonIgnore) are considered, so you're pretty much left with a simple bean transformation. 70 | 71 | * The output format is JSON-esque, but things will probably be comprehensible for non-JSON wire formats, too, assuming that our object model scanning assumptions pan out. 72 | 73 | * Only a subset of the Spring web bind annotations are supported. This isn't by design or due to fundamental limitations; we've just only built support for the parts that we use. 74 | 75 | 76 | #### Configuration and options 77 | 78 | * Configuring the intermediate output destination 79 | 80 | wsdoc runs in two distinct execution phases, and stores some data in your WARs to communicate from the first phase to the second. This data must be stored in a resource located at WEB-INF/classes/org.versly.rest.wsdoc.web-service-api.ser. Typically, this means you should run the wsdoc annotation processor with the same build output configuration (javac -d on the command line, etc.) as you use for your WAR. 81 | 82 | * Specifying the HTML output file name 83 | 84 | You can specify where wsdoc should generate its output to with the --out flag when invoking org.versly.rest.wsdoc.RestDocAssembler: 85 | 86 | java org.versly.rest.wsdoc.RestDocAssembler --out snow-report.html *.war 87 | 88 | * Excluding endpoint patterns 89 | 90 | If you have some endpoints that you'd like to exclude from the generated output, use the --exclude option to specify regular expressions to omit when invoking org.versly.rest.wsdoc.RestDocAssembler: 91 | 92 | java org.versly.rest.wsdoc.RestDocAssembler --out snow-report.html *.war --exclude /api/v1/admin.* 93 | 94 | * Adding a prefix to the generated URLs 95 | 96 | Often, an entire WAR or a particular controller is bound to a root URL path. This information may not be available to wsdoc when processing the Java source files alone. For example, we might want to bind the snow-report endpoint in our example to /api/v1/snow-report rather than just /snow-report. To include this sort of prefix information in the generated documentation, use the [@RestApiMountPoint](https://github.com/versly/wsdoc/blob/master/org/versly/rest/wsdoc/RestApiMountPoint.java) annotation: 97 | 98 | @Controller 99 | @RestApiMountPoint("/api/v1") 100 | public class SnowReportController { 101 | ... 102 | } 103 | 104 | All endpoints inside the annotated class will be prefixed with the text provided to the annotation. Note, this will 105 | be in addition to the prefixing of any class level request paths contributed by other annotations. For example: 106 | 107 | @RestApiMountPoint("/mount/api/v1") 108 | @RequestMapping("/myservice") 109 | public class RestDocEndpoint { 110 | ... 111 | } 112 | 113 | will result in the prefixing of endpoints with "/mount/api/v1/myservice". 114 | 115 | * Set Output Format 116 | 117 | Output can be generated in either HTML or RAML format. The default format is HTML, however either may be specified 118 | using the command-line option --format. For example: 119 | 120 | java org.versly.rest.wsdoc.RestDocAssembler --format raml --out snow-report.raml *.war 121 | 122 | or 123 | 124 | java org.versly.rest.wsdoc.RestDocAssembler --format html --out snow-report.html *.war 125 | 126 | * Controlling Publication Scope 127 | 128 | Publication scoping may be asserted using the @DocumentationScope annotation. This annotation supports user defined 129 | scopes but also defines convenient constants for common scopes, such as DocumentationScope.PUBLIC and 130 | DocumentationScope.PRIVATE. When executing the doc assembler phase of doc generation, the --scope command line option 131 | can be used to indicate a particular scope on which to filter the production of API documentation, or "all" 132 | to indicate all endpoints are to be documented regardless of scope. 133 | 134 | If both an endpoint and it's containing class are explicitly annotated with a @DocumentationScope the scopes of that 135 | endpoint are regarded as the union of the values provided in both annotations (note an endpoint may belong to multiple 136 | scopes). 137 | 138 | @DocumentationScope(DocumentationScope.PRIVATE) 139 | public static class ExperimentalController { 140 | 141 | @DocumentationScope(DocumentationScope.PUBLIC) 142 | @RequestMapping(value = "/m1", method = RequestMethod.GET) 143 | public void m1() { 144 | ... 145 | } 146 | 147 | @RequestMapping(value = "/m2", method = RequestMethod.GET) 148 | public void m2() { 149 | ... 150 | } 151 | 152 | @DocumentationScope("experimental") 153 | @RequestMapping(value = "/m3", method = RequestMethod.GET) 154 | public void m3() { 155 | ... 156 | } 157 | } 158 | 159 | In the above example, m1 will have both public and private scopes, m2 will have only private scope, and m3 will 160 | have private and experimental scope. If, during the doc assembler phase, the --scope command line argument is 161 | provided with a value of "public", only m1 will be documented. Likewise, if a value of "experimental" is provided 162 | for --scope, then only m3 will be documented. If a value of "private" (or "all") is provided than m1, m2, and m3 will 163 | all be documented. 164 | 165 | * Generating API Level Documentation 166 | 167 | Developers may optionally use the `@DocumentationRestApi` annotation, at the class level, to identify a class containing 168 | rest endpoints as representing all or part of a single REST API. If the `@DocumentationRestApi` annotation is present, 169 | all classes annotated with the same `id` value will be regarded by wsdoc as part of the same API and will be documented 170 | together. For a given `id` value, there should be one such annotated class for which the `@DocumentationRestApi` 171 | annotation also includes a human-friendly `title` and `version`. The javadocs of this class will also be included 172 | in the generated documentation as the high-level overview describing the API. 173 | 174 | For example: 175 | 176 | /** 177 | * This is the header documentation text for RestApi2. This API actually spans 178 | * multiple controller classes, RestApi2_A and RestApi2_B. This javadoc text 179 | * will appear as an "overview" in the generated documentation. 180 | */ 181 | @DocumentationRestApi(id = "RestApi2", title = "The RestApi2 API", version = "v1") 182 | @Path("/restapi2/api/v1") 183 | public class RestApi2_A { 184 | 185 | @GET 186 | @Path("/gadgets") 187 | public void getGadgets() { 188 | } 189 | } 190 | 191 | @DocumentationRestApi(id = "RestApi2") 192 | @Path("/restapi2/api/v1") 193 | public class RestApi2_B { 194 | 195 | @GET 196 | @Path("/whirlygigs") 197 | public void getWhirlygigs() { 198 | } 199 | } 200 | 201 | The above defines a single API that spans two controller classes. The javadocs of the `RestApi2_A` class will be used as 202 | the class level documentation for the API, and the resources of that API will include the union of those defined in both 203 | `RestApi2_A` and `RestApi2_B`. All REST endpoints defined in classes for which no `@DocumentationRestApi` annotation is 204 | present will be included together in a separate anonymous default API. 205 | 206 | If two or more APIs are present during either the annotation processing phase or the document assembly phase, 207 | each API will result in a separate generated output document. Endpoints that land in the anonymous default API 208 | will be included together in a single output file with the exact name as given in the `--out` parameter of the 209 | document assembly command. Each other API explicitly defined by the `@DocumentationRestApi` annotation will result in 210 | an output file named after the `--out` parameter but with the addition of a suffix indicating the `id` of the API that 211 | file represents. For example, a documentation assembly phase initiated with the command: 212 | 213 | java org.versly.rest.wsdoc.RestDocAssembler --format raml --out snow-report.raml *.war 214 | 215 | May result in several output files with names such as: 216 | 217 | snow-report.raml 218 | snow-report-RestApi2.raml 219 | snow-report-SomeOtherApi.raml 220 | 221 | where the first contains endpoints not defined in `@DocumentationRestApi` annotated classes, and the second contains 222 | endpoints defined in classes annotated with `@DocumentationRestApi(id = "RestApi2")`. 223 | 224 | 225 | * Method and API Level Traits 226 | 227 | A flexible `@DocumentationTraits` annotation can be used to tag APIs and 228 | methods with markers that identify them as having certain characteristics. For 229 | example, the `@DocumentationTraits` annotation can be used to note that a given 230 | method is deprecated, experimental, or some other developer defined tag. 231 | 232 | @DocumentationTraits(DocumentationTraits.EXPERIMENTAL) 233 | public static class RestController { 234 | 235 | @GET 236 | @Path("/method1") 237 | public void method1() { 238 | } 239 | 240 | @GET 241 | @DocumentationTraits(DocumentationTraits.DEPRECATED) 242 | @Path("/method2") 243 | public void method2() { 244 | } 245 | 246 | @GET 247 | @DocumentationTraits("service-scope") 248 | @Path("/method3") 249 | public void method2() { 250 | } 251 | } 252 | 253 | In the example above, all methods inherit the `EXPERIMENTAL` trait from the 254 | controller class. The `method2` is additionally associated with the `DEPRECATED` 255 | trait, and `method3` is similarly tagged with a developer defined 256 | `service-scope` which might represent the level of authorization that is 257 | required to use that method. During RAML documentation generation, these tags 258 | are manifest as RAML traits in the composed RAML documentation, where they can 259 | be subsequently augmented with text that describes the semantics of each trait. 260 | 261 | * Method and API Level Authorization Scopes 262 | 263 | The `@AuthorizationScope` annotation may be used to assign one or more OAuth2 authorization scopes to a given endpoint 264 | handler or to an entire controller. For example, a controller may be annotated to permit authorization for all contained 265 | endpoint handlers based on one of several scopes declared at the class level, such as `two_scope_service:read` and 266 | `two_scope_service:admin`. It may alternatively permit authorization for particular contained endpoint handlers based on 267 | scopes declared at the method level, such as `two_scope_service:write`. This might look as follows. 268 | 269 | @AuthorizationScope( { "two_scope_service:read", "two_scope_service:admin" } ) 270 | public static class TwoScopeController { 271 | 272 | @AuthorizationScope("two_scope_service:write") 273 | @RequestMapping(value = "/twoscope", method = RequestMethod.POST) 274 | public void get() { 275 | } 276 | 277 | @RequestMapping(value = "/twoscope", method = RequestMethod.GET) 278 | public void post() { 279 | } 280 | } 281 | 282 | Note that method-level declarations will override class-level declarations. 283 | 284 | * Override generated response object 285 | 286 | Sometimes it is neccesary for APIs to return generic response objects such as `javax.ws.rs.core.Response` with the actual response object wrapped as an entity. In these cases the `@ReturnType` annotation can be used to manually specify the expected response object: 287 | 288 | @POST 289 | @Path("genericResponsePost") 290 | @ReturnType(Customer.class) 291 | public Response genericResponsePost(final CustomerPOST customerPost) { 292 | return Response.created(customerService(customerPost)).build(); 293 | } 294 | 295 | When the documentation is generated for the example above, the response object schema generation will use `Customer.class` class instead of the generic `Response.class`. 296 | 297 | * Using templates in REST documentation 298 | 299 | In cases where REST mount-point, description etc. are not available during the annotation processing phase, a user can annotate such values with special markers: 300 | 301 | @DocumentationRestApi( 302 | id = DocumentationRestApi.ID_TEMPLATE, 303 | title = DocumentationRestApi.TITLE_TEMPLATE, 304 | version = DocumentationRestApi.VERSION_TEMPLATE, 305 | mount = DocumentationRestApi.MOUNT_TEMPLATE) 306 | public class RestApi4 { 307 | } 308 | 309 | During documentation generation, the user can pass in the actual values as follows: 310 | 311 | java org.versly.rest.wsdoc.RestDocAssembler \ 312 | --template-id rest4 \ 313 | --template-title "REST API 4" \ 314 | --template-version v1 \ 315 | --template-mount /restapi4/api \ 316 | *.ser 317 | 318 | 319 | #### wsdoc in a Maven build environment 320 | 321 | 322 | 323 | 324 | 325 | org.bsc.maven 326 | maven-processor-plugin 327 | 1.3.6 328 | 329 | 330 | true 331 | 332 | org.versly.rest.wsdoc.AnnotationProcessor 333 | 334 | 335 | 336 | 337 | 338 | compile 339 | 340 | process 341 | 342 | 343 | 344 | 345 | 346 | 347 | org.versly 348 | versly-wsdoc 349 | 1.1-SNAPSHOT 350 | compile 351 | 352 | 353 | 354 | 355 | 357 | 358 | org.codehaus.mojo 359 | exec-maven-plugin 360 | 1.2 361 | 362 | 363 | 364 | prepare-package 365 | 366 | java 367 | 368 | 369 | org.versly.rest.wsdoc.RestDocAssembler 370 | 371 | ${project.build.directory}/classes 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | #### License 383 | 384 | wsdoc is licensed under the Apache Software License v2. The text of the license is available here: http://www.apache.org/licenses/LICENSE-2.0.html 385 | 386 | 387 | #### Releasing 388 | 389 | Take care to update the version number to be a SNAPSHOT or a non-SNAPSHOT as appropriate. 390 | Please build a commit that has the non-SNAPSHOT version, so we have it for posterity. 391 | 392 | To deploy, do the following: 393 | 394 | mvn clean && mvn deploy -P release 395 | 396 | This assumes that you've got gpg keys set up locally, and your `~/.m2/settings.xml` has 397 | credentials for the Sonatype repo. 398 | -------------------------------------------------------------------------------- /src/main/java/org/versly/rest/wsdoc/impl/RestDocumentation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 TaskDock, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.versly.rest.wsdoc.impl; 18 | 19 | import org.apache.commons.lang3.StringUtils; 20 | import org.versly.rest.wsdoc.DocumentationRestApi; 21 | 22 | import java.io.*; 23 | import java.util.*; 24 | import java.util.regex.Pattern; 25 | 26 | public class RestDocumentation implements Serializable { 27 | private static final long serialVersionUID = -3416760457544073264L; 28 | public static final String DEFAULT_API = "default"; 29 | 30 | private Map _apis = new LinkedHashMap(); 31 | 32 | public RestApi getRestApi(String apiBaseUrl) { 33 | if (!_apis.containsKey(apiBaseUrl)) 34 | _apis.put(apiBaseUrl, new RestApi(apiBaseUrl)); 35 | return _apis.get(apiBaseUrl); 36 | } 37 | 38 | public Collection getApis() { 39 | return _apis.values(); 40 | } 41 | 42 | /** 43 | * Read and return a serialized {@link RestDocumentation} instance from in, 44 | * as serialized by {@link #toStream}. 45 | */ 46 | public static RestDocumentation fromStream(InputStream in) 47 | throws IOException, ClassNotFoundException { 48 | ObjectInputStream ois = null; 49 | try { 50 | ois = new ObjectInputStream(in); 51 | return (RestDocumentation) ois.readObject(); 52 | } finally { 53 | if (ois != null) 54 | ois.close(); 55 | } 56 | } 57 | 58 | public void toStream(OutputStream out) throws IOException { 59 | ObjectOutputStream oos = new ObjectOutputStream(out); 60 | oos.writeObject(this); 61 | oos.flush(); 62 | } 63 | 64 | /** 65 | * This inspects the method paths and establishes parent/child relationships. This helps in particular 66 | * with generating RAML documentation, as RAML represents endpoints hierarchically. 67 | */ 68 | public void postProcess() { 69 | for (RestApi api : _apis.values()) { 70 | String mount = api.getMount(); 71 | if (null != mount && mount.length() > 0) { 72 | api.getResourceDocumentation(api.getMount()); 73 | } 74 | for (RestApi.Resource visitor : api.getResources()) { 75 | for (RestApi.Resource visitee : api.getResources()) { 76 | if (visitee != visitor && visitee.path.startsWith(visitor.path + "/") && 77 | (visitee._parent == null || visitee._parent.path.length() < visitor.path.length())) { 78 | if (visitee._parent != null) { 79 | visitee._parent._children.remove(visitee); 80 | } 81 | visitee._parent = visitor; 82 | visitor._children.add(visitee); 83 | } 84 | } 85 | } 86 | } 87 | } 88 | 89 | public class RestApi implements Serializable { 90 | private static final long serialVersionUID = 5665219205108618731L; 91 | public static final String DEFAULT_IDENTIFIER = "(default)"; 92 | 93 | private Map _resources = new LinkedHashMap(); 94 | private String _identifier; 95 | private String _apiMount; 96 | private String _apiTitle; 97 | private String _apiVersion; 98 | private String _apiDocumentation; 99 | private HashSet _traits = new HashSet(); 100 | 101 | public RestApi(String identifier) { 102 | _identifier = identifier; 103 | } 104 | 105 | public Object readResolve() throws ObjectStreamException { 106 | _identifier = Utils.fillTemplate(_identifier); 107 | _apiMount = Utils.fillTemplate(_apiMount); 108 | _apiTitle = Utils.fillTemplate(_apiTitle); 109 | _apiVersion = Utils.fillTemplate(_apiVersion); 110 | return this; 111 | } 112 | 113 | public String getIdentifier() { 114 | return _identifier; 115 | } 116 | 117 | public String getMount() { 118 | return _apiMount; 119 | } 120 | 121 | public void setMount(String apiBaseUrl) { 122 | if (null != apiBaseUrl && !apiBaseUrl.trim().isEmpty()) { 123 | _apiMount = apiBaseUrl; 124 | } 125 | } 126 | 127 | public String getApiTitle() { 128 | return _apiTitle; 129 | } 130 | 131 | public void setApiTitle(String apiTitle) { 132 | if (null != apiTitle && !apiTitle.trim().isEmpty()) { 133 | _apiTitle = apiTitle; 134 | } 135 | } 136 | 137 | public String getApiVersion() { 138 | return _apiVersion; 139 | } 140 | 141 | public void setApiVersion(String apiVersion) { 142 | if (null != apiVersion && !apiVersion.trim().isEmpty()) { 143 | _apiVersion = apiVersion; 144 | } 145 | } 146 | 147 | public String getApiDocumentation() { 148 | 149 | return _apiDocumentation; 150 | } 151 | 152 | public void setApiDocumentation(String apiDocumentation) { 153 | if (null != apiDocumentation && !apiDocumentation.trim().isEmpty()) { 154 | _apiDocumentation = apiDocumentation; 155 | } 156 | } 157 | 158 | public String getIndentedApiDocumentationText(int indent) { 159 | if (_apiDocumentation != null) { 160 | String whitespace = StringUtils.leftPad("", indent); 161 | return whitespace + _apiDocumentation.replaceAll("\n", "\n" + whitespace); 162 | } 163 | return ""; 164 | } 165 | 166 | public HashSet getTraits() { 167 | return _traits; 168 | } 169 | 170 | public void setTraits(HashSet traits) { 171 | this._traits = traits; 172 | } 173 | 174 | public String getIndentedApiTraits(int indent) { 175 | StringBuilder retval = new StringBuilder(); 176 | for (String trait : _traits) { 177 | retval.append(StringUtils.leftPad("", indent)); 178 | retval.append("- "); 179 | retval.append(trait); 180 | retval.append(":\n"); 181 | retval.append(StringUtils.leftPad("", 2*indent)); 182 | retval.append("description: TBD\n"); 183 | } 184 | return retval.toString(); 185 | } 186 | 187 | public Collection getResources() { 188 | return _resources.values(); 189 | } 190 | 191 | public void merge(RestApi api) { 192 | if (null == _apiTitle || _apiTitle.trim().isEmpty()) { 193 | _apiTitle = api.getApiTitle(); 194 | } 195 | if (null == _apiVersion || _apiVersion.trim().isEmpty()) { 196 | _apiVersion = api.getApiVersion(); 197 | } 198 | if (null == _apiMount || _apiMount.trim().isEmpty()) { 199 | _apiMount = api.getMount(); 200 | } 201 | if (null == _apiDocumentation || _apiDocumentation.trim().isEmpty()) { 202 | _apiDocumentation = api.getApiDocumentation(); 203 | } 204 | _resources.putAll(api._resources); 205 | _traits.addAll(api._traits); 206 | } 207 | 208 | public Resource getResourceDocumentation(String path) { 209 | if (!_resources.containsKey(path)) 210 | _resources.put(path, new Resource(path)); 211 | return _resources.get(path); 212 | } 213 | 214 | public RestApi filter(Iterable excludePatterns) { 215 | RestApi filtered = new RestApi(_identifier); 216 | filtered.setMount(_apiMount); 217 | filtered.setApiTitle(_apiTitle); 218 | filtered.setApiVersion(_apiVersion); 219 | filtered.setApiDocumentation(_apiDocumentation); 220 | filtered.setTraits(_traits); 221 | OUTER: 222 | for (Map.Entry entry : _resources.entrySet()) { 223 | for (Pattern excludePattern : excludePatterns) 224 | if (excludePattern.matcher(entry.getKey()).matches()) 225 | continue OUTER; 226 | 227 | filtered._resources.put(entry.getKey(), entry.getValue()); 228 | } 229 | return filtered; 230 | } 231 | 232 | public class Resource implements Serializable { 233 | private static final long serialVersionUID = -3436348850301436626L; 234 | 235 | private String path; 236 | private Map _methods = new LinkedHashMap(); 237 | private Resource _parent; 238 | private Collection _children = new LinkedList(); 239 | 240 | public Resource(String path) { 241 | this.path = path; 242 | } 243 | 244 | public String getPath() { 245 | return path; 246 | } 247 | 248 | public Collection getRequestMethodDocs() { 249 | return _methods.values(); 250 | } 251 | 252 | public Resource getParent() { 253 | return _parent; 254 | } 255 | 256 | ; 257 | 258 | public String getPathLeaf() { 259 | return (_parent != null) ? path.substring(_parent.path.length()) : path; 260 | } 261 | 262 | public Collection getChildren() { 263 | return _children; 264 | } 265 | 266 | /** 267 | * Creates and returns a new {@link Method} instance, and adds it to 268 | * the current resource location. If this is invoked multiple times 269 | * with the same argument, the same {@link Method} object 270 | * will be returned. 271 | */ 272 | public Method newMethodDocumentation(String meth) { 273 | if (_methods.containsKey(meth)) { 274 | return _methods.get(meth); 275 | } 276 | Method method = new Method(meth); 277 | _methods.put(meth, method); 278 | return method; 279 | } 280 | 281 | public UrlFields getResourceUrlSubstitutions() { 282 | UrlFields aggregateUrlFields = new UrlFields(); 283 | for (Method method : _methods.values()) { 284 | UrlFields fields = method.getMethodSpecificUrlSubstitutions(); 285 | aggregateUrlFields.getFields().putAll(fields.getFields()); 286 | } 287 | return aggregateUrlFields; 288 | } 289 | 290 | private Object readResolve() throws ObjectStreamException { 291 | path = Utils.fillTemplate(path); 292 | return this; 293 | } 294 | 295 | public class Method implements Serializable { 296 | 297 | private String _meth; 298 | private HashSet _docScopes; 299 | private HashSet _traits; 300 | private HashSet _authScopes; 301 | private JsonType _requestBody; 302 | private UrlFields _urlSubstitutions = new UrlFields(); 303 | private UrlFields _urlParameters = new UrlFields(); 304 | private JsonType _responseBody; 305 | private String _commentText; 306 | private boolean _isMultipartRequest; 307 | private String _requestSchema; 308 | private String _responseSchema; 309 | private String _responseExample; 310 | private String _requestExample; 311 | 312 | public HashSet getDocScopes() { 313 | return _docScopes; 314 | } 315 | 316 | public void setDocScopes(HashSet scopes) { 317 | this._docScopes = scopes; 318 | } 319 | 320 | public HashSet getTraits() { 321 | return _traits; 322 | } 323 | 324 | public String getTraitsAsString() { 325 | StringBuilder sb = new StringBuilder("[ "); 326 | for (String trait : _traits) { 327 | sb.append(trait).append(","); 328 | } 329 | sb.setCharAt(sb.length() - 1, ']'); 330 | return sb.toString(); 331 | } 332 | 333 | public void setTraits(HashSet traits) { 334 | this._traits = traits; 335 | } 336 | 337 | public HashSet getAuthScopes() { 338 | return _authScopes; 339 | } 340 | 341 | public String getAuthScopesAsString() { 342 | if (null != _authScopes && _authScopes.size() > 0) { 343 | StringBuilder sb = new StringBuilder(); 344 | sb.append("["); 345 | for (String authScope : _authScopes) { 346 | sb.append("\""); 347 | sb.append(authScope); 348 | sb.append("\","); 349 | } 350 | if (sb.length() > 1) { 351 | sb.setCharAt(sb.length() - 1, ']'); 352 | } 353 | else { 354 | sb.append("]"); 355 | } 356 | return sb.toString(); 357 | } 358 | return null; 359 | } 360 | 361 | public void setAuthScopes(HashSet _authScopes) { 362 | this._authScopes = _authScopes; 363 | } 364 | 365 | public String getResponseSchema() { 366 | return _responseSchema; 367 | } 368 | 369 | public void setResponseSchema(String _responseSchema) { 370 | this._responseSchema = _responseSchema; 371 | } 372 | 373 | public String getRequestSchema() { 374 | return _requestSchema; 375 | } 376 | 377 | public void setRequestSchema(String _requestSchema) { 378 | this._requestSchema = _requestSchema; 379 | } 380 | 381 | public void setResponseExample(String wsDocResponseSchema) { 382 | this._responseExample = wsDocResponseSchema; 383 | } 384 | 385 | public String getResponseExample() { 386 | return _responseExample; 387 | } 388 | 389 | public void setRequestExample(String wsDocRequestSchema) { 390 | this._requestExample = wsDocRequestSchema; 391 | } 392 | 393 | public String getRequestExample() { 394 | return _requestExample; 395 | } 396 | 397 | public Method(String meth) { 398 | this._meth = meth; 399 | } 400 | 401 | public String getRequestMethod() { 402 | return _meth; 403 | } 404 | 405 | public JsonType getRequestBody() { 406 | return _requestBody; 407 | } 408 | 409 | public void setRequestBody(JsonType body) { 410 | _requestBody = body; 411 | } 412 | 413 | public UrlFields getUrlSubstitutions() { 414 | return _urlSubstitutions; 415 | } 416 | 417 | /** 418 | * Get the URI parameters specific to this method (useful in RAML where the parent hierarchy will already include it's own) 419 | * 420 | * @return 421 | */ 422 | public UrlFields getMethodSpecificUrlSubstitutions() { 423 | Resource parent = _parent; 424 | Map methodFields = new HashMap(_urlSubstitutions.getFields()); 425 | while (parent != null) { 426 | Iterator iter = parent.getRequestMethodDocs().iterator(); 427 | while (iter.hasNext()) { 428 | for (String key : iter.next()._urlSubstitutions.getFields().keySet()) { 429 | methodFields.remove(key); 430 | } 431 | } 432 | parent = parent._parent; 433 | } 434 | UrlFields urlFields = new UrlFields(); 435 | urlFields.getFields().putAll(methodFields); 436 | return urlFields; 437 | } 438 | 439 | public UrlFields getUrlParameters() { 440 | return _urlParameters; 441 | } 442 | 443 | public JsonType getResponseBody() { 444 | return _responseBody; 445 | } 446 | 447 | public void setResponseBody(JsonType body) { 448 | _responseBody = body; 449 | } 450 | 451 | public String getCommentText() { 452 | return _commentText; 453 | } 454 | 455 | public String getIndentedCommentText(int indent) { 456 | if (_commentText != null) { 457 | String whitespace = StringUtils.leftPad("", indent); 458 | return whitespace + _commentText.split("\n @")[0].replaceAll("\n", "\n" + whitespace); 459 | } 460 | return null; 461 | } 462 | 463 | public void setCommentText(String text) { 464 | _commentText = text; 465 | } 466 | 467 | public boolean isMultipartRequest() { 468 | return _isMultipartRequest; 469 | } 470 | 471 | public void setMultipartRequest(boolean multipart) { 472 | _isMultipartRequest = multipart; 473 | } 474 | 475 | /** 476 | * An HTML-safe, textual key that uniquely identifies this endpoint. 477 | */ 478 | public String getKey() { 479 | String key = path + "_" + _meth; 480 | for (String param : _urlParameters.getFields().keySet()) { 481 | key += "_" + param; 482 | } 483 | return key; 484 | } 485 | } 486 | 487 | public class UrlFields implements Serializable { 488 | 489 | private Map _jsonFields = new LinkedHashMap(); 490 | 491 | public class UrlField implements Serializable { 492 | 493 | private JsonType fieldType; 494 | private String fieldDescription; 495 | 496 | public UrlField(JsonType type, String desc) { 497 | fieldType = type; 498 | fieldDescription = desc; 499 | } 500 | 501 | public JsonType getFieldType() { 502 | return fieldType; 503 | } 504 | 505 | public void setFieldType(JsonType fieldType) { 506 | this.fieldType = fieldType; 507 | } 508 | 509 | public String getFieldDescription() { 510 | return fieldDescription; 511 | } 512 | 513 | public void setFieldDescription(String fieldDescription) { 514 | this.fieldDescription = fieldDescription; 515 | } 516 | } 517 | 518 | public Map getFields() { 519 | return _jsonFields; 520 | } 521 | 522 | public void addField(String name, JsonType jsonType, String description) { 523 | _jsonFields.put(name, new UrlField(jsonType, description)); 524 | } 525 | } 526 | } 527 | } 528 | } 529 | --------------------------------------------------------------------------------