diff --git a/pom.xml b/pom.xml index 35bb4a2..e3af596 100644 --- a/pom.xml +++ b/pom.xml @@ -40,6 +40,7 @@ 1.2.4 1.6.4 1.0.1 + 1.5 @@ -143,6 +144,14 @@ + + org.apache.maven.plugins + maven-compiler-plugin + + ${java.version} + ${java.version} + + org.apache.maven.plugins maven-source-plugin diff --git a/src/main/java/com/knappsack/swagger4springweb/controller/ApiDocumentationController.java b/src/main/java/com/knappsack/swagger4springweb/controller/ApiDocumentationController.java index 6b69a71..f838e0f 100644 --- a/src/main/java/com/knappsack/swagger4springweb/controller/ApiDocumentationController.java +++ b/src/main/java/com/knappsack/swagger4springweb/controller/ApiDocumentationController.java @@ -26,10 +26,10 @@ public class ApiDocumentationController { private List additionalModelPackages = new ArrayList(); private String basePath = ""; private String apiVersion = "v1"; - private Documentation resourceList; private Map documentation; private List ignorableAnnotations = new ArrayList(); - + private boolean ignoreUnusedPathVariables = true; + private Documentation resourceList; @RequestMapping(value = "/resourceList", method = RequestMethod.GET, produces = "application/json") public @@ -46,11 +46,13 @@ Documentation getDocumentation(HttpServletRequest request) { HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); //trim the operation request mapping from the desired value handlerMappingPath = handlerMappingPath.substring(handlerMappingPath.lastIndexOf("/doc") + 4, handlerMappingPath.length()); - if (getDocs(request) == null) { + + Map docs = getDocs(request); + if (docs == null) { return new Documentation(); } - return getDocs(request).get(handlerMappingPath); + return docs.get(handlerMappingPath); } @SuppressWarnings("unused") @@ -139,8 +141,9 @@ private Map getDocs(HttpServletRequest request) { if(request != null) { servletPath = request.getServletPath(); } - ApiParser apiParser = new ApiParserImpl(getControllerPackages(), getModelPackages(), getBasePath(), servletPath, apiVersion, ignorableAnnotations); - this.documentation = apiParser.createDocuments(); + ApiParser apiParser = new ApiParserImpl(getControllerPackages(), getModelPackages(), getBasePath(), + servletPath, apiVersion, ignorableAnnotations, ignoreUnusedPathVariables); + documentation = apiParser.createDocuments(); } return documentation; } @@ -152,8 +155,9 @@ private Documentation getResourceList(HttpServletRequest request) { servletPath = request.getServletPath(); servletPath = servletPath.replace("/resourceList", ""); } - ApiParser apiParser = new ApiParserImpl(getControllerPackages(), getModelPackages(), getBasePath(), servletPath, apiVersion, ignorableAnnotations); - this.resourceList = apiParser.getResourceListing(getDocs(request)); + ApiParser apiParser = new ApiParserImpl(getControllerPackages(), getModelPackages(), getBasePath(), + servletPath, apiVersion, ignorableAnnotations, ignoreUnusedPathVariables); + resourceList = apiParser.getResourceListing(getDocs(request)); } return resourceList; } @@ -196,4 +200,12 @@ public List getIgnorableAnnotations() { public void setIgnorableAnnotations(List ignorableAnnotations) { this.ignorableAnnotations = ignorableAnnotations; } + + public boolean isIgnoreUnusedPathVariables() { + return ignoreUnusedPathVariables; + } + + public void setIgnoreUnusedPathVariables(final boolean ignoreUnusedPathVariables) { + this.ignoreUnusedPathVariables = ignoreUnusedPathVariables; + } } diff --git a/src/main/java/com/knappsack/swagger4springweb/parser/ApiParserImpl.java b/src/main/java/com/knappsack/swagger4springweb/parser/ApiParserImpl.java index 2b59724..cef37cd 100644 --- a/src/main/java/com/knappsack/swagger4springweb/parser/ApiParserImpl.java +++ b/src/main/java/com/knappsack/swagger4springweb/parser/ApiParserImpl.java @@ -33,13 +33,16 @@ public class ApiParserImpl implements ApiParser { private String servletPath = "/api"; private String apiVersion = "v1"; private List ignorableAnnotations; + private boolean ignoreUnusedPathVariables; private final Map documents = new HashMap(); - public ApiParserImpl(List baseControllerPackage, List baseModelPackage, String basePath, String servletPath, String apiVersion, List ignorableAnnotations) { + public ApiParserImpl(List baseControllerPackage, List baseModelPackage, String basePath, String servletPath, + String apiVersion, List ignorableAnnotations, boolean ignoreUnusedPathVariables) { this.controllerPackages = baseControllerPackage; this.modelPackages = baseModelPackage; this.ignorableAnnotations = ignorableAnnotations; + this.ignoreUnusedPathVariables = ignoreUnusedPathVariables; this.basePath = basePath; this.apiVersion = apiVersion; if (servletPath != null && !servletPath.isEmpty()) { @@ -117,8 +120,9 @@ private Documentation processControllerDocumentation(Class controllerClass) { } //Allow for multiple controllers having the same resource path. - if (documents.containsKey(resourcePath)){ - return documents.get(resourcePath); + Documentation documentation = documents.get(resourcePath); + if (documentation != null){ + return documentation; } return new Documentation(apiVersion, swaggerVersion, basePath, resourcePath); @@ -143,7 +147,7 @@ private void processMethods(Set methods, Documentation documentation, St String value = AnnotationUtils.getMethodRequestMappingValue(method); DocumentationEndPoint documentationEndPoint = endPointMap.get(value); - DocumentationOperationParser documentationOperationParser = new DocumentationOperationParser(ignorableAnnotations); + DocumentationOperationParser documentationOperationParser = new DocumentationOperationParser(documentation.resourcePath(), ignorableAnnotations, ignoreUnusedPathVariables); DocumentationOperation documentationOperation = documentationOperationParser.getDocumentationOperation(method); documentationEndPoint.addOperation(documentationOperation); @@ -159,7 +163,7 @@ private void processMethods(Set methods, Documentation documentation, St } } } - + private void populateEndpointMapForDocumentation(Documentation documentation, Map endPointMap){ if (documentation.getApis() != null){ for (DocumentationEndPoint endpoint : documentation.getApis()){ diff --git a/src/main/java/com/knappsack/swagger4springweb/parser/DocumentationOperationParser.java b/src/main/java/com/knappsack/swagger4springweb/parser/DocumentationOperationParser.java index 9470b59..b624e7c 100644 --- a/src/main/java/com/knappsack/swagger4springweb/parser/DocumentationOperationParser.java +++ b/src/main/java/com/knappsack/swagger4springweb/parser/DocumentationOperationParser.java @@ -10,19 +10,18 @@ import org.springframework.web.bind.annotation.RequestMethod; import java.lang.reflect.Method; -import java.util.ArrayList; import java.util.List; public class DocumentationOperationParser { + private String resourcePath; private List ignorableAnnotations; + private boolean ignoreUnusedPathVariables; - public DocumentationOperationParser() { - ignorableAnnotations = new ArrayList(); - } - - public DocumentationOperationParser(List ignorableAnnotations) { + public DocumentationOperationParser(String resourcePath, List ignorableAnnotations, boolean ignoreUnusedPathVariables) { this.ignorableAnnotations = ignorableAnnotations; + this.ignoreUnusedPathVariables = ignoreUnusedPathVariables; + this.resourcePath = resourcePath; } public DocumentationOperation getDocumentationOperation(Method method) { @@ -72,15 +71,39 @@ public DocumentationOperation getDocumentationOperation(Method method) { List documentationParameters = documentationParameterParser .getDocumentationParams(method); documentationOperation.setParameters(documentationParameters); + addUnusedPathVariables(documentationOperation, methodRequestMapping.value()); return documentationOperation; } - private void addError(DocumentationOperation documentationOperation, - ApiError apiError) { + private void addError(DocumentationOperation documentationOperation, ApiError apiError) { DocumentationError documentationError = new DocumentationError(); documentationError.setCode(apiError.code()); documentationError.setReason(apiError.reason()); documentationOperation.addErrorResponse(documentationError); } + + private void addUnusedPathVariables(DocumentationOperation documentationOperation, String[] methodPath) { + if(ignoreUnusedPathVariables){ + return; + } + + for(DocumentationParameter documentationParameter : new DocumentationPathParser().getPathParameters(resourcePath, methodPath)){ + if(!isParameterPresented(documentationOperation, documentationParameter.getName())){ + documentationOperation.addParameter(documentationParameter); + } + } + } + + private boolean isParameterPresented(DocumentationOperation documentationOperation, String parameter){ + if(parameter == null || documentationOperation.getParameters() == null){ + return false; + } + for(DocumentationParameter documentationParameter : documentationOperation.getParameters()){ + if(parameter.equals(documentationParameter.getName())){ + return true; + } + } + return false; + } } diff --git a/src/main/java/com/knappsack/swagger4springweb/parser/DocumentationParameterParser.java b/src/main/java/com/knappsack/swagger4springweb/parser/DocumentationParameterParser.java index 87e3357..a6b9150 100644 --- a/src/main/java/com/knappsack/swagger4springweb/parser/DocumentationParameterParser.java +++ b/src/main/java/com/knappsack/swagger4springweb/parser/DocumentationParameterParser.java @@ -2,18 +2,17 @@ import com.knappsack.swagger4springweb.model.AnnotatedParameter; import com.knappsack.swagger4springweb.util.AnnotationUtils; +import com.knappsack.swagger4springweb.util.DocumentationUtils; import com.wordnik.swagger.annotations.ApiParam; import com.wordnik.swagger.core.ApiValues; import com.wordnik.swagger.core.DocumentationAllowableListValues; import com.wordnik.swagger.core.DocumentationParameter; import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.List; public class DocumentationParameterParser { @@ -38,10 +37,10 @@ public List getDocumentationParams(Method method) { } DocumentationParameter documentationParameter = new DocumentationParameter(); // default values from the Method - documentationParameter.setDataType(getSwaggerTypeFor(annotatedParameter.getParameterType())); + documentationParameter.setDataType(DocumentationUtils.getSwaggerTypeFor(annotatedParameter.getParameterType())); documentationParameter.setName(annotatedParameter.getParameterName()); documentationParameter.setValueTypeInternal(annotatedParameter.getParameterType().getName()); - documentationParameter.setAllowMultiple(isAllowMultiple(annotatedParameter.getParameterType())); + documentationParameter.setAllowMultiple(DocumentationUtils.isAllowMultiple(annotatedParameter.getParameterType())); // apply default values from spring annotations first for (Annotation annotation : annotatedParameter.getAnnotations()) { addSpringParams(annotation, documentationParameter); @@ -58,41 +57,6 @@ public List getDocumentationParams(Method method) { return documentationParameters; } - private String getSwaggerTypeFor(Class parameterType) { - Class type = parameterType; - if(parameterType.isArray()) { - type = type.getComponentType(); - } - // swagger types are - // byte - // boolean - // int - // long - // float - // double - // string - // Date - if (String.class.isAssignableFrom(type)) { - return "string"; - } else if (Boolean.class.isAssignableFrom(type)) { - return "boolean"; - } else if(Byte.class.isAssignableFrom(type)) { - return "byte"; - } else if(Long.class.isAssignableFrom(type)) { - return "long"; - } else if(Integer.class.isAssignableFrom(type)) { - return "int"; - } else if(Float.class.isAssignableFrom(type)) { - return "float"; - } else if(MultipartFile.class.isAssignableFrom(type)) { - return "file"; - } else if (Number.class.isAssignableFrom(type)) { - return "double"; - } - // others - return type.getSimpleName(); - } - private void addSpringParams(Annotation annotation, DocumentationParameter documentationParameter) { if (annotation instanceof RequestParam) { addRequestParams((RequestParam) annotation, documentationParameter); @@ -114,7 +78,7 @@ private void addRequestBody(DocumentationParameter documentationParameter) { } private void addPathVariable(PathVariable pathVariable, DocumentationParameter documentationParameter) { - if (isSet(pathVariable.value())) { + if (DocumentationUtils.isSet(pathVariable.value())) { documentationParameter.setName(pathVariable.value()); } @@ -124,10 +88,10 @@ private void addPathVariable(PathVariable pathVariable, DocumentationParameter d private void addRequestParams(RequestParam requestParam, DocumentationParameter documentationParameter) { - if (isSet(requestParam.value())) { + if (DocumentationUtils.isSet(requestParam.value())) { documentationParameter.setName(requestParam.value()); } - if (isSet(requestParam.defaultValue())) { + if (DocumentationUtils.isSet(requestParam.defaultValue())) { documentationParameter.setDefaultValue(requestParam.defaultValue()); } documentationParameter.setRequired(requestParam.required()); @@ -138,16 +102,12 @@ private void addRequestParams(RequestParam requestParam, } } - private boolean isSet(String value) { - return value != null && !value.trim().isEmpty() && !ValueConstants.DEFAULT_NONE.equals(value); - } - private void addRequestHeader(RequestHeader requestHeader, DocumentationParameter documentationParameter) { - if (isSet(requestHeader.value())) { + if (DocumentationUtils.isSet(requestHeader.value())) { documentationParameter.setName(requestHeader.value()); } - if (isSet(requestHeader.defaultValue())) { + if (DocumentationUtils.isSet(requestHeader.defaultValue())) { documentationParameter.setDefaultValue(requestHeader.defaultValue()); } documentationParameter.setRequired(requestHeader.required()); @@ -156,21 +116,21 @@ private void addRequestHeader(RequestHeader requestHeader, private void addApiParams(ApiParam apiParam, DocumentationParameter documentationParameter) { - if (isSet(apiParam.allowableValues())) { + if (DocumentationUtils.isSet(apiParam.allowableValues())) { // we use only one simple string List allowableValues = Arrays.asList(apiParam.allowableValues().split("\\s*,\\s*")); documentationParameter.setAllowableValues(new DocumentationAllowableListValues(allowableValues)); } documentationParameter.setAllowMultiple(apiParam.allowMultiple()); - if (isSet(apiParam.defaultValue())) { + if (DocumentationUtils.isSet(apiParam.defaultValue())) { documentationParameter.setDefaultValue(apiParam.defaultValue()); } documentationParameter.setDescription(apiParam.value()); documentationParameter.setInternalDescription(apiParam .internalDescription()); // overwrite default name - if (isSet(apiParam.name())) { + if (DocumentationUtils.isSet(apiParam.name())) { documentationParameter.setName(apiParam.name()); } // documentationParameter.setNotes(apiParam.); @@ -182,10 +142,6 @@ private void addApiParams(ApiParam apiParam, } } - private boolean isAllowMultiple(Class parameterType) { - return parameterType != null && (parameterType.isArray() || Collection.class.isAssignableFrom(parameterType)); - } - private boolean hasIgnorableAnnotations(List annotations) { for(Annotation annotation : annotations) { if(ignorableAnnotations.contains(annotation.annotationType().getCanonicalName())) { diff --git a/src/main/java/com/knappsack/swagger4springweb/parser/DocumentationPathParser.java b/src/main/java/com/knappsack/swagger4springweb/parser/DocumentationPathParser.java new file mode 100644 index 0000000..c2d8ed2 --- /dev/null +++ b/src/main/java/com/knappsack/swagger4springweb/parser/DocumentationPathParser.java @@ -0,0 +1,95 @@ +package com.knappsack.swagger4springweb.parser; + +import com.knappsack.swagger4springweb.util.DocumentationUtils; +import com.wordnik.swagger.core.ApiValues; +import com.wordnik.swagger.core.DocumentationParameter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class DocumentationPathParser { + + private static final Pattern PATTERN = Pattern.compile("\\{([a-zA-Z]+)\\}"); + private static final Class TYPE = String.class; + + public List getPathParameters(String resourcePath, String[] methodPaths) { + Map parameters = new HashMap(); + + addParameters(parameters, resourcePath); + + if (methodPaths != null) { + for (String methodPath : methodPaths) { + addParameters(parameters, methodPath); + } + } + + return new ArrayList(parameters.values()); + } + + private void addParameters(Map parameters, String path) { + for (String parameter : getPathParameters(path)) { + parameters.put(parameter, createParameter(parameter)); + } + } + + private DocumentationParameter createParameter(String parameter) { + DocumentationParameter documentationParameter = new DocumentationParameter(); + documentationParameter.setName(parameter); + documentationParameter.setRequired(true); + documentationParameter.setDataType(DocumentationUtils.getSwaggerTypeFor(TYPE)); + documentationParameter.setValueTypeInternal(TYPE.getName()); + documentationParameter.setAllowMultiple(DocumentationUtils.isAllowMultiple(TYPE)); + documentationParameter.setParamType(ApiValues.TYPE_PATH); + documentationParameter.setDescription(getDescription(parameter)); + + return documentationParameter; + } + + private String getDescription(String parameter) { + StringBuffer buffer = new StringBuffer(); + + //Parameters with name length less then 3 will have description equal to their name + if (parameter == null || parameter.length() < 3) { + return parameter; + } + + String tmp = parameter; + + for (int i = 0; i < tmp.length(); i++) { + if (Character.isUpperCase(tmp.charAt(i))) { + if (buffer.length() == 0) { + // First letter is capital + buffer.append(tmp.substring(0, 1).toUpperCase()).append(tmp.substring(1, i)); + } else { + buffer.append(tmp.substring(0, i).toLowerCase()); + } + buffer.append(" "); + tmp = tmp.substring(i); + i = 0; + } + } + + return buffer.append(tmp.toLowerCase()).toString(); + } + + private List getPathParameters(String path) { + List parameters = new ArrayList(); + + if (path == null || path.trim().length() == 0) { + return Collections.emptyList(); + } + + Matcher matcher = PATTERN.matcher(path); + + while (matcher.find()) { + parameters.add(matcher.group(1)); + } + + return parameters; + } +} diff --git a/src/main/java/com/knappsack/swagger4springweb/util/DocumentationUtils.java b/src/main/java/com/knappsack/swagger4springweb/util/DocumentationUtils.java new file mode 100644 index 0000000..7752528 --- /dev/null +++ b/src/main/java/com/knappsack/swagger4springweb/util/DocumentationUtils.java @@ -0,0 +1,56 @@ +package com.knappsack.swagger4springweb.util; + +import org.springframework.web.bind.annotation.ValueConstants; +import org.springframework.web.multipart.MultipartFile; + +import java.util.Collection; + +/** + * Author: andrey.antonov + * Date: 7/3/13 + */ +public class DocumentationUtils { + + public static String getSwaggerTypeFor(Class parameterType) { + Class type = parameterType; + if(parameterType.isArray()) { + type = type.getComponentType(); + } + // swagger types are + // byte + // boolean + // int + // long + // float + // double + // string + // Date + if (String.class.isAssignableFrom(type)) { + return "string"; + } else if (Boolean.class.isAssignableFrom(type)) { + return "boolean"; + } else if(Byte.class.isAssignableFrom(type)) { + return "byte"; + } else if(Long.class.isAssignableFrom(type)) { + return "long"; + } else if(Integer.class.isAssignableFrom(type)) { + return "int"; + } else if(Float.class.isAssignableFrom(type)) { + return "float"; + } else if(MultipartFile.class.isAssignableFrom(type)) { + return "file"; + } else if (Number.class.isAssignableFrom(type)) { + return "double"; + } + // others + return type.getSimpleName(); + } + + public static boolean isSet(String value) { + return value != null && !value.trim().isEmpty() && !ValueConstants.DEFAULT_NONE.equals(value); + } + + public static boolean isAllowMultiple(Class parameterType) { + return parameterType != null && (parameterType.isArray() || Collection.class.isAssignableFrom(parameterType)); + } +} diff --git a/src/test/java/com/knappsack/swagger4springweb/ApiParserTest.java b/src/test/java/com/knappsack/swagger4springweb/ApiParserTest.java index a54ff2a..a6a3633 100644 --- a/src/test/java/com/knappsack/swagger4springweb/ApiParserTest.java +++ b/src/test/java/com/knappsack/swagger4springweb/ApiParserTest.java @@ -23,7 +23,8 @@ public void testParseControllerDocumentation() { controllerPackage.add(BASE_CONTROLLER_PACKAGE); List modelPackage = new ArrayList(); modelPackage.add(BASE_MODEL_PACKAGE); - ApiParser apiParser = new ApiParserImpl(controllerPackage, modelPackage, "http://localhost:8080/api", "/api", "v1", new ArrayList()); + ApiParser apiParser = new ApiParserImpl(controllerPackage, modelPackage, "http://localhost:8080/api", "/api", + "v1", new ArrayList(), true); Map documentList = apiParser.createDocuments(); for (String key : documentList.keySet()) { Documentation documentation = documentList.get(key); @@ -42,14 +43,14 @@ public void testParseControllerDocumentation() { } } - @Test public void testResourceListing() { List controllerPackage = new ArrayList(); controllerPackage.add(BASE_CONTROLLER_PACKAGE); List modelPackage = new ArrayList(); modelPackage.add(BASE_MODEL_PACKAGE); - ApiParser apiParser = new ApiParserImpl(controllerPackage, modelPackage, "http://localhost:8080/api", "/api", "v1", new ArrayList()); + ApiParser apiParser = new ApiParserImpl(controllerPackage, modelPackage, "http://localhost:8080/api", "/api", + "v1", new ArrayList(), true); Map documentList = apiParser.createDocuments(); Documentation resourceList = apiParser.getResourceListing(documentList); assertTrue(resourceList.basePath().equals("http://localhost:8080/api")); diff --git a/src/test/java/com/knappsack/swagger4springweb/parser/DocumentationPathParserTest.java b/src/test/java/com/knappsack/swagger4springweb/parser/DocumentationPathParserTest.java new file mode 100644 index 0000000..8e302cf --- /dev/null +++ b/src/test/java/com/knappsack/swagger4springweb/parser/DocumentationPathParserTest.java @@ -0,0 +1,61 @@ +package com.knappsack.swagger4springweb.parser; + +import com.wordnik.swagger.core.ApiValues; +import com.wordnik.swagger.core.DocumentationParameter; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class DocumentationPathParserTest { + + @Test + public void getPathParametersReturnsCorrectParametersList() { + String resourcePath = "/owner/{ownerName}/pets/{someOther}"; + String[] methodPaths = { "/dogs/{dogId}", "/legacy/path/to/dogs/breed/{dogId}" }; + + List parameters = new DocumentationPathParser() + .getPathParameters(resourcePath, methodPaths); + + assertEquals(3, parameters.size()); + assertTrue(contains(parameters, "ownerName")); + assertTrue(contains(parameters, "dogId")); + assertTrue(contains(parameters, "someOther")); + } + + @Test + public void getPathParametersReturnsCorrectParameterFields() { + String resourcePath = "/owner/{ownerName}/pets"; + + List parameters = new DocumentationPathParser().getPathParameters(resourcePath, null); + + assertEquals(1, parameters.size()); + + DocumentationParameter documentationParameter = parameters.get(0); + + assertEquals("ownerName", documentationParameter.getName()); + assertEquals(true, documentationParameter.getRequired()); + assertEquals(ApiValues.TYPE_PATH, documentationParameter.getParamType()); + assertEquals("string", documentationParameter.getDataType()); + assertEquals(String.class.getName(), documentationParameter.getValueTypeInternal()); + assertEquals(false, documentationParameter.getAllowMultiple()); + assertEquals("Owner name", documentationParameter.getDescription()); + } + + @Test + public void getPathParametersReturnsEmptyListWhenBadParametersArePassed() { + List parameters = new DocumentationPathParser().getPathParameters(null, null); + assertEquals(parameters.size(), 0); + } + + private boolean contains(List parameters, String parameter) { + for (DocumentationParameter documentationParameter : parameters) { + if (parameter.equals(documentationParameter.getName())) { + return true; + } + } + return false; + } +}