Java tutorial
/* * Copyright 2011 TaskDock, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.versly.rest.wsdoc; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.joda.JodaModule; import com.fasterxml.jackson.module.jsonSchema.factories.SchemaFactoryWrapper; import org.apache.commons.lang3.StringUtils; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.multipart.MultipartHttpServletRequest; import org.versly.rest.wsdoc.impl.*; import javax.annotation.processing.*; import javax.lang.model.SourceVersion; import javax.lang.model.element.*; import javax.lang.model.type.*; import javax.lang.model.util.AbstractTypeVisitor6; import javax.lang.model.util.ElementFilter; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; import javax.tools.Diagnostic; import javax.tools.FileObject; import javax.tools.StandardLocation; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.lang.annotation.Annotation; import java.util.*; import static org.apache.commons.lang3.StringUtils.isNotEmpty; import static org.apache.commons.lang3.StringUtils.join; /** * Generates an HTML documentation file describing the REST / JSON endpoints as defined with the * Spring {@link org.springframework.web.bind.annotation.RequestMapping} annotation. Outputs to <code>rest-api.html</code> in the top of the classes directory. */ // TODO: // - @CookieValue // - @RequestHeader // - @ResponseStatus // - combine class-level and method-level annotations properly // - MethodNameResolver // - plural RequestMapping value support (i.e., two paths bound to one method) // - support for methods not marked with @RequestMapping whose class does have a @RequestMapping annotation @SupportedAnnotationTypes({ "org.springframework.web.bind.annotation.RequestMapping", "javax.ws.rs.Path" }) @SupportedSourceVersion(SourceVersion.RELEASE_8) public class AnnotationProcessor extends AbstractProcessor { private RestDocumentation _docs = new RestDocumentation(); private boolean _isComplete = false; private Map<TypeMirror, JsonType> _memoizedTypeMirrors = new HashMap<TypeMirror, JsonType>(); private Map<DeclaredType, JsonType> _memoizedDeclaredTypes = new HashMap<DeclaredType, JsonType>(); private ProcessingEnvironment _processingEnv; private Types _typeUtils; @Override public void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); _processingEnv = processingEnv; _typeUtils = _processingEnv.getTypeUtils(); } @Override public boolean process(Set<? extends TypeElement> supportedAnnotations, RoundEnvironment roundEnvironment) { // short-circuit if there are multiple rounds if (_isComplete) return true; Collection<String> processedPackageNames = new LinkedHashSet<String>(); processElements(roundEnvironment, processedPackageNames, new SpringMVCRestImplementationSupport()); processElements(roundEnvironment, processedPackageNames, new JaxRSRestImplementationSupport()); processElements(roundEnvironment, processedPackageNames, new JavaEEWebsocketImplementationSupport()); _docs.postProcess(); if (_docs.getApis().size() > 0) { OutputStream fileOutput = null; try { FileObject file = getOutputFile(); boolean exists = new File(file.getName()).exists(); fileOutput = file.openOutputStream(); _docs.toStream(fileOutput); processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, String.format("Wrote REST docs for %s apis to %s file at %s", _docs.getApis().size(), exists ? "existing" : "new", file.getName())); } catch (Exception e) { throw new RuntimeException(e); // TODO wrap in something nicer } finally { if (fileOutput != null) { try { fileOutput.close(); } catch (IOException ignored) { // ignored } } } } _isComplete = true; return true; } private void processElements(RoundEnvironment roundEnvironment, Collection<String> processedPackageNames, RestImplementationSupport implementationSupport) { for (Element e : roundEnvironment .getElementsAnnotatedWith(implementationSupport.getMappingAnnotationType())) { if (e instanceof ExecutableElement) { addPackageName(processedPackageNames, e); processRequestMappingMethod((ExecutableElement) e, implementationSupport); } } } private void addPackageName(Collection<String> processedPackageNames, Element e) { processedPackageNames.add(processingEnv.getElementUtils().getPackageOf(e).getQualifiedName().toString()); } private FileObject getOutputFile() throws IOException { return this.processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", Utils.SERIALIZED_RESOURCE_LOCATION); } private void processRequestMappingMethod(ExecutableElement executableElement, RestImplementationSupport implementationSupport) { TypeElement cls = (TypeElement) executableElement.getEnclosingElement(); for (final String basePath : getClassLevelUrlPaths(cls, implementationSupport)) { for (final String requestPath : implementationSupport.getRequestPaths(executableElement, cls)) { String fullPath = Utils.joinPaths(basePath, requestPath); String meth; try { meth = implementationSupport.getRequestMethod(executableElement, cls); } catch (IllegalStateException ex) { // if something is bad with the request method annotations (no PATCH support currently, for example), // then just warn and continue, so the docs don't break the dev process. this.processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "error processing element: " + ex.getMessage(), executableElement); continue; } // both spring and jersey permit colon delimited regexes in path annotations which are not compatible with RAML // which expects resource identifiers to comply with RFC-6570 URI template semantics - so remove regex portion fullPath = fullPath.replaceAll(":.*}", "}"); // set documentation and metadata on api RestDocumentation.RestApi api = null; DocumentationRestApi apidoc = cls.getAnnotation(DocumentationRestApi.class); if (null != apidoc) { api = _docs.getRestApi(apidoc.id()); api.setApiTitle(apidoc.title()); api.setApiVersion(apidoc.version()); api.setMount(apidoc.mount()); } else { api = _docs.getRestApi(RestDocumentation.RestApi.DEFAULT_IDENTIFIER); api.setApiTitle(""); api.setApiVersion(""); api.setMount(""); } api.setApiDocumentation(processingEnv.getElementUtils().getDocComment(cls)); // set documentation text on method RestDocumentation.RestApi.Resource resource = api.getResourceDocumentation(fullPath); RestDocumentation.RestApi.Resource.Method method = resource.newMethodDocumentation(meth); method.setCommentText(processingEnv.getElementUtils().getDocComment(executableElement)); // set documentation scope on method (doc scope is non-scalar, methods can be part of multiple doc scopes) { HashSet<String> docScopes = new HashSet<String>(); DocumentationScope clsDocScopes = cls.getAnnotation(DocumentationScope.class); if (null != clsDocScopes) { docScopes.addAll(Arrays.asList(clsDocScopes.value())); } DocumentationScope methodDocScopes = executableElement.getAnnotation(DocumentationScope.class); if (null != methodDocScopes) { docScopes.addAll(Arrays.asList(methodDocScopes.value())); } method.setDocScopes(docScopes); } // set authorization scope on method (auth scope is non-scalar, methods can have multiple auth scopes) { HashSet<String> authScopes = new HashSet<String>(); AuthorizationScope clsAuthScopes = cls.getAnnotation(AuthorizationScope.class); if (null != clsAuthScopes) { authScopes.addAll(Arrays.asList(clsAuthScopes.value())); } AuthorizationScope methodAuthScopes = executableElement.getAnnotation(AuthorizationScope.class); if (null != methodAuthScopes) { authScopes.addAll(Arrays.asList(methodAuthScopes.value())); } method.setAuthScopes(authScopes); } // set traits on method (traits is non-scalar, methods may have multiple traits) { HashSet<String> traits = new HashSet<String>(method.getDocScopes()); DocumentationTraits clsTraits = cls.getAnnotation(DocumentationTraits.class); if (null != clsTraits) { traits.addAll(Arrays.asList(clsTraits.value())); } DocumentationTraits methodTraits = executableElement.getAnnotation(DocumentationTraits.class); if (null != methodTraits) { traits.addAll(Arrays.asList(methodTraits.value())); } method.setTraits(traits); } // add method's traits as included with overall API traits (used in RAML for uniform documentation) api.getTraits().addAll(method.getTraits()); // set path and query parameter information on method buildParameterData(executableElement, method, implementationSupport); // set response entity data information on method buildResponseFormat(unwrapReturnType(executableElement.getReturnType()), method); } } } private TypeMirror unwrapReturnType(TypeMirror originalReturnType) { if (originalReturnType.getKind() != TypeKind.DECLARED) { return originalReturnType; } DeclaredType declaredType = (DeclaredType) originalReturnType; if (declaredType.getTypeArguments().size() == 0) { return originalReturnType; } TypeElement element = (TypeElement) declaredType.asElement(); // For Spring's Async Support if ("org.springframework.web.context.request.async.DeferredResult" .equalsIgnoreCase(element.getQualifiedName().toString())) { return declaredType.getTypeArguments().get(0); } // For Spring's Async Support if ("java.util.concurrent.Callable".equalsIgnoreCase(element.getQualifiedName().toString())) { return declaredType.getTypeArguments().get(0); } return originalReturnType; } private void buildParameterData(ExecutableElement executableElement, RestDocumentation.RestApi.Resource.Method doc, RestImplementationSupport implementationSupport) { // only process @RequestBody, @PathVariable and @RequestParam parameters for now. // TODO Consider expanding this to include other Spring REST annotations. // We can safely ignore @RequestMapping.params, as Spring requires that a @RequestParam exists // for each entry listed in this list. I expect that this might be the same for @RequestMapping.headers scanForSpringMVCMultipart(executableElement, doc); scanForWebsocket(executableElement, doc); buildPathVariables(executableElement, doc, implementationSupport); buildUrlParameters(executableElement, doc, implementationSupport); buildPojoQueryParameters(executableElement, doc, implementationSupport); buildRequestBodies(executableElement, doc, implementationSupport); } /** * This is Spring-MVC only -- JAX-RS doesn't have an (obvious) analog. */ private void scanForSpringMVCMultipart(ExecutableElement executableElement, RestDocumentation.RestApi.Resource.Method doc) { for (VariableElement var : executableElement.getParameters()) { TypeMirror varType = var.asType(); if (varType.toString().startsWith(MultipartHttpServletRequest.class.getName())) { doc.setMultipartRequest(true); return; } } } private void scanForWebsocket(ExecutableElement executableElement, RestDocumentation.RestApi.Resource.Method doc) { for (VariableElement var : executableElement.getParameters()) { TypeMirror varType = var.asType(); if (varType.toString().equalsIgnoreCase("org.atmosphere.cpr.AtmosphereResource")) { doc.setWebsocket(true); return; } } } private void buildRequestBodies(ExecutableElement executableElement, RestDocumentation.RestApi.Resource.Method doc, RestImplementationSupport implementationSupport) { List<VariableElement> requestBodies = new ArrayList<VariableElement>(); for (VariableElement var : executableElement.getParameters()) { if (implementationSupport.isRequestBody(var)) requestBodies.add(var); } if (requestBodies.size() > 1) throw new IllegalStateException(String.format("Method %s in class %s has multiple @RequestBody params", executableElement.getSimpleName(), executableElement.getEnclosingElement())); if (requestBodies.size() == 1) buildRequestBody(requestBodies.get(0), doc); } private void buildRequestBody(VariableElement var, RestDocumentation.RestApi.Resource.Method doc) { doc.setRequestBody(jsonTypeFromTypeMirror(var.asType(), new HashSet<String>())); doc.setRequestSchema(jsonSchemaFromTypeMirror(var.asType())); doc.setRequestExample(exampleFromJsonType(doc.getRequestBody())); } private void buildPathVariables(ExecutableElement executableElement, RestDocumentation.RestApi.Resource.Method doc, RestImplementationSupport implementationSupport) { RestDocumentation.RestApi.Resource.UrlFields subs = doc.getUrlSubstitutions(); for (VariableElement var : executableElement.getParameters()) { String pathVariable = implementationSupport.getPathVariable(var); if (pathVariable != null) { String paramName = var.getSimpleName().toString(); addUrlField(subs, var, pathVariable, findParamDescription(paramName, doc.getCommentText())); } } } private void addUrlField(RestDocumentation.RestApi.Resource.UrlFields subs, VariableElement var, String annoValue, String description) { String name = (annoValue == null || annoValue.isEmpty()) ? var.getSimpleName().toString() : annoValue; subs.addField(name, jsonTypeFromTypeMirror(var.asType(), new HashSet<String>()), description); } private void buildUrlParameters(ExecutableElement executableElement, RestDocumentation.RestApi.Resource.Method doc, RestImplementationSupport implementationSupport) { RestDocumentation.RestApi.Resource.UrlFields subs = doc.getUrlParameters(); for (VariableElement var : executableElement.getParameters()) { String reqParam = implementationSupport.getRequestParam(var); if (reqParam != null) { String paramName = var.getSimpleName().toString(); addUrlField(subs, var, reqParam, findParamDescription(paramName, doc.getCommentText())); } } } String findParamDescription(String paramName, String methodJavaDoc) { String desc = null; if (methodJavaDoc != null && StringUtils.isNotEmpty(paramName)) { String token = "@param " + paramName; int startIndex = methodJavaDoc.indexOf(token); if (startIndex != -1) { int endIndex = methodJavaDoc.indexOf("@param", startIndex + 1); if (endIndex == -1) { endIndex = methodJavaDoc.indexOf("@return", startIndex + 1); } if (endIndex != -1) { desc = methodJavaDoc.substring(startIndex + token.length(), endIndex); } else { desc = methodJavaDoc.substring(startIndex + token.length()); } desc = fixCommentWhitespace(desc); } } return desc; } private String fixCommentWhitespace(String desc) { return desc == null ? null : StringUtils.strip(desc.replace("\n", " ").replace("\r", " ").replaceAll(" {2,}", " ")); } /** * Finds any request parameters that can be bound to (which are pojos) and adds each of the POJOs fields to the url parameters */ private void buildPojoQueryParameters(ExecutableElement executableElement, RestDocumentation.RestApi.Resource.Method doc, RestImplementationSupport implementationSupport) { if (doc.getRequestMethod().equals(RequestMethod.GET.name())) { RestDocumentation.RestApi.Resource.UrlFields subs = doc.getUrlParameters(); for (VariableElement var : executableElement.getParameters()) { if (implementationSupport.getPojoRequestParam(var) != null) { Element paramType = _typeUtils.asElement(var.asType()); List<ExecutableElement> methods = ElementFilter.methodsIn(paramType.getEnclosedElements()); for (ExecutableElement method : methods) { if (method.getSimpleName().toString().startsWith("set") && method.getParameters().size() == 1) { String setterComment = processingEnv.getElementUtils().getDocComment(method); TypeMirror setterType = method.getParameters().get(0).asType(); JsonType jsonType = jsonTypeFromTypeMirror(setterType, new HashSet<String>()); String propName = StringUtils .uncapitalize(method.getSimpleName().toString().substring(3)); subs.addField(propName, jsonType, fixCommentWhitespace(setterComment)); } } } } } } private JsonType jsonTypeFromTypeMirror(TypeMirror typeMirror, Collection<String> typeRecursionGuard) { JsonType type; if (_memoizedTypeMirrors.containsKey(typeMirror)) { return _memoizedTypeMirrors.get(typeMirror); } if (isJsonPrimitive(typeMirror)) { type = new JsonPrimitive(typeMirror.toString()); } else if (typeMirror.getKind() == TypeKind.DECLARED) { // some sort of object... walk it DeclaredType declaredType = (DeclaredType) typeMirror; type = jsonTypeForDeclaredType(declaredType, declaredType.getTypeArguments(), typeRecursionGuard); } else if (typeMirror.getKind() == TypeKind.VOID) { type = null; } else if (typeMirror.getKind() == TypeKind.ARRAY) { TypeMirror componentType = ((ArrayType) typeMirror).getComponentType(); type = jsonTypeFromTypeMirror(componentType, typeRecursionGuard); } else if (typeMirror.getKind() == TypeKind.ERROR) { type = new JsonPrimitive("(unresolvable type)"); } else { throw new UnsupportedOperationException(typeMirror.toString()); } _memoizedTypeMirrors.put(typeMirror, type); return type; } /** * Return a JSON type for the given declared type. The caller is responsible for * providing a list of concrete types to use to replace parameterized type placeholders. */ private JsonType jsonTypeForDeclaredType(DeclaredType type, List<? extends TypeMirror> concreteTypes, Collection<String> typeRecursionGuard) { JsonType jt = _memoizedDeclaredTypes.get(type); if (jt == null) { TypeVisitorImpl visitor = new TypeVisitorImpl(type, concreteTypes, typeRecursionGuard); jt = type.accept(visitor, null); _memoizedDeclaredTypes.put(type, jt); } return jt; } private boolean isJsonPrimitive(TypeMirror typeMirror) { return (typeMirror.getKind().isPrimitive() || JsonPrimitive.isPrimitive(typeMirror.toString())); } private void buildResponseFormat(TypeMirror type, RestDocumentation.RestApi.Resource.Method doc) { doc.setResponseBody(jsonTypeFromTypeMirror(type, new HashSet<String>())); doc.setResponseSchema(jsonSchemaFromTypeMirror(type)); doc.setResponseExample(exampleFromJsonType(doc.getResponseBody())); } private String[] getClassLevelUrlPaths(TypeElement cls, RestImplementationSupport implementationSupport) { String basePath = null; DocumentationRestApi api = cls.getAnnotation(DocumentationRestApi.class); RestApiMountPoint mp = cls.getAnnotation(RestApiMountPoint.class); if (null != api) { basePath = api.mount(); } else if (null != mp) { basePath = mp.value(); } else { basePath = "/"; } String[] paths = implementationSupport.getRequestPaths(cls); if (paths.length == 0) { return new String[] { basePath }; } else { for (int i = 0; i < paths.length; i++) { paths[i] = Utils.joinPaths(basePath, paths[i]); } return paths; } } private class TypeVisitorImpl extends AbstractTypeVisitor6<JsonType, Void> { private Map<Name, DeclaredType> _typeArguments = new HashMap<Name, DeclaredType>(); private Collection<String> _typeRecursionDetector; private DeclaredType _type; public TypeVisitorImpl(DeclaredType type, List<? extends TypeMirror> typeArguments, Collection<String> typeRecursionGuard) { _typeRecursionDetector = typeRecursionGuard; _type = type; loadTypeElements(type, typeArguments); } private void loadTypeElements(DeclaredType type, List<? extends TypeMirror> typeArguments) { // TODO test this with generic interfaces, including in superclasses. Issue #10. TypeElement elem = (TypeElement) type.asElement(); if (Object.class.getName().equals(elem.getQualifiedName().toString())) return; if (elem.getSuperclass() instanceof DeclaredType) { DeclaredType sup = (DeclaredType) elem.getSuperclass(); loadTypeElements(sup, sup.getTypeArguments()); } List<? extends TypeParameterElement> generics = elem.getTypeParameters(); for (int i = 0; i < generics.size(); i++) { DeclaredType value = (typeArguments.isEmpty() || !(typeArguments.get(i) instanceof DeclaredType)) ? null : (DeclaredType) typeArguments.get(i); _typeArguments.put(generics.get(i).getSimpleName(), value); } } @Override public JsonType visitPrimitive(PrimitiveType primitiveType, Void o) { return jsonTypeFromTypeMirror(primitiveType, new HashSet<String>(_typeRecursionDetector)); } @Override public JsonType visitNull(NullType nullType, Void o) { throw new UnsupportedOperationException(nullType.toString()); } @Override public JsonType visitArray(ArrayType arrayType, Void o) { throw new UnsupportedOperationException(arrayType.toString()); } @Override public JsonType visitDeclared(DeclaredType declaredType, Void o) { if (_typeRecursionDetector.contains(declaredType.toString())) return new JsonRecursiveObject(declaredType.asElement().getSimpleName().toString()); if (isJsonPrimitive(declaredType)) { // 'primitive'-ish things return new JsonPrimitive(declaredType.toString()); } else if (isInstanceOf(declaredType, Collection.class)) { if (declaredType.getTypeArguments().size() == 0) { return new JsonArray(new JsonPrimitive(Object.class.getName())); } else { TypeParameterElement elem = ((TypeElement) declaredType.asElement()).getTypeParameters().get(0); _typeRecursionDetector.add(declaredType.toString()); return new JsonArray(acceptOrRecurse(o, elem.asType())); } } else if (isInstanceOf(declaredType, Map.class)) { if (declaredType.getTypeArguments().size() == 0) { return new JsonDict(new JsonPrimitive(Object.class.getName()), new JsonPrimitive(Object.class.getName())); } else { TypeMirror key = declaredType.getTypeArguments().get(0); TypeMirror val = declaredType.getTypeArguments().get(1); _typeRecursionDetector.add(declaredType.toString()); JsonType keyJson = acceptOrRecurse(o, key); JsonType valJson = acceptOrRecurse(o, val); return new JsonDict(keyJson, valJson); } } else { TypeElement element = (TypeElement) declaredType.asElement(); if (element.getKind() == ElementKind.ENUM) { List<String> enumConstants = new ArrayList<String>(); for (Element e : element.getEnclosedElements()) { if (e.getKind() == ElementKind.ENUM_CONSTANT) { enumConstants.add(e.toString()); } } JsonPrimitive primitive = new JsonPrimitive(String.class.getName()); // TODO is this always a string? primitive.setRestrictions(enumConstants); return primitive; } else { JsonType mappedType = mapDeclaredType(declaredType, element); if (mappedType != null) { return mappedType; } return buildType(declaredType, element); } } } private JsonType mapDeclaredType(DeclaredType declaredType, TypeElement element) { // built-in non-primitive types are typically serialized to string if (element.getQualifiedName().toString().startsWith("java.")) { return new JsonPrimitive(String.class.getName()); } return null; } private JsonType acceptOrRecurse(Void o, TypeMirror type) { return type instanceof DeclaredType ? recurseForJsonType((DeclaredType) type) : type.accept(this, o); } private JsonType buildType(DeclaredType declaredType, TypeElement element) { if (_typeRecursionDetector.contains(declaredType.toString())) return new JsonRecursiveObject(element.getSimpleName().toString()); JsonObject json = new JsonObject(); buildTypeContents(json, element); return json; // we've already added to the cache; short-circuit to handle recursion } private boolean isInstanceOf(TypeMirror typeMirror, Class type) { if (!(typeMirror instanceof DeclaredType)) return false; if (typeMirror.toString().startsWith(type.getName())) return true; DeclaredType declaredType = (DeclaredType) typeMirror; TypeElement typeElement = (TypeElement) declaredType.asElement(); for (TypeMirror iface : typeElement.getInterfaces()) { if (isInstanceOf(iface, type)) return true; } return isInstanceOf(typeElement.getSuperclass(), type); } private void buildTypeContents(JsonObject o, TypeElement element) { // Spring-MVC and JAX-RS both support methods that return a builder object // that contains the real underlying response payload. These should not be // expressed as response values. if ("org.springframework.web.servlet.ModelAndView".equals(element.getQualifiedName().toString())) { return; } if ("javax.ws.rs.core.Response".equals(element.getQualifiedName().toString())) { return; } if (element.getSuperclass().getKind() != TypeKind.NONE) { // an interface's superclass is TypeKind.NONE DeclaredType sup = (DeclaredType) element.getSuperclass(); if (!isJsonPrimitive(sup)) buildTypeContents(o, (TypeElement) sup.asElement()); } for (Element e : element.getEnclosedElements()) { if (e instanceof ExecutableElement) { addFieldFromBeanMethod(o, (ExecutableElement) e); } } } private void addFieldFromBeanMethod(JsonObject o, ExecutableElement executableElement) { if (!isJsonBeanGetter(executableElement)) return; TypeMirror type = executableElement.getReturnType(); String methodName = executableElement.getSimpleName().toString(); int trimLength = methodName.startsWith("is") ? 2 : 3; // if the name is something trivial like 'get', skip it. See issue #15. if (methodName.length() <= trimLength) { return; } String beanName = methodName.substring(trimLength + 1, methodName.length()); beanName = methodName.substring(trimLength, trimLength + 1).toLowerCase() + beanName; // replace variables with the current concrete manifestation if (type instanceof TypeVariable) { type = getDeclaredTypeForTypeVariable((TypeVariable) type); if (type == null) return; // couldn't find a replacement -- must be a generics-capable type with no generics info } String docComment = processingEnv.getElementUtils().getDocComment(executableElement); if (type instanceof DeclaredType) { JsonType jsonType = recurseForJsonType((DeclaredType) type); o.addField(beanName, jsonType).setCommentText(docComment); } else { o.addField(beanName, jsonTypeFromTypeMirror(type, new HashSet<String>(_typeRecursionDetector))) .setCommentText(docComment); } } private JsonType recurseForJsonType(DeclaredType type) { // loop over the element's generic types, and build a concrete list from the owning context List<DeclaredType> concreteTypes = new ArrayList<DeclaredType>(); for (TypeMirror generic : type.getTypeArguments()) { if (generic instanceof DeclaredType) concreteTypes.add((DeclaredType) generic); else if (generic.getKind() == TypeKind.ARRAY) { ArrayType arrayType = (ArrayType) generic; return new JsonArray(acceptOrRecurse(null, arrayType.getComponentType())); } else { concreteTypes.add(_typeArguments.get(((TypeVariable) generic).asElement().getSimpleName())); } } _typeRecursionDetector.add(_type.toString()); Collection<String> types = new HashSet<String>(_typeRecursionDetector); return jsonTypeForDeclaredType(type, concreteTypes, types); } private boolean isJsonBeanGetter(ExecutableElement executableElement) { if (executableElement.getKind() != ElementKind.METHOD) return false; if (executableElement.getReturnType().getKind() == TypeKind.NULL) return false; if (!(executableElement.getSimpleName().toString().startsWith("get") || executableElement.getSimpleName().toString().startsWith("is"))) return false; if (executableElement.getParameters().size() > 0) return false; return executableElement.getAnnotation(JsonIgnore.class) == null; } @Override public JsonType visitError(ErrorType errorType, Void o) { throw new UnsupportedOperationException(errorType.toString()); } @Override public JsonType visitTypeVariable(TypeVariable typeVariable, Void o) { DeclaredType type = getDeclaredTypeForTypeVariable(typeVariable); if (type != null) { // null: un-parameterized usage of a generics-having type try { return type.accept(this, o); } catch (UnsupportedOperationException e) { // likely we ran into a type we can't work with (e.g. ErrorType), continue with best effort return null; } } else { return null; } } private DeclaredType getDeclaredTypeForTypeVariable(TypeVariable typeVariable) { Name name = typeVariable.asElement().getSimpleName(); if (!_typeArguments.containsKey(name)) { throw new UnsupportedOperationException( String.format("Unknown parameterized type: %s. Available types in this context: %s.", typeVariable.toString(), _typeArguments)); } else { return _typeArguments.get(name); } } @Override public JsonType visitWildcard(WildcardType wildcardType, Void o) { throw new UnsupportedOperationException(wildcardType.toString()); } @Override public JsonType visitExecutable(ExecutableType executableType, Void o) { throw new UnsupportedOperationException(executableType.toString()); } @Override public JsonType visitNoType(NoType noType, Void o) { throw new UnsupportedOperationException(noType.toString()); } @Override public JsonType visitUnknown(TypeMirror typeMirror, Void o) { throw new UnsupportedOperationException(typeMirror.toString()); } } public interface RestImplementationSupport { Class<? extends Annotation> getMappingAnnotationType(); String[] getRequestPaths(ExecutableElement executableElement, TypeElement contextClass); String[] getRequestPaths(TypeElement cls); String getRequestMethod(ExecutableElement executableElement, TypeElement contextClass); String getPathVariable(VariableElement var); String getRequestParam(VariableElement var); String getPojoRequestParam(VariableElement var); boolean isRequestBody(VariableElement var); } String exampleFromJsonType(JsonType type) { return renderJson(type); } String renderJson(JsonType type) { String schema = ""; if (type instanceof org.versly.rest.wsdoc.impl.JsonPrimitive) { schema = schema + renderJsonPrimitive((JsonPrimitive) type, null); } else if (type instanceof org.versly.rest.wsdoc.impl.JsonObject) { schema = schema + renderJsonObject((JsonObject) type); } else if (type instanceof org.versly.rest.wsdoc.impl.JsonRecursiveObject) { schema = schema + renderJsonRecursiveObject((JsonRecursiveObject) type); } else if (type instanceof org.versly.rest.wsdoc.impl.JsonArray) { schema = schema + renderJsonArray((JsonArray) type); } else if (type instanceof org.versly.rest.wsdoc.impl.JsonDict) { schema = schema + renderJsonDict((JsonDict) type); } return schema; } private String renderJsonArray(JsonArray type) { return "[" + renderJson(type.getElementType()) + "]"; } private String renderJsonDict(JsonDict type) { return "{" + renderJson(type.getKeyType()) + ": " + renderJson(type.getValueType()) + " }"; } private String renderJsonPrimitive(JsonPrimitive type, String comment) { String primStr = "\"" + type.getTypeName(); if (!CollectionUtils.isEmpty(type.getRestrictions())) { primStr = primStr + " one of [" + join(type.getRestrictions().toArray(), ",") + "]"; } if (comment != null) { primStr = primStr + " /* " + comment + " */"; } return primStr + "\""; } private String renderJsonObject(JsonObject type) { String objStr = "{"; Iterator<JsonObject.JsonField> fieldIt = type.getFields().iterator(); while (fieldIt.hasNext()) { JsonObject.JsonField jsonField = fieldIt.next(); objStr = objStr + "\"" + jsonField.getFieldName() + "\": "; if (jsonField.getFieldType() instanceof JsonPrimitive) { objStr = objStr + renderJsonPrimitive((JsonPrimitive) jsonField.getFieldType(), getComment(jsonField)); } else { objStr = objStr + renderJson(jsonField.getFieldType()); } if (fieldIt.hasNext()) { objStr = objStr + ","; } } return objStr + "}"; } private String getComment(JsonObject.JsonField jsonField) { String comment = null; if (isNotEmpty(jsonField.getCommentText())) { comment = jsonField.getCommentText().replaceAll("(\r|\n)+| {2,}", " "); } return comment; } private String renderJsonRecursiveObject(JsonRecursiveObject type) { return "\"" + type.getRecursedObjectTypeName() + " recursive\""; } String jsonSchemaFromTypeMirror(TypeMirror type) { String serializedSchema = null; if (type.getKind().isPrimitive() || type.getKind() == TypeKind.VOID) { return null; } // we need the dto class to generate schema using jackson json-schema module // note: Types.erasure() provides canonical names whereas Class.forName() wants a "regular" name, // so forName will fail for nested and inner classes as "regular" names use $ between parent and child. Class dtoClass = null; StringBuffer erasure = new StringBuffer(_typeUtils.erasure(type).toString()); for (boolean done = false; !done;) { try { dtoClass = Class.forName(erasure.toString()); done = true; } catch (ClassNotFoundException e) { if (erasure.lastIndexOf(".") != -1) { erasure.setCharAt(erasure.lastIndexOf("."), '$'); } else { done = true; } } } // if we were able to figure out the dto class, use jackson json-schema module to serialize it Exception e = null; if (dtoClass != null) { try { ObjectMapper m = new ObjectMapper(); m.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING); m.registerModule(new JodaModule()); SchemaFactoryWrapper visitor = new SchemaFactoryWrapper(); m.acceptJsonFormatVisitor(m.constructType(dtoClass), visitor); serializedSchema = m.writeValueAsString(visitor.finalSchema()); } catch (Exception ex) { e = ex; } } // report warning if we were not able to generate schema for non-primitive type if (serializedSchema == null) { this.processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "cannot generate json-schema for class " + type.toString() + " (erasure " + erasure + "), " + ((e != null) ? ("exception: " + e.getMessage()) : "class not found")); } return serializedSchema; } }