diff --git a/src/main/java/com/knappsack/swagger4springweb/annotation/ApiCategory.java b/src/main/java/com/knappsack/swagger4springweb/annotation/ApiCategory.java
new file mode 100644
index 0000000..207f89c
--- /dev/null
+++ b/src/main/java/com/knappsack/swagger4springweb/annotation/ApiCategory.java
@@ -0,0 +1,19 @@
+package com.knappsack.swagger4springweb.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * An annotation which indicates if this Api should be added to a category
+ *
+ * This annotation is applicable to the controller class
+ */
+@Target(ElementType.ANNOTATION_TYPE)
+@Retention(value = RetentionPolicy.RUNTIME)
+public @interface ApiCategory {
+ java.lang.String value();
+
+ java.lang.String description() default "";
+}
diff --git a/src/main/java/com/knappsack/swagger4springweb/parser/ApiOperationParser.java b/src/main/java/com/knappsack/swagger4springweb/parser/ApiOperationParser.java
index a1d1d21..42e0933 100644
--- a/src/main/java/com/knappsack/swagger4springweb/parser/ApiOperationParser.java
+++ b/src/main/java/com/knappsack/swagger4springweb/parser/ApiOperationParser.java
@@ -1,27 +1,35 @@
package com.knappsack.swagger4springweb.parser;
-import com.knappsack.swagger4springweb.util.JavaToScalaUtil;
-import com.wordnik.swagger.annotations.*;
-import com.wordnik.swagger.converter.ModelConverters;
-import com.wordnik.swagger.model.*;
-import com.wordnik.swagger.model.Authorization;
-import com.wordnik.swagger.model.AuthorizationScope;
-import org.apache.commons.lang.ArrayUtils;
-import org.springframework.http.HttpMethod;
-import org.springframework.util.StringUtils;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestMethod;
-import scala.Option;
+import static java.lang.String.format;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
+import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
-import static java.lang.String.format;
+import org.apache.commons.lang.ArrayUtils;
+import org.springframework.http.HttpMethod;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+
+import com.knappsack.swagger4springweb.util.JavaToScalaUtil;
+import com.wordnik.swagger.annotations.ApiOperation;
+import com.wordnik.swagger.annotations.ApiResponse;
+import com.wordnik.swagger.annotations.ApiResponses;
+import com.wordnik.swagger.converter.ModelConverters;
+import com.wordnik.swagger.model.Authorization;
+import com.wordnik.swagger.model.AuthorizationScope;
+import com.wordnik.swagger.model.Model;
+import com.wordnik.swagger.model.Operation;
+import com.wordnik.swagger.model.Parameter;
+import com.wordnik.swagger.model.ResponseMessage;
+
+import scala.Option;
public class ApiOperationParser {
@@ -51,6 +59,8 @@ public Operation parseDocumentationOperation(Method method) {
final Type type = parameterizedType.getActualTypeArguments()[0];
if (type instanceof ParameterizedType) {
documentationOperation.setResponseClass((Class>) ((ParameterizedType) type).getRawType());
+ } else if (type instanceof WildcardType) {
+ documentationOperation.setResponseClass(Object.class);
} else {
documentationOperation.setResponseClass((Class>) type);
}
diff --git a/src/main/java/com/knappsack/swagger4springweb/parser/ApiParserImpl.java b/src/main/java/com/knappsack/swagger4springweb/parser/ApiParserImpl.java
index 765ba99..c964b4d 100644
--- a/src/main/java/com/knappsack/swagger4springweb/parser/ApiParserImpl.java
+++ b/src/main/java/com/knappsack/swagger4springweb/parser/ApiParserImpl.java
@@ -1,27 +1,46 @@
package com.knappsack.swagger4springweb.parser;
+import static org.reflections.ReflectionUtils.withAnnotation;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+import org.reflections.Reflections;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.knappsack.swagger4springweb.annotation.ApiCategory;
import com.knappsack.swagger4springweb.controller.ApiDocumentationController;
import com.knappsack.swagger4springweb.filter.ApiExcludeFilter;
import com.knappsack.swagger4springweb.filter.Filter;
import com.knappsack.swagger4springweb.util.AnnotationUtils;
+import com.knappsack.swagger4springweb.util.ApiListingUtil;
import com.knappsack.swagger4springweb.util.JavaToScalaUtil;
import com.knappsack.swagger4springweb.util.ScalaToJavaUtil;
import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.config.SwaggerConfig;
-import com.wordnik.swagger.model.*;
-import org.reflections.Reflections;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-import scala.Option;
+import com.wordnik.swagger.model.ApiDescription;
+import com.wordnik.swagger.model.ApiInfo;
+import com.wordnik.swagger.model.ApiListing;
+import com.wordnik.swagger.model.ApiListingReference;
+import com.wordnik.swagger.model.Model;
+import com.wordnik.swagger.model.Operation;
+import com.wordnik.swagger.model.ResourceListing;
-import java.lang.annotation.Annotation;
-import java.lang.reflect.Method;
-import java.util.*;
-
-import static org.reflections.ReflectionUtils.withAnnotation;
+import scala.Option;
+import scala.Some;
public class ApiParserImpl implements ApiParser {
@@ -29,7 +48,7 @@ public class ApiParserImpl implements ApiParser {
private static final String swaggerVersion = com.wordnik.swagger.core.SwaggerSpec.version();
- private final Map apiListingMap = new HashMap();
+ private final Map apiListingMap = new TreeMap<>();
private final List controllerPackages;
private final List ignorableAnnotations;
@@ -70,7 +89,7 @@ public ResourceListing getResourceListing(Map apiListingMap)
List apiListingReferences = new ArrayList();
for (String key : apiListingMap.keySet()) {
ApiListing apiListing = apiListingMap.get(key);
- String docPath = "/doc"; //servletPath + "/doc"; //"/api/doc";
+ String docPath = "/doc"; //servletPath + "/doc"; //"/api/doc";
ApiListingReference apiListingReference = new ApiListingReference(docPath + key, apiListing.description(),
apiListing.position());
@@ -98,7 +117,7 @@ else if (o1.position() > o2.position())
swaggerConfig.info());
}
- public Map createApiListings() {
+ public Map createApiListings() {
Set> controllerClasses = new HashSet>();
for (String controllerPackage : controllerPackages) {
Reflections reflections = new Reflections(controllerPackage);
@@ -125,24 +144,43 @@ private Map processControllers(Set> controllerClass
.getAllMethods(controllerClass, withAnnotation(RequestMapping.class));
ApiListing apiListing = processControllerApi(controllerClass);
String description = "";
- Api controllerApi = controllerClass.getAnnotation(Api.class);
- if (controllerApi != null) {
+ ApiCategory apiCategory = AnnotationUtils.getAnnotationAnnotation(ApiCategory.class, controllerClass);
+ if (apiCategory != null) {
+ description = apiCategory.description();
+ } else {
+ Api controllerApi = controllerClass.getAnnotation(Api.class);
+ if (controllerApi != null) {
description = controllerApi.description();
+ }
}
-
if (apiListing.apis() == null) {
apiListing = processMethods(requestMappingMethods, controllerClass, apiListing, description);
}
+ if (apiCategory != null && apiCategory.value() != null) {
+ String key = prependSlashIfMissing(apiCategory.value());
+ ApiListing existingApiListing = apiListingMap.get(key);
+ if (existingApiListing != null) {
+ apiListing = ApiListingUtil.combine(existingApiListing, apiListing);
+ }
+
+ addApiListingToMap(apiListing, key);
+ continue;
+ }
// controllers without any operations are excluded from the apiListingMap list
if (apiListing.apis() != null && !apiListing.apis().isEmpty()) {
- apiListingMap.put(apiListing.resourcePath(), apiListing);
+ addApiListingToMap(apiListing, apiListing.resourcePath());
}
}
return apiListingMap;
}
+ private void addApiListingToMap(ApiListing apiListing, String key) {
+ apiListingMap.put(key, ApiListingUtil.sortApisByPath(apiListing));
+ }
+
+
private ApiListing processControllerApi(Class> controllerClass) {
String resourcePath = "";
Api controllerApi = controllerClass.getAnnotation(Api.class);
@@ -159,22 +197,39 @@ private ApiListing processControllerApi(Class> controllerClass) {
resourcePath = controllerClass.getName();
}
}
- if (!resourcePath.startsWith("/")) {
- resourcePath = "/" + resourcePath;
- }
+ resourcePath = prependSlashIfMissing(resourcePath);
- String docRoot = resourcePath;
+ String docRoot = resourcePath;
if(docRoot.contains(controllerClass.getName())) {
docRoot = docRoot.replace(controllerClass.getName(), "");
}
SpringApiReader reader = new SpringApiReader();
Option apiListingOption = reader.read(docRoot, controllerClass, swaggerConfig);
+
ApiListing apiListing = null;
+ Option descriptionOption = null;
+ String categoryValue = null;
+
+ ApiCategory category = AnnotationUtils.getAnnotationAnnotation(ApiCategory.class, controllerClass);
+ String description = null;
+ if (category != null) {
+ categoryValue = category.value();
+ description = category.description();
+ descriptionOption = Some.apply(description);
+ }
+
if (apiListingOption.nonEmpty()) {
- apiListing = apiListingOption.get();
+ apiListing = apiListingOption.get();
+ apiListing = ApiListingUtil.changeDescription(apiListing, description);
}
//Allow for multiple controllers having the same resource path.
+ if (categoryValue != null) {
+ ApiListing existingApiListing = apiListingMap.get(categoryValue);
+ if (existingApiListing != null) {
+ return existingApiListing;
+ }
+ }
ApiListing existingApiListing = apiListingMap.get(resourcePath);
if (existingApiListing != null) {
return existingApiListing;
@@ -185,7 +240,7 @@ private ApiListing processControllerApi(Class> controllerClass) {
}
return new ApiListing(apiVersion, swaggerVersion, basePath, resourcePath, null, null, null, null, null, null,
- null, 0);
+ descriptionOption, 0);
}
private ApiListing processMethods(Collection methods, Class> controllerClass, ApiListing apiListing, String description) {
@@ -269,4 +324,11 @@ private boolean ignore(Method method) {
return false;
}
+ private String prependSlashIfMissing(String key) {
+ if (!key.startsWith("/")) {
+ key = "/" + key;
+ }
+ return key;
+ }
+
}
diff --git a/src/main/java/com/knappsack/swagger4springweb/util/AnnotationUtils.java b/src/main/java/com/knappsack/swagger4springweb/util/AnnotationUtils.java
index 5e3df60..7aba25d 100644
--- a/src/main/java/com/knappsack/swagger4springweb/util/AnnotationUtils.java
+++ b/src/main/java/com/knappsack/swagger4springweb/util/AnnotationUtils.java
@@ -1,18 +1,19 @@
package com.knappsack.swagger4springweb.util;
-import com.google.common.collect.Lists;
-import com.knappsack.swagger4springweb.model.AnnotatedParameter;
-import com.thoughtworks.paranamer.BytecodeReadingParanamer;
-import com.thoughtworks.paranamer.Paranamer;
-import com.wordnik.swagger.annotations.ApiParam;
-import org.springframework.web.bind.annotation.RequestMapping;
-
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import com.google.common.collect.Lists;
+import com.knappsack.swagger4springweb.model.AnnotatedParameter;
+import com.thoughtworks.paranamer.BytecodeReadingParanamer;
+import com.thoughtworks.paranamer.Paranamer;
+import com.wordnik.swagger.annotations.ApiParam;
+
public class AnnotationUtils {
/**
@@ -84,4 +85,23 @@ public static List getAnnotatedParameters(Method method) {
}
return annotatedParameters;
}
+
+
+ public static T getAnnotationAnnotation(Class annotationClass, Class> controllerClass) {
+ Annotation[] annotations = controllerClass.getAnnotations();
+ for (Annotation annotation : annotations) {
+ T value = getAnnotation(annotation, annotationClass);
+ if (value != null) {
+ return value;
+ }
+ }
+ return null;
+ }
+
+ private static T getAnnotation(Annotation ann, Class annotationType) {
+ if (annotationType.isInstance(ann)) {
+ return (T) ann;
+ }
+ return ann.annotationType().getAnnotation(annotationType);
+ }
}
diff --git a/src/main/scala/com/knappsack/swagger4springweb/util/ApiListingUtil.scala b/src/main/scala/com/knappsack/swagger4springweb/util/ApiListingUtil.scala
new file mode 100644
index 0000000..b622b83
--- /dev/null
+++ b/src/main/scala/com/knappsack/swagger4springweb/util/ApiListingUtil.scala
@@ -0,0 +1,12 @@
+package com.knappsack.swagger4springweb.util
+
+import com.wordnik.swagger.model.ApiListing
+
+/**
+ * @author Allar Tammik
+ */
+object ApiListingUtil {
+ def combine(main: ApiListing, additional : ApiListing) : ApiListing = main.copy(apis = main.apis ++ additional.apis)
+ def sortApisByPath(obj: ApiListing) : ApiListing = obj.copy(apis = obj.apis.sortBy(api => api.path))
+ def changeDescription(obj: ApiListing, desc: String) : ApiListing = obj.copy(description = Some(desc))
+}
diff --git a/src/test/java/com/knappsack/swagger4springweb/AbstractTest.java b/src/test/java/com/knappsack/swagger4springweb/AbstractTest.java
index 4a0b48a..67f9f8e 100644
--- a/src/test/java/com/knappsack/swagger4springweb/AbstractTest.java
+++ b/src/test/java/com/knappsack/swagger4springweb/AbstractTest.java
@@ -1,14 +1,14 @@
package com.knappsack.swagger4springweb;
-import com.wordnik.swagger.model.ApiInfo;
-
import java.util.Arrays;
import java.util.List;
+import com.wordnik.swagger.model.ApiInfo;
+
public abstract class AbstractTest {
public static final String BASE_CONTROLLER_PACKAGE = "com.knappsack.swagger4springweb.testController";
public static final String EXCLUDE_LABEL = "exclude";
- public static final List END_POINT_PATHS = Arrays.asList("/doc/api/v1/partialExclude", "/doc/api/v1/test", "/doc/api/v1/exclude2", "/doc/com.knappsack.swagger4springweb.testController.NoClassLevelMappingController");
+ public static final List END_POINT_PATHS = Arrays.asList("/doc/api/v1/partialExclude", "/doc/api/v1/test", "/doc/api/v1/exclude2", "/doc/com.knappsack.swagger4springweb.testController.NoClassLevelMappingController", "/doc/api/v1/nocolor", "/doc/dark", "/doc/light");
public static final ApiInfo API_INFO = new ApiInfo("swagger4spring-web example app", "This is a basic web app for demonstrating swagger4spring-web", "http://localhost:8080/terms", "http://localhost:8080/contact", "MIT", "http://opensource.org/licenses/MIT");
}
diff --git a/src/test/java/com/knappsack/swagger4springweb/ApiCategoryTest.java b/src/test/java/com/knappsack/swagger4springweb/ApiCategoryTest.java
new file mode 100644
index 0000000..0afeadd
--- /dev/null
+++ b/src/test/java/com/knappsack/swagger4springweb/ApiCategoryTest.java
@@ -0,0 +1,83 @@
+package com.knappsack.swagger4springweb;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Test;
+
+import com.knappsack.swagger4springweb.parser.ApiParser;
+import com.knappsack.swagger4springweb.parser.ApiParserImpl;
+import com.wordnik.swagger.model.ApiDescription;
+import com.wordnik.swagger.model.ApiListing;
+import com.wordnik.swagger.model.ApiListingReference;
+import com.wordnik.swagger.model.ResourceListing;
+
+import scala.collection.Iterator;
+
+
+public class ApiCategoryTest extends AbstractTest {
+
+ @Test
+ public void category() {
+
+ ApiParser apiParser = createApiParser();
+ Map documents = apiParser.createApiListings();
+
+ assertEquals("api keys expected", 3, documents.size());
+ assertTrue(documents.containsKey("/dark"));
+ assertTrue(documents.containsKey("/light"));
+
+ ApiListing darkApiListing = documents.get("/dark");
+
+ scala.collection.immutable.List dark = darkApiListing.apis();
+ assertEquals("dark colors expected", 4, dark.size());
+
+ // Validate alphabetic ordering
+ assertTrue("earth should be first", dark.apply(0).path().equals("/api/v1/black/earth"));
+ assertTrue("wildcard should be second", dark.apply(1).path().equals("/api/v1/black/wildcard"));
+ assertTrue("x-planet should be 3rd", dark.apply(2).path().equals("/api/v1/black/x-planet"));
+ assertTrue("sky should be 4th", dark.apply(3).path().equals("/api/v1/blue/sky"));
+
+ assertEquals("This is so dark", darkApiListing.description().get());
+
+ }
+
+ @Test
+ public void resourceListing() {
+ ApiParser apiParser = createApiParser();
+ ResourceListing resourceListing = apiParser.getResourceListing(apiParser.createApiListings());
+ assertEquals(3, resourceListing.apis().size());
+ scala.collection.immutable.List apis = resourceListing.apis();
+
+
+ boolean foundDark = false;
+ boolean foundLight = false;
+ boolean foundNoColor = false;
+ for (Iterator it = apis.iterator(); it.hasNext(); ) {
+
+ ApiListingReference apiListingReference = it.next();
+ if (apiListingReference.path().equals("/doc/dark")) foundDark = true;
+ if (apiListingReference.path().equals("/doc/light")) foundLight = true;
+ if (apiListingReference.path().equals("/doc/api/v1/nocolor")) foundNoColor = true;
+ }
+ assertTrue("found dark category", foundDark);
+ assertTrue("found light category", foundLight);
+ assertTrue("found no color category", foundNoColor);
+ }
+
+
+ private ApiParser createApiParser() {
+ return createApiParser(Arrays.asList(BASE_CONTROLLER_PACKAGE + ".category"));
+ }
+
+ private ApiParser createApiParser(List controllerPackages) {
+ return new ApiParserImpl(API_INFO, controllerPackages, "http://localhost:8080/api", "/api", "v1",
+ new ArrayList(), true, null);
+ }
+
+}
diff --git a/src/test/java/com/knappsack/swagger4springweb/controller/ApiDocumentationControllerTest.java b/src/test/java/com/knappsack/swagger4springweb/controller/ApiDocumentationControllerTest.java
index 027844d..ca726e8 100644
--- a/src/test/java/com/knappsack/swagger4springweb/controller/ApiDocumentationControllerTest.java
+++ b/src/test/java/com/knappsack/swagger4springweb/controller/ApiDocumentationControllerTest.java
@@ -1,15 +1,18 @@
package com.knappsack.swagger4springweb.controller;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.web.servlet.HandlerMapping;
+
import com.knappsack.swagger4springweb.AbstractTest;
import com.knappsack.swagger4springweb.util.ScalaToJavaUtil;
import com.wordnik.swagger.model.ApiListing;
import com.wordnik.swagger.model.ApiListingReference;
import com.wordnik.swagger.model.ResourceListing;
-import org.junit.Test;
-import org.springframework.mock.web.MockHttpServletRequest;
-import org.springframework.web.servlet.HandlerMapping;
-
-import static org.junit.Assert.*;
public class ApiDocumentationControllerTest extends AbstractTest {
@@ -41,7 +44,6 @@ public void getResourceList() {
assertNotNull(documentation);
assertEquals("v1", documentation.apiVersion());
assertEquals(END_POINT_PATHS.size(), documentation.apis().size());
- assertEquals("/doc/api/v1/test", documentation.apis().apply(0).path());
for (ApiListingReference endPoint : ScalaToJavaUtil.toJavaList(documentation.apis())) {
assertTrue(END_POINT_PATHS.contains(endPoint.path()));
diff --git a/src/test/java/com/knappsack/swagger4springweb/testController/category/BlackController.java b/src/test/java/com/knappsack/swagger4springweb/testController/category/BlackController.java
new file mode 100644
index 0000000..8dd449f
--- /dev/null
+++ b/src/test/java/com/knappsack/swagger4springweb/testController/category/BlackController.java
@@ -0,0 +1,37 @@
+package com.knappsack.swagger4springweb.testController.category;
+
+
+import static java.util.Arrays.asList;
+import static org.springframework.web.bind.annotation.RequestMethod.GET;
+
+import java.util.List;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import com.wordnik.swagger.annotations.ApiOperation;
+
+@Controller
+@RequestMapping("/api/v1/black")
+@DarkCategory
+public class BlackController {
+
+ @ApiOperation("This is for testing ordering")
+ @RequestMapping(value = "/x-planet", method = GET)
+ public String xPlanet() {
+ return "x-planet";
+ }
+
+ @ApiOperation("Black is earth")
+ @RequestMapping(value = "/earth", method = GET)
+ public String earth() {
+ return "earth";
+ }
+
+ @ApiOperation("Wildcard list")
+ @RequestMapping(value = "/wildcard", method = GET)
+ public List extends Number> genericType() {
+ return asList(6L, 4L);
+ }
+
+}
diff --git a/src/test/java/com/knappsack/swagger4springweb/testController/category/BlueController.java b/src/test/java/com/knappsack/swagger4springweb/testController/category/BlueController.java
new file mode 100644
index 0000000..6f53a2e
--- /dev/null
+++ b/src/test/java/com/knappsack/swagger4springweb/testController/category/BlueController.java
@@ -0,0 +1,22 @@
+package com.knappsack.swagger4springweb.testController.category;
+
+import static org.springframework.web.bind.annotation.RequestMethod.GET;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import com.wordnik.swagger.annotations.Api;
+import com.wordnik.swagger.annotations.ApiOperation;
+
+@Controller
+@RequestMapping("/api/v1/blue")
+@Api(value = "/api/v1/blue", description = "Test Category, blue") // this will be ignored due to @ApiCategory
+@DarkCategory
+public class BlueController {
+
+ @ApiOperation("Blue is sky")
+ @RequestMapping(value = "/sky", method = GET)
+ public String sky() {
+ return "sky";
+ }
+}
diff --git a/src/test/java/com/knappsack/swagger4springweb/testController/category/DarkCategory.java b/src/test/java/com/knappsack/swagger4springweb/testController/category/DarkCategory.java
new file mode 100644
index 0000000..377ca39
--- /dev/null
+++ b/src/test/java/com/knappsack/swagger4springweb/testController/category/DarkCategory.java
@@ -0,0 +1,17 @@
+package com.knappsack.swagger4springweb.testController.category;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import com.knappsack.swagger4springweb.annotation.ApiCategory;
+
+/**
+ * @author Allar Tammik
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@ApiCategory(value = "dark", description = "This is so dark")
+public @interface DarkCategory {
+}
diff --git a/src/test/java/com/knappsack/swagger4springweb/testController/category/LightCategory.java b/src/test/java/com/knappsack/swagger4springweb/testController/category/LightCategory.java
new file mode 100644
index 0000000..cf45c20
--- /dev/null
+++ b/src/test/java/com/knappsack/swagger4springweb/testController/category/LightCategory.java
@@ -0,0 +1,17 @@
+package com.knappsack.swagger4springweb.testController.category;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import com.knappsack.swagger4springweb.annotation.ApiCategory;
+
+/**
+ * @author Allar Tammik
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@ApiCategory(value = "light", description = "This is so bright")
+public @interface LightCategory {
+}
diff --git a/src/test/java/com/knappsack/swagger4springweb/testController/category/NoColorController.java b/src/test/java/com/knappsack/swagger4springweb/testController/category/NoColorController.java
new file mode 100644
index 0000000..c07cf0c
--- /dev/null
+++ b/src/test/java/com/knappsack/swagger4springweb/testController/category/NoColorController.java
@@ -0,0 +1,22 @@
+package com.knappsack.swagger4springweb.testController.category;
+
+
+import static org.springframework.web.bind.annotation.RequestMethod.GET;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import com.wordnik.swagger.annotations.Api;
+import com.wordnik.swagger.annotations.ApiOperation;
+
+@Controller
+@RequestMapping("/api/v1/nocolor")
+@Api(value = "/api/v1/nocolor", description = "Test Category, this controller does not have ApiCategory annotation, thus will be mapped as earlier")
+public class NoColorController {
+
+ @RequestMapping(value = "/invisible", method = GET)
+ @ApiOperation("I am invisible")
+ public String invisible() {
+ return "invisible";
+ }
+}
diff --git a/src/test/java/com/knappsack/swagger4springweb/testController/category/WhiteController.java b/src/test/java/com/knappsack/swagger4springweb/testController/category/WhiteController.java
new file mode 100644
index 0000000..130b156
--- /dev/null
+++ b/src/test/java/com/knappsack/swagger4springweb/testController/category/WhiteController.java
@@ -0,0 +1,22 @@
+package com.knappsack.swagger4springweb.testController.category;
+
+import static org.springframework.web.bind.annotation.RequestMethod.GET;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import com.wordnik.swagger.annotations.Api;
+import com.wordnik.swagger.annotations.ApiOperation;
+
+@Controller
+@RequestMapping("/api/v1/white")
+@Api(value = "/api/v1/white",description = "Test Category, white")
+@LightCategory
+public class WhiteController {
+
+ @ApiOperation("White is fortune")
+ @RequestMapping(value = "/fortune", method = GET)
+ public String fortune() {
+ return "fortune";
+ }
+}