/**
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
*
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
* graphic logo is a trademark of OpenMRS Inc.
*/
package org.openmrs.module.webservices.docs.swagger;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Level;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.atteo.evo.inflector.English;
import org.openmrs.api.context.Context;
import org.openmrs.module.Module;
import org.openmrs.module.ModuleFactory;
import org.openmrs.module.webservices.docs.ResourceRepresentation;
import org.openmrs.module.webservices.docs.SearchHandlerDoc;
import org.openmrs.module.webservices.docs.SearchQueryDoc;
import org.openmrs.module.webservices.rest.SimpleObject;
import org.openmrs.module.webservices.rest.web.RequestContext;
import org.openmrs.module.webservices.rest.web.RestConstants;
import org.openmrs.module.webservices.rest.web.annotation.Resource;
import org.openmrs.module.webservices.rest.web.annotation.SubResource;
import org.openmrs.module.webservices.rest.web.api.RestService;
import org.openmrs.module.webservices.rest.web.representation.Representation;
import org.openmrs.module.webservices.rest.web.resource.api.SearchHandler;
import org.openmrs.module.webservices.rest.web.resource.api.SearchParameter;
import org.openmrs.module.webservices.rest.web.resource.api.SearchQuery;
import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceDescription;
import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceDescription.Property;
import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceHandler;
import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingSubclassHandler;
import org.openmrs.module.webservices.rest.web.response.ResourceDoesNotSupportOperationException;
import org.openmrs.util.OpenmrsConstants;
import org.springframework.util.ReflectionUtils;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
public class SwaggerSpecificationCreator {
private SwaggerSpecification swaggerSpecification;
private String baseUrl;
private static List<SearchHandlerDoc> searchHandlerDocs;
PrintStream originalErr;
PrintStream originalOut;
Map<Integer, Level> originalLevels = new HashMap<Integer, Level>();
Map<String, Definition> definitionMap = new HashMap<String, Definition>();
private Map<String, Tag> tags;
private Logger log = Logger.getLogger(this.getClass());
public SwaggerSpecificationCreator(String baseUrl) {
this.swaggerSpecification = new SwaggerSpecification();
this.baseUrl = baseUrl;
List<SearchHandler> searchHandlers = Context.getService(RestService.class).getAllSearchHandlers();
searchHandlerDocs = fillSearchHandlers(searchHandlers, baseUrl);
tags = new HashMap<String, Tag>();
}
public String BuildJSON() {
synchronized (this) {
log.info("Initiating Swagger specification creation");
toggleLogs(RestConstants.SWAGGER_LOGS_OFF);
try {
createApiDefinition();
addPaths();
addDefinitions();
addSubclassOperations();
}
catch (Exception e) {
log.error("Error while creating Swagger specification", e);
}
finally {
toggleLogs(RestConstants.SWAGGER_LOGS_ON);
log.info("Swagger specification creation complete");
}
}
return createJSON();
}
private void addDefinitions() {
Definitions definitions = new Definitions();
definitions.setDefinitions(definitionMap);
swaggerSpecification.setDefinitions(definitions);
}
private void toggleLogs(boolean targetState) {
if (Context.getAdministrationService().getGlobalProperty(RestConstants.SWAGGER_QUIET_DOCS_GLOBAL_PROPERTY_NAME)
.equals("true")) {
if (targetState == RestConstants.SWAGGER_LOGS_OFF) {
// turn off the log4j loggers
List<Logger> loggers = Collections.<Logger> list(LogManager.getCurrentLoggers());
loggers.add(LogManager.getRootLogger());
for (Logger logger : loggers) {
originalLevels.put(logger.hashCode(), logger.getLevel());
logger.setLevel(Level.OFF);
}
// silence stderr and stdout
originalErr = System.err;
System.setErr(new PrintStream(new OutputStream() {
public void write(int b) {
// noop
}
}));
originalOut = System.out;
System.setOut(new PrintStream(new OutputStream() {
public void write(int b) {
// noop
}
}));
} else if (targetState == RestConstants.SWAGGER_LOGS_ON) {
List<Logger> loggers = Collections.<Logger> list(LogManager.getCurrentLoggers());
loggers.add(LogManager.getRootLogger());
for (Logger logger : loggers) {
logger.setLevel(originalLevels.get(logger.hashCode()));
}
System.setErr(originalErr);
System.setOut(originalOut);
}
}
}
private void createApiDefinition() {
Info info = new Info();
// basic info
info.setVersion(OpenmrsConstants.OPENMRS_VERSION_SHORT);
info.setTitle("OpenMRS API Docs");
info.setDescription("OpenMRS RESTful API specification");
// contact
info.setContact(new Contact("OpenMRS", "http://openmrs.org"));
// license
info.setLicense(new License("MPL-2.0 w/ HD", "http://openmrs.org/license"));
// detailed versions
info.setVersions(new Versions(OpenmrsConstants.OPENMRS_VERSION, getModuleVersions()));
swaggerSpecification.setInfo(info);
// security definitions
SecurityDefinitions sd = new SecurityDefinitions();
sd.setBasicAuth(new SecurityScheme("basic", "HTTP basic access authentication using OpenMRS username and password"));
swaggerSpecification.setSecurityDefinitions(sd);
List<String> produces = new ArrayList<String>();
produces.add("application/json");
produces.add("application/xml");
List<String> consumes = new ArrayList<String>();
consumes.add("application/json");
// TODO: figure out how to post XML using Swagger UI
//consumes.add("application/xml");
swaggerSpecification.setHost(getBaseUrl());
swaggerSpecification.setBasePath("/" + RestConstants.VERSION_1);
swaggerSpecification.setProduces(produces);
swaggerSpecification.setConsumes(consumes);
}
private List<ModuleVersion> getModuleVersions() {
List<ModuleVersion> moduleVersions = new ArrayList<ModuleVersion>();
for (Module module : ModuleFactory.getLoadedModules()) {
moduleVersions.add(new ModuleVersion(module.getModuleId(), module.getVersion()));
}
return moduleVersions;
}
private boolean testOperationImplemented(OperationEnum operation, DelegatingResourceHandler<?> resourceHandler) {
Method method;
try {
switch (operation) {
case get:
method = ReflectionUtils.findMethod(resourceHandler.getClass(), "getAll", RequestContext.class);
if (method == null) {
return false;
} else {
method.invoke(resourceHandler, new RequestContext());
}
break;
case getSubresource:
method = ReflectionUtils.findMethod(resourceHandler.getClass(), "getAll", String.class,
RequestContext.class);
if (method == null) {
return false;
} else {
method.invoke(resourceHandler, RestConstants.SWAGGER_IMPOSSIBLE_UNIQUE_ID, new RequestContext());
}
break;
case getWithUUID:
case getSubresourceWithUUID:
method = ReflectionUtils.findMethod(resourceHandler.getClass(), "getByUniqueId", String.class);
if (method == null) {
return false;
} else {
method.invoke(resourceHandler, RestConstants.SWAGGER_IMPOSSIBLE_UNIQUE_ID);
}
break;
case getWithDoSearch:
method = ReflectionUtils.findMethod(resourceHandler.getClass(), "search", RequestContext.class);
if (method == null) {
return false;
} else {
method.invoke(resourceHandler, new RequestContext());
}
break;
case postCreate:
method = ReflectionUtils.findMethod(resourceHandler.getClass(), "create", SimpleObject.class,
RequestContext.class);
if (method == null) {
return false;
} else {
try {
// to avoid saving data to the database, we pass a null SimpleObject
method.invoke(resourceHandler, null, new RequestContext());
}
catch (ResourceDoesNotSupportOperationException re) {
return false;
}
catch (Exception ee) {
// if the resource doesn't immediate throw ResourceDoesNotSupportOperationException
// then we need to check if it's thrown in the save() method
resourceHandler.save(null);
}
}
break;
case postSubresource:
method = ReflectionUtils.findMethod(resourceHandler.getClass(), "create", String.class,
SimpleObject.class, RequestContext.class);
if (method == null) {
return false;
} else {
try {
// to avoid saving data to the database, we pass a null SimpleObject
method.invoke(resourceHandler, null, RestConstants.SWAGGER_IMPOSSIBLE_UNIQUE_ID,
new RequestContext());
}
catch (ResourceDoesNotSupportOperationException re) {
return false;
}
catch (Exception ee) {
// if the resource doesn't immediate throw ResourceDoesNotSupportOperationException
// then we need to check if it's thrown in the save() method
resourceHandler.save(null);
}
}
break;
case postUpdate:
method = ReflectionUtils.findMethod(resourceHandler.getClass(), "update", String.class,
SimpleObject.class, RequestContext.class);
if (method == null) {
return false;
} else {
method.invoke(resourceHandler, RestConstants.SWAGGER_IMPOSSIBLE_UNIQUE_ID,
buildPOSTUpdateSimpleObject(resourceHandler), new RequestContext());
}
break;
case postUpdateSubresouce:
method = ReflectionUtils.findMethod(resourceHandler.getClass(), "update", String.class, String.class,
SimpleObject.class, RequestContext.class);
if (method == null) {
return false;
} else {
method.invoke(resourceHandler, RestConstants.SWAGGER_IMPOSSIBLE_UNIQUE_ID,
RestConstants.SWAGGER_IMPOSSIBLE_UNIQUE_ID, buildPOSTUpdateSimpleObject(resourceHandler),
new RequestContext());
}
break;
case delete:
method = ReflectionUtils.findMethod(resourceHandler.getClass(), "delete", String.class, String.class,
RequestContext.class);
if (method == null) {
return false;
} else {
method.invoke(resourceHandler, RestConstants.SWAGGER_IMPOSSIBLE_UNIQUE_ID, new String(),
new RequestContext());
}
break;
case deleteSubresource:
method = ReflectionUtils.findMethod(resourceHandler.getClass(), "delete", String.class, String.class,
String.class, RequestContext.class);
if (method == null) {
return false;
} else {
method.invoke(resourceHandler, RestConstants.SWAGGER_IMPOSSIBLE_UNIQUE_ID,
RestConstants.SWAGGER_IMPOSSIBLE_UNIQUE_ID, new String(), new RequestContext());
}
break;
case purge:
method = ReflectionUtils.findMethod(resourceHandler.getClass(), "purge", String.class,
RequestContext.class);
if (method == null) {
return false;
} else {
method.invoke(resourceHandler, RestConstants.SWAGGER_IMPOSSIBLE_UNIQUE_ID, new RequestContext());
}
break;
case purgeSubresource:
method = ReflectionUtils.findMethod(resourceHandler.getClass(), "purge", String.class, String.class,
RequestContext.class);
if (method == null) {
return false;
} else {
method.invoke(resourceHandler, RestConstants.SWAGGER_IMPOSSIBLE_UNIQUE_ID,
RestConstants.SWAGGER_IMPOSSIBLE_UNIQUE_ID, new RequestContext());
}
}
return true;
}
catch (Exception e) {
if (e instanceof ResourceDoesNotSupportOperationException
|| e.getCause() instanceof ResourceDoesNotSupportOperationException) {
return false;
} else {
return true;
}
}
}
private void sortResourceHandlers(List<DelegatingResourceHandler<?>> resourceHandlers) {
Collections.sort(resourceHandlers, new Comparator<DelegatingResourceHandler<?>>() {
@Override
public int compare(DelegatingResourceHandler<?> left, DelegatingResourceHandler<?> right) {
return isSubclass(left).compareTo(isSubclass(right));
}
private Boolean isSubclass(DelegatingResourceHandler<?> resourceHandler) {
return resourceHandler.getClass().getAnnotation(SubResource.class) != null;
}
});
}
private void addResourceTag(String tagString) {
if (!tags.containsKey(tagString)) {
Tag tag = new Tag();
tag.setName(tagString);
tags.put(tagString, tag);
}
}
private ResourceRepresentation getGETRepresentation(DelegatingResourceHandler<?> resourceHandler) {
ResourceRepresentation getRepresentation = null;
try {
// first try the full representation
getRepresentation = new ResourceRepresentation("GET", resourceHandler
.getRepresentationDescription(Representation.FULL).getProperties().keySet());
return getRepresentation;
}
catch (Exception e) {
// don't panic
}
try {
// next try the default representation
getRepresentation = new ResourceRepresentation("GET", resourceHandler
.getRepresentationDescription(Representation.DEFAULT).getProperties().keySet());
return getRepresentation;
}
catch (Exception e) {
// don't panic
}
return getRepresentation;
}
private ResourceRepresentation getPOSTCreateRepresentation(DelegatingResourceHandler<?> resourceHandler) {
ResourceRepresentation postCreateRepresentation = null;
try {
DelegatingResourceDescription description = resourceHandler.getCreatableProperties();
List<String> properties = getPOSTProperties(description);
postCreateRepresentation = new ResourceRepresentation("POST create", properties);
}
catch (Exception e) {
// don't panic
}
return postCreateRepresentation;
}
private SimpleObject buildPOSTUpdateSimpleObject(DelegatingResourceHandler<?> resourceHandler) {
SimpleObject simpleObject = new SimpleObject();
for (String property : resourceHandler.getUpdatableProperties().getProperties().keySet()) {
simpleObject.put(property, property);
}
return simpleObject;
}
private ResourceRepresentation getPOSTUpdateRepresentation(DelegatingResourceHandler<?> resourceHandler) {
ResourceRepresentation postCreateRepresentation = null;
try {
DelegatingResourceDescription description = resourceHandler.getUpdatableProperties();
List<String> properties = getPOSTProperties(description);
postCreateRepresentation = new ResourceRepresentation("POST update", properties);
}
catch (Exception e) {
// don't panic
}
return postCreateRepresentation;
}
private Path buildFetchAllPath(Path path, DelegatingResourceHandler<?> resourceHandler, String resourceName,
String resourceParentName) {
ResourceRepresentation getRepresentation = getGETRepresentation(resourceHandler);
if (getRepresentation != null) {
Operation getOperation = null;
if (resourceParentName == null) {
if (testOperationImplemented(OperationEnum.get, resourceHandler)) {
getOperation = createOperation(resourceHandler, "get", resourceName, resourceParentName,
getRepresentation, OperationEnum.get);
}
} else {
if (testOperationImplemented(OperationEnum.getSubresource, resourceHandler)) {
getOperation = createOperation(resourceHandler, "get", resourceName, resourceParentName,
getRepresentation, OperationEnum.getSubresource);
}
}
if (getOperation != null) {
Map<String, Operation> operationsMap = path.getOperations();
String tag = resourceParentName == null ? resourceName : resourceParentName;
tag = tag.replaceAll("/", "_");
addResourceTag(tag);
getOperation.setTags(Arrays.asList(tag));
operationsMap.put("get", getOperation);
path.setOperations(operationsMap);
}
}
return path;
}
private Path buildGetWithUUIDPath(Path path, DelegatingResourceHandler<?> resourceHandler, String resourceName,
String resourceParentName) {
ResourceRepresentation getRepresentation = getGETRepresentation(resourceHandler);
if (getRepresentation != null) {
Operation getOperation = null;
if (testOperationImplemented(OperationEnum.getWithUUID, resourceHandler)) {
if (resourceParentName == null) {
getOperation = createOperation(resourceHandler, "get", resourceName, resourceParentName,
getRepresentation, OperationEnum.getWithUUID);
} else {
getOperation = createOperation(resourceHandler, "get", resourceName, resourceParentName,
getRepresentation, OperationEnum.getSubresourceWithUUID);
}
}
if (getOperation != null) {
Map<String, Operation> operationsMap = path.getOperations();
String tag = resourceParentName == null ? resourceName : resourceParentName;
tag = tag.replaceAll("/", "_");
addResourceTag(tag);
getOperation.setTags(Arrays.asList(tag));
operationsMap.put("get", getOperation);
path.setOperations(operationsMap);
}
}
return path;
}
private Path buildCreatePath(Path path, DelegatingResourceHandler<?> resourceHandler, String resourceName,
String resourceParentName) {
ResourceRepresentation postCreateRepresentation = getPOSTCreateRepresentation(resourceHandler);
if (postCreateRepresentation != null) {
Operation postCreateOperation = null;
if (resourceParentName == null) {
if (testOperationImplemented(OperationEnum.postCreate, resourceHandler)) {
postCreateOperation = createOperation(resourceHandler, "post", resourceName, resourceParentName,
postCreateRepresentation, OperationEnum.postCreate);
}
} else {
if (testOperationImplemented(OperationEnum.postSubresource, resourceHandler)) {
postCreateOperation = createOperation(resourceHandler, "post", resourceName, resourceParentName,
postCreateRepresentation, OperationEnum.postSubresource);
}
}
if (postCreateOperation != null) {
Map<String, Operation> operationsMap = path.getOperations();
String tag = resourceParentName == null ? resourceName : resourceParentName;
tag = tag.replaceAll("/", "_");
addResourceTag(tag);
postCreateOperation.setTags(Arrays.asList(tag));
operationsMap.put("post", postCreateOperation);
path.setOperations(operationsMap);
}
}
return path;
}
private Path buildUpdatePath(Path path, DelegatingResourceHandler<?> resourceHandler, String resourceName,
String resourceParentName) {
ResourceRepresentation postUpdateRepresentation = getPOSTUpdateRepresentation(resourceHandler);
if (postUpdateRepresentation != null) {
Operation postUpdateOperation = null;
if (resourceParentName == null) {
if (testOperationImplemented(OperationEnum.postUpdate, resourceHandler)) {
postUpdateOperation = createOperation(resourceHandler, "post", resourceName, resourceParentName,
postUpdateRepresentation, OperationEnum.postUpdate);
}
} else {
if (testOperationImplemented(OperationEnum.postUpdateSubresouce, resourceHandler)) {
postUpdateOperation = createOperation(resourceHandler, "post", resourceName, resourceParentName,
postUpdateRepresentation, OperationEnum.postUpdateSubresouce);
}
}
if (postUpdateOperation != null) {
Map<String, Operation> operationsMap = path.getOperations();
String tag = resourceParentName == null ? resourceName : resourceParentName;
tag = tag.replaceAll("/", "_");
addResourceTag(tag);
postUpdateOperation.setTags(Arrays.asList(tag));
operationsMap.put("post", postUpdateOperation);
path.setOperations(operationsMap);
}
}
return path;
}
private Path buildDeletePath(Path path, DelegatingResourceHandler<?> resourceHandler, String resourceName,
String resourceParentName) {
Operation deleteOperation = null;
if (resourceParentName == null) {
if (testOperationImplemented(OperationEnum.delete, resourceHandler)) {
deleteOperation = createOperation(resourceHandler, "delete", resourceName, resourceParentName,
new ResourceRepresentation("delete", new ArrayList()), OperationEnum.delete);
}
} else {
if (testOperationImplemented(OperationEnum.deleteSubresource, resourceHandler)) {
deleteOperation = createOperation(resourceHandler, "delete", resourceName, resourceParentName,
new ResourceRepresentation("delete", new ArrayList()), OperationEnum.deleteSubresource);
}
}
if (deleteOperation != null) {
Map<String, Operation> operationsMap = path.getOperations();
String tag = resourceParentName == null ? resourceName : resourceParentName;
tag = tag.replaceAll("/", "_");
addResourceTag(tag);
deleteOperation.setTags(Arrays.asList(tag));
operationsMap.put("delete", deleteOperation);
path.setOperations(operationsMap);
}
return path;
}
private Path buildPurgePath(Path path, DelegatingResourceHandler<?> resourceHandler, String resourceName,
String resourceParentName) {
if (path.getOperations().containsKey("delete")) {
// just add optional purge parameter
Operation deleteOperation = path.getOperations().get("delete");
deleteOperation.setSummary("Delete or purge resource by uuid");
deleteOperation.setDescription("The resource will be voided/retired unless purge = 'true'");
Parameter purge = new Parameter();
purge.setName("purge");
purge.setIn("query");
purge.setType("boolean");
List<Parameter> parameterList = deleteOperation.getParameters() == null ? new ArrayList<Parameter>()
: deleteOperation.getParameters();
parameterList.add(purge);
deleteOperation.setParameters(parameterList);
} else {
// create standalone purge operation with required
Operation purgeOperation = null;
if (resourceParentName == null) {
if (testOperationImplemented(OperationEnum.purge, resourceHandler)) {
purgeOperation = createOperation(resourceHandler, "delete", resourceName, resourceParentName,
new ResourceRepresentation("purge", new ArrayList()), OperationEnum.purge);
}
} else {
if (testOperationImplemented(OperationEnum.purgeSubresource, resourceHandler)) {
purgeOperation = createOperation(resourceHandler, "delete", resourceName, resourceParentName,
new ResourceRepresentation("purge", new ArrayList()), OperationEnum.purgeSubresource);
}
}
if (purgeOperation != null) {
Map<String, Operation> operationsMap = path.getOperations();
String tag = resourceParentName == null ? resourceName : resourceParentName;
tag = tag.replaceAll("/", "_");
addResourceTag(tag);
purgeOperation.setTags(Arrays.asList(tag));
operationsMap.put("delete", purgeOperation);
path.setOperations(operationsMap);
}
}
return path;
}
private void addIndividualPath(Map<String, Path> pathMap, Path pathCheck, String resourceParentName,
String resourceName, Path path, String pathSuffix) {
if (pathCheck != null) {
if (resourceParentName == null) {
pathMap.put("/" + resourceName + pathSuffix, path);
} else {
pathMap.put("/" + resourceParentName + "/{parent-uuid}/" + resourceName + pathSuffix, path);
}
}
}
private String buildSearchParameterDependencyString(Set<SearchParameter> dependencies) {
StringBuffer sb = new StringBuffer();
sb.append("Must be used with ");
sb.append(StringUtils.join(dependencies, ", "));
String ret = sb.toString();
int ind = ret.lastIndexOf(", ");
if (ind > -1) {
ret = new StringBuilder(ret).replace(ind, ind + 2, " or ").toString();
}
return ret;
}
private void addSearchOperations(DelegatingResourceHandler<?> resourceHandler, String resourceName,
String resourceParentName, Path getAllPath, Map<String, Path> pathMap) {
if (resourceName == null) {
return;
}
boolean hasDoSearch = testOperationImplemented(OperationEnum.getWithDoSearch, resourceHandler);
boolean hasSearchHandler = hasSearchHandler(resourceName);
boolean wasNew = false;
if (hasSearchHandler || hasDoSearch) {
// if the path has no operations, add a note that at least one search parameter must be specified
Operation get;
if (getAllPath.getOperations().isEmpty() || getAllPath.getOperations().get("get") == null) {
// create search-only operation
get = new Operation();
get.setName("get");
get.setSummary("Search for " + resourceName);
get.setDescription("At least one search parameter must be specified");
// produces
List<String> produces = new ArrayList<String>();
produces.add("application/json");
produces.add("application/xml");
get.setProduces(produces);
// schema
Response statusOKResponse = new Response();
statusOKResponse.setDescription(resourceName + " response");
Schema schema = new Schema();
// response
statusOKResponse.setSchema(schema);
List<String> resourceTags = new ArrayList<String>();
resourceTags.add(resourceName);
get.setTags(resourceTags);
Map<String, Response> responses = new HashMap<String, Response>();
responses.put("200", statusOKResponse);
get.setResponses(responses);
// if path has no existing get operations then it is considered new
wasNew = true;
} else {
get = getAllPath.getOperations().get("get");
get.setSummary("Fetch all non-retired " + resourceName + " resources or perform search");
get.setDescription("All search parameters are optional");
}
Map<String, Parameter> parameterMap = new HashMap<String, Parameter>();
if (hasSearchHandler) {
// FIXME: this isn't perfect, it doesn't cover the case where multiple parameters are required together
// FIXME: See https://github.com/OAI/OpenAPI-Specification/issues/256
for (SearchHandler searchHandler : Context.getService(RestService.class).getAllSearchHandlers()) {
String supportedResourceWithVersion = searchHandler.getSearchConfig().getSupportedResource();
String supportedResource = supportedResourceWithVersion.substring(supportedResourceWithVersion
.indexOf('/') + 1);
if (resourceName.equals(supportedResource)) {
for (SearchQuery searchQuery : searchHandler.getSearchConfig().getSearchQueries()) {
// parameters with no dependencies
for (SearchParameter requiredParameter : searchQuery.getRequiredParameters()) {
Parameter p = new Parameter();
p.setName(requiredParameter.getName());
p.setIn("query");
parameterMap.put(requiredParameter.getName(), p);
}
// parameters with dependencies
for (SearchParameter requiredParameter : searchQuery.getOptionalParameters()) {
Parameter p = new Parameter();
p.setName(requiredParameter.getName());
p.setDescription(buildSearchParameterDependencyString(searchQuery.getRequiredParameters()));
p.setIn("query");
parameterMap.put(requiredParameter.getName(), p);
}
}
}
}
}
// representations query parameter
Parameter v = new Parameter();
v.setName("v");
v.setDescription("The representation to return (ref, default, full or custom)");
v.setIn("query");
v.setType("string");
parameterMap.put("v", v);
// query parameter
Parameter q = new Parameter();
q.setName("q");
q.setDescription("The search query");
q.setIn("query");
q.setType("string");
if (wasNew && !hasSearchHandler) {
// This implies that the resource has no custom SearchHandler or doGetAll, but has doSearch implemented
// As there is only one query param 'q', mark it as required
q.setRequired(true);
}
parameterMap.put("q", q);
get.setParameters(new ArrayList(parameterMap.values()));
get.getParameters().addAll(buildPagingParameters());
get.setOperationId("getAll" + getOperationTitle(resourceHandler, true));
if (wasNew) {
getAllPath.getOperations().put("get", get);
addIndividualPath(pathMap, getAllPath, resourceParentName, resourceName, getAllPath, "");
}
}
}
private void addPaths() {
Map<String, Path> pathMap = new HashMap<String, Path>();
// get all registered resource handlers
List<DelegatingResourceHandler<?>> resourceHandlers = Context.getService(RestService.class).getResourceHandlers();
sortResourceHandlers(resourceHandlers);
// generate swagger JSON for each handler
for (DelegatingResourceHandler<?> resourceHandler : resourceHandlers) {
// get name and parent if it's a subresource
Resource annotation = resourceHandler.getClass().getAnnotation(Resource.class);
String resourceParentName = null;
String resourceName = null;
if (annotation != null) {
// top level resource
resourceName = annotation.name().substring(annotation.name().indexOf('/') + 1, annotation.name().length());
} else {
// subresource
SubResource subResourceAnnotation = resourceHandler.getClass().getAnnotation(SubResource.class);
if (subResourceAnnotation != null) {
Resource parentResourceAnnotation = subResourceAnnotation.parent().getAnnotation(Resource.class);
resourceName = subResourceAnnotation.path();
resourceParentName = parentResourceAnnotation.name().substring(
parentResourceAnnotation.name().indexOf('/') + 1, parentResourceAnnotation.name().length());
}
}
// subclass operations are handled separately in another method
if (resourceHandler instanceof DelegatingSubclassHandler)
continue;
// set up paths
Path rootPath = new Path();
rootPath.setOperations(new HashMap<String, Operation>());
Path uuidPath = new Path();
uuidPath.setOperations(new HashMap<String, Operation>());
/////////////////////////
// GET all //
/////////////////////////
Path rootPathGetAll = buildFetchAllPath(rootPath, resourceHandler, resourceName, resourceParentName);
addIndividualPath(pathMap, rootPathGetAll, resourceParentName, resourceName, rootPathGetAll, "");
/////////////////////////
// GET search //
/////////////////////////
addSearchOperations(resourceHandler, resourceName, resourceParentName, rootPathGetAll, pathMap);
/////////////////////////
// POST create //
/////////////////////////
Path rootPathPostCreate = buildCreatePath(rootPathGetAll, resourceHandler, resourceName, resourceParentName);
addIndividualPath(pathMap, rootPathPostCreate, resourceParentName, resourceName, rootPathPostCreate, "");
/////////////////////////
// GET with UUID //
/////////////////////////
Path uuidPathGetAll = buildGetWithUUIDPath(uuidPath, resourceHandler, resourceName, resourceParentName);
addIndividualPath(pathMap, uuidPathGetAll, resourceParentName, resourceName, uuidPathGetAll, "/{uuid}");
/////////////////////////
// POST update //
/////////////////////////
Path uuidPathPostUpdate = buildUpdatePath(uuidPathGetAll, resourceHandler, resourceName, resourceParentName);
addIndividualPath(pathMap, uuidPathGetAll, resourceParentName, resourceName, uuidPathPostUpdate, "/{uuid}");
/////////////////////////
// DELETE //
/////////////////////////
Path uuidPathDelete = buildDeletePath(uuidPathPostUpdate, resourceHandler, resourceName, resourceParentName);
//addIndividualPath(pathMap, uuidPathDelete, resourceParentName, resourceName, uuidPathDelete, "/{uuid}");
/////////////////////////
// DELETE (purge) //
/////////////////////////
Path uuidPathPurge = buildPurgePath(uuidPathDelete, resourceHandler, resourceName, resourceParentName);
addIndividualPath(pathMap, uuidPathPurge, resourceParentName, resourceName, uuidPathPurge, "/{uuid}");
}
Paths paths = new Paths();
paths.setPaths(pathMap);
swaggerSpecification.setPaths(paths);
ArrayList<Tag> tagList = new ArrayList<Tag>(tags.values());
Collections.sort(tagList);
swaggerSpecification.setTags(tagList);
}
private void addSubclassOperations() {
// FIXME: this needs to be improved a lot
List<DelegatingResourceHandler<?>> resourceHandlers = Context.getService(RestService.class).getResourceHandlers();
for (DelegatingResourceHandler<?> resourceHandler : resourceHandlers) {
if (!(resourceHandler instanceof DelegatingSubclassHandler))
continue;
Class<?> resourceClass = ((DelegatingSubclassHandler<?, ?>) resourceHandler).getSuperclass();
String resourceName = resourceClass.getSimpleName().toLowerCase();
if (resourceName == null)
continue;
// 1. add non-optional enum property to model
Path path = swaggerSpecification.getPaths().getPaths().get("/" + resourceName);
if (path == null)
continue;
// FIXME: implement other operations when required
Operation post = path.getOperations().get("post");
if (post == null)
continue;
Definition definition = swaggerSpecification.getDefinitions().getDefinitions()
.get(StringUtils.capitalize(resourceName) + "Create");
if (definition == null)
continue;
Properties properties = definition.getProperties();
Map<String, DefinitionProperty> props = properties.getProperties();
DefinitionProperty type = props.get("type");
if (type == null) {
type = new DefinitionProperty();
properties.addProperty("type", type);
type.setType("string");
definition.addRequired("type");
}
type.addEnumerationItem(((DelegatingSubclassHandler) resourceHandler).getTypeName());
// 2. merge subclass properties into definition
for (String prop : resourceHandler.getRepresentationDescription(Representation.FULL).getProperties().keySet()) {
if (props.get(prop) == null) {
DefinitionProperty dp = new DefinitionProperty();
dp.setType("string");
props.put(prop, dp);
}
}
// 3. update description
post.setDescription("Certain properties may be required depending on type");
}
}
private static List<String> getPOSTProperties(DelegatingResourceDescription description) {
List<String> properties = new ArrayList<String>();
for (Entry<String, Property> property : description.getProperties().entrySet()) {
if (property.getValue().isRequired()) {
properties.add("*" + property.getKey() + "*");
} else {
properties.add(property.getKey());
}
}
return properties;
}
private List<Parameter> getParametersListForSearchHandlers(String resourceName, String searchHandlerId, int queryIndex) {
List<Parameter> parameters = new ArrayList<Parameter>();
String resourceURL = getResourceUrl(getBaseUrl(), resourceName);
for (SearchHandlerDoc searchDoc : searchHandlerDocs) {
if (searchDoc.getSearchHandlerId().equals(searchHandlerId) && searchDoc.getResourceURL().equals(resourceURL)) {
SearchQueryDoc queryDoc = searchDoc.getSearchQueriesDoc().get(queryIndex);
for (SearchParameter requiredParameter : queryDoc.getRequiredParameters()) {
Parameter parameter = new Parameter();
parameter.setName(requiredParameter.getName());
parameter.setIn("query");
parameter.setDescription("");
parameter.setRequired(true);
parameters.add(parameter);
}
for (SearchParameter optionalParameter : queryDoc.getOptionalParameters()) {
Parameter parameter = new Parameter();
parameter.setName(optionalParameter.getName());
parameter.setIn("query");
parameter.setDescription("");
parameter.setRequired(false);
parameters.add(parameter);
}
break;
}
}
return parameters;
}
private String createJSON() {
String json = "";
try {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
mapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, true);
mapper.setSerializationInclusion(Include.NON_NULL);
mapper.getSerializerProvider().setNullKeySerializer(new NullSerializer());
json = mapper.writeValueAsString(swaggerSpecification);
}
catch (Exception exp) {
exp.printStackTrace();
}
return json;
}
private Parameter buildRequiredUUIDParameter(String name, String label) {
Parameter parameter = new Parameter();
parameter.setName(name);
parameter.setIn("path");
parameter.setDescription(label);
parameter.setRequired(true);
return parameter;
}
private List<Parameter> buildPagingParameters() {
List<Parameter> pagingParams = new ArrayList<Parameter>();
Parameter limit = new Parameter();
limit.setName("limit");
limit.setDescription("The number of results to return");
limit.setIn("query");
limit.setType("integer");
pagingParams.add(limit);
Parameter startIndex = new Parameter();
startIndex.setName("startIndex");
startIndex.setDescription("The offset at which to start");
startIndex.setIn("query");
startIndex.setType("integer");
pagingParams.add(startIndex);
return pagingParams;
}
private Parameter buildPOSTBodyParameter(String resourceName, String resourceParentName, OperationEnum operationEnum) {
Parameter parameter = new Parameter();
Schema bodySchema = new Schema();
parameter.setIn("body");
parameter.setRequired(true);
parameter.setSchema(bodySchema);
switch (operationEnum) {
case postCreate:
case postSubresource:
parameter.setName("resource");
parameter.setDescription("Resource to create");
break;
case postUpdate:
case postUpdateSubresouce:
parameter.setName("resource");
parameter.setDescription("Resource properties to update");
}
bodySchema.setRef(getSchemaRef(resourceName, resourceParentName, operationEnum));
return parameter;
}
private String getSchemaName(String resourceName, String resourceParentName, OperationEnum operationEnum) {
String suffix = "";
switch (operationEnum) {
case get:
case getSubresource:
case getWithUUID:
case getSubresourceWithUUID:
suffix = "Get";
break;
case postCreate:
case postSubresource:
suffix = "Create";
break;
case postUpdate:
case postUpdateSubresouce:
suffix = "Update";
break;
}
String modelRefName;
if (resourceParentName == null) {
modelRefName = StringUtils.capitalize(resourceName) + suffix;
} else {
modelRefName = StringUtils.capitalize(resourceParentName) + StringUtils.capitalize(resourceName) + suffix;
}
// get rid of slashes in model names
String[] split = modelRefName.split("\\/");
String ret = "";
for (String s : split) {
ret += StringUtils.capitalize(s);
}
return ret;
}
private String getSchemaRef(String resourceName, String resourceParentName, OperationEnum operationEnum) {
return "#/definitions/" + getSchemaName(resourceName, resourceParentName, operationEnum);
}
private String getModelTitle(String schemaName) {
if (schemaName.toLowerCase().endsWith("get")) {
return schemaName.substring(0, schemaName.length() - 3);
} else if (schemaName.toLowerCase().endsWith("create") || schemaName.toLowerCase().endsWith("update")) {
return schemaName.substring(0, schemaName.length() - 6);
}
return schemaName;
}
private String getOperationTitle(DelegatingResourceHandler<?> resourceHandler, Boolean pluralize) {
StringBuilder ret = new StringBuilder();
English inflector = new English();
// get rid of slashes
String simpleClassName = resourceHandler.getClass().getSimpleName();
// get rid of 'Resource' and version number suffixes
simpleClassName = simpleClassName.replaceAll("\\d_\\d{1,2}$", "");
simpleClassName = simpleClassName.replaceAll("Resource$", "");
// pluralize if require
if (pluralize) {
String[] words = simpleClassName.split("(?<!(^|[A-Z]))(?=[A-Z])|(?<!^)(?=[A-Z][a-z])");
String suffix = words[words.length - 1];
for (int i = 0; i < words.length - 1; i++) {
ret.append(words[i]);
}
ret.append(inflector.getPlural(suffix));
} else {
ret.append(simpleClassName);
}
return ret.toString();
}
private void createDefinition(OperationEnum operationEnum, String resourceName, String resourceParentName,
ResourceRepresentation representation) {
String definitionName = getSchemaName(resourceName, resourceParentName, operationEnum);
Definition definition = new Definition();
definition.setType("object");
Xml xml = new Xml();
xml.setName(getModelTitle(getSchemaName(resourceName, resourceParentName, operationEnum).toLowerCase()));
definition.setXml(xml);
Properties props = new Properties();
definition.setProperties(props);
Collection<String> properties = representation.getProperties();
for (String property : properties) {
DefinitionProperty defProp = new DefinitionProperty();
String propName;
if (property.startsWith("*")) {
propName = property.replace("*", "");
definition.addRequired(propName);
} else {
propName = property;
}
defProp.setType("string");
props.addProperty(propName, defProp);
}
definitionMap.put(definitionName, definition);
}
private Operation createOperation(DelegatingResourceHandler<?> resourceHandler, String operationName,
String resourceName, String resourceParentName, ResourceRepresentation representation,
OperationEnum operationEnum) {
Map<String, Response> responses = new HashMap<String, Response>();
Operation operation = new Operation();
operation.setName(operationName);
operation.setDescription(null);
List<String> produces = new ArrayList<String>();
produces.add("application/json");
produces.add("application/xml");
operation.setProduces(produces);
List<Parameter> parameters = new ArrayList<Parameter>();
operation.setParameters(parameters);
// create definition
if (operationName == "post" || operationName == "get") {
createDefinition(operationEnum, resourceName, resourceParentName, representation);
}
// 200 response (Successful operation)
Response statusOKResponse = new Response();
statusOKResponse.setDescription(resourceName + " response");
Schema responseBodySchema = new Schema();
// 201 response (Successfully created)
Response createdOKResponse = new Response();
createdOKResponse.setDescription(resourceName + " response");
createdOKResponse.setSchema(responseBodySchema);
// 204 delete success
Response deletedOKResponse = new Response();
deletedOKResponse.setDescription("Delete successful");
// 401 response (User not logged in)
Response notLoggedInResponse = new Response();
notLoggedInResponse.setDescription("User not logged in");
// 404 (Object with given uuid doesn't exist)
Response notFoundResponse = new Response();
notFoundResponse.setDescription("Resource with given uuid doesn't exist");
// representations query parameter
Parameter v = new Parameter();
v.setName("v");
v.setDescription("The representation to return (ref, default, full or custom)");
v.setIn("query");
v.setType("string");
// query parameter
Parameter q = new Parameter();
q.setName("q");
q.setDescription("The search query");
q.setIn("query");
q.setType("string");
if (operationEnum == OperationEnum.get) {
operation.setSummary("Fetch all non-retired");
operation.setOperationId("getAll" + getOperationTitle(resourceHandler, true));
responseBodySchema.setRef(getSchemaRef(resourceName, resourceParentName, OperationEnum.get));
parameters.add(v);
parameters.add(q);
parameters.addAll(buildPagingParameters());
statusOKResponse.setSchema(responseBodySchema);
responses.put("200", statusOKResponse);
} else if (operationEnum == OperationEnum.getWithUUID) {
operation.setSummary("Fetch by uuid");
operation.setOperationId("get" + getOperationTitle(resourceHandler, false));
responseBodySchema.setRef(getSchemaRef(resourceName, resourceParentName, OperationEnum.getWithUUID));
parameters.add(buildRequiredUUIDParameter("uuid", "uuid to filter by"));
parameters.add(v);
statusOKResponse.setSchema(responseBodySchema);
responses.put("200", statusOKResponse);
responses.put("404", notFoundResponse);
} else if (operationEnum == OperationEnum.postCreate) {
operation.setSummary("Create with properties in request");
operation.setOperationId("create" + getOperationTitle(resourceHandler, false));
responseBodySchema.setRef(getSchemaRef(resourceName, resourceParentName, OperationEnum.get));
parameters.add(buildPOSTBodyParameter(resourceName, resourceParentName, OperationEnum.postCreate));
responses.put("201", createdOKResponse);
} else if (operationEnum == OperationEnum.postUpdate) {
operation.setSummary("Edit with given uuid, only modifying properties in request");
operation.setOperationId("update" + getOperationTitle(resourceHandler, false));
responseBodySchema.setRef(getSchemaRef(resourceName, resourceParentName, OperationEnum.get));
parameters.add(buildRequiredUUIDParameter("uuid", "uuid of resource to update"));
parameters.add(buildPOSTBodyParameter(resourceName, resourceParentName, OperationEnum.postUpdate));
responses.put("201", createdOKResponse);
} else if (operationEnum == OperationEnum.getSubresource) {
operation.setSummary("Fetch all non-retired " + resourceName + " subresources");
operation.setOperationId("getAll" + getOperationTitle(resourceHandler, true));
parameters.add(buildRequiredUUIDParameter("parent-uuid", "parent resource uuid"));
responseBodySchema.setRef(getSchemaRef(resourceName, resourceParentName, OperationEnum.get));
parameters.add(v);
parameters.add(q);
parameters.addAll(buildPagingParameters());
statusOKResponse.setSchema(responseBodySchema);
responses.put("200", statusOKResponse);
} else if (operationEnum == OperationEnum.postSubresource) {
operation.setSummary("Create " + resourceName + " subresource with properties in request");
operation.setOperationId("create" + getOperationTitle(resourceHandler, false));
parameters.add(buildRequiredUUIDParameter("parent-uuid", "parent resource uuid"));
responseBodySchema.setRef(getSchemaRef(resourceName, resourceParentName, OperationEnum.get));
parameters.add(buildPOSTBodyParameter(resourceName, resourceParentName, OperationEnum.postSubresource));
responses.put("201", createdOKResponse);
} else if (operationEnum == OperationEnum.postUpdateSubresouce) {
operation.setSummary("Edit " + resourceName
+ " subresource with given uuid, only modifying properties in request");
operation.setOperationId("update" + getOperationTitle(resourceHandler, false));
parameters.add(buildRequiredUUIDParameter("parent-uuid", "parent resource uuid"));
parameters.add(buildRequiredUUIDParameter("uuid", "uuid of resource to update"));
responseBodySchema.setRef(getSchemaRef(resourceName, resourceParentName, OperationEnum.get));
parameters.add(buildPOSTBodyParameter(resourceName, resourceParentName, OperationEnum.postUpdateSubresouce));
responses.put("201", createdOKResponse);
} else if (operationEnum == OperationEnum.getSubresourceWithUUID) {
operation.setSummary("Fetch " + resourceName + " subresources by uuid");
operation.setOperationId("get" + getOperationTitle(resourceHandler, false));
responseBodySchema.setRef(getSchemaRef(resourceName, resourceParentName, OperationEnum.getSubresourceWithUUID));
parameters.add(buildRequiredUUIDParameter("parent-uuid", "parent resource uuid"));
parameters.add(buildRequiredUUIDParameter("uuid", "uuid to filter by"));
parameters.add(v);
statusOKResponse.setSchema(responseBodySchema);
responses.put("200", statusOKResponse);
responses.put("404", notFoundResponse);
} else if (operationEnum == OperationEnum.delete) {
operation.setSummary("Delete resource by uuid");
operation.setOperationId("delete" + getOperationTitle(resourceHandler, false));
statusOKResponse.setDescription("Successful operation");
parameters.add(buildRequiredUUIDParameter("uuid", "uuid to delete"));
responses.put("204", deletedOKResponse);
responses.put("404", notFoundResponse);
} else if (operationEnum == OperationEnum.deleteSubresource) {
operation.setSummary("Delete " + resourceName + " subresource by uuid");
operation.setOperationId("delete" + getOperationTitle(resourceHandler, false));
statusOKResponse.setDescription("Successful operation");
parameters.add(buildRequiredUUIDParameter("parent-uuid", "parent resource uuid"));
parameters.add(buildRequiredUUIDParameter("uuid", "uuid to delete"));
responses.put("204", deletedOKResponse);
responses.put("404", notFoundResponse);
} else if (operationEnum == OperationEnum.purge) {
operation.setSummary("Purge resource by uuid");
operation.setOperationId("purge" + getOperationTitle(resourceHandler, false));
statusOKResponse.setDescription("Successful operation");
parameters.add(buildRequiredUUIDParameter("uuid", "uuid to delete"));
responses.put("204", deletedOKResponse);
} else if (operationEnum == OperationEnum.purgeSubresource) {
operation.setSummary("Purge " + resourceName + " subresource by uuid");
operation.setOperationId("purge" + getOperationTitle(resourceHandler, false));
statusOKResponse.setDescription("Successful operation");
parameters.add(buildRequiredUUIDParameter("parent-uuid", "parent resource uuid"));
parameters.add(buildRequiredUUIDParameter("uuid", "uuid to delete"));
responses.put("204", deletedOKResponse);
}
List<String> resourceTags = new ArrayList<String>();
resourceTags.add(resourceName);
operation.setTags(resourceTags);
responses.put("401", notLoggedInResponse);
operation.setResponses(responses);
return operation;
}
private Operation createSearchHandlerOperation(String operationName, String resourceName, String searchHandlerId,
OperationEnum operationEnum, int queryIndex) {
Operation operation = new Operation();
operation.setName(operationName);
operation.setDescription(null);
List<String> produces = new ArrayList<String>();
produces.add("application/json");
operation.setProduces(produces);
operation.setIsSearchHandler("true");
List<Parameter> parameters = new ArrayList<Parameter>();
parameters = getParametersListForSearchHandlers(resourceName, searchHandlerId, queryIndex);
operation.setParameters(parameters);
Response statusOKResponse = new Response();
statusOKResponse.setDescription(resourceName + " response");
Schema schema = new Schema();
schema.setRef("#/definitions/" + resourceName);
statusOKResponse.setSchema(schema);
List<String> resourceTags = new ArrayList<String>();
resourceTags.add(resourceName);
operation.setTags(resourceTags);
Map<String, Response> responses = new HashMap<String, Response>();
responses.put("200", statusOKResponse);
operation.setResponses(responses);
String resourceURL = getResourceUrl(getBaseUrl(), resourceName);
for (SearchHandlerDoc searchDoc : searchHandlerDocs) {
if (searchDoc.getSearchHandlerId().equals(searchHandlerId) && searchDoc.getResourceURL().equals(resourceURL)) {
SearchQueryDoc queryDoc = searchDoc.getSearchQueriesDoc().get(queryIndex);
operation.setSummary(queryDoc.getDescription());
}
}
return operation;
}
private static List<SearchHandlerDoc> fillSearchHandlers(List<SearchHandler> searchHandlers, String url) {
List<SearchHandlerDoc> searchHandlerDocList = new ArrayList<SearchHandlerDoc>();
String baseUrl = url.replace("/rest", "");
for (int i = 0; i < searchHandlers.size(); i++) {
if (searchHandlers.get(i) != null) {
SearchHandler searchHandler = searchHandlers.get(i);
SearchHandlerDoc searchHandlerDoc = new SearchHandlerDoc(searchHandler, baseUrl);
searchHandlerDocList.add(searchHandlerDoc);
}
}
return searchHandlerDocList;
}
private String getResourceUrl(String baseUrl, String resourceName) {
String resourceUrl = baseUrl;
//Set the root url.
return resourceUrl + "/v1/" + resourceName;
}
private boolean hasSearchHandler(String resourceName) {
for (SearchHandlerDoc doc : searchHandlerDocs) {
if (doc.getResourceURL().contains(resourceName)) {
return true;
}
}
return false;
}
public String getBaseUrl() {
return baseUrl;
}
public SwaggerSpecification getSwaggerSpecification() {
return swaggerSpecification;
}
}