/** * Copyright © 2006-2016 Web Cohesion (info@webcohesion.com) * * 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 com.webcohesion.enunciate.modules.spring_web.model; import com.webcohesion.enunciate.facets.Facet; import com.webcohesion.enunciate.facets.HasFacets; import com.webcohesion.enunciate.javac.decorations.TypeMirrorDecorator; import com.webcohesion.enunciate.javac.decorations.element.DecoratedExecutableElement; import com.webcohesion.enunciate.javac.decorations.type.DecoratedDeclaredType; import com.webcohesion.enunciate.javac.decorations.type.DecoratedTypeMirror; import com.webcohesion.enunciate.javac.decorations.type.TypeMirrorUtils; import com.webcohesion.enunciate.javac.decorations.type.TypeVariableContext; import com.webcohesion.enunciate.javac.javadoc.JavaDoc; import com.webcohesion.enunciate.javac.javadoc.ParamDocComment; import com.webcohesion.enunciate.javac.javadoc.ReturnDocComment; import com.webcohesion.enunciate.metadata.rs.RequestHeader; import com.webcohesion.enunciate.metadata.rs.*; import com.webcohesion.enunciate.modules.spring_web.EnunciateSpringWebContext; import com.webcohesion.enunciate.modules.spring_web.model.util.RSParamDocComment; import com.webcohesion.enunciate.modules.spring_web.model.util.ReturnWrappedDocComment; import com.webcohesion.enunciate.util.AnnotationUtils; import com.webcohesion.enunciate.util.IgnoreUtils; import com.webcohesion.enunciate.util.TypeHintUtils; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import javax.annotation.security.RolesAllowed; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeMirror; import java.lang.annotation.IncompleteAnnotationException; import java.util.*; import java.util.concurrent.Callable; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * A JAX-RS resource method. * * @author Ryan Heaton */ public class RequestMapping extends DecoratedExecutableElement implements HasFacets, PathContext { private static final Pattern CONTEXT_PARAM_PATTERN = Pattern.compile("\\{([^\\}]+)\\}"); private final EnunciateSpringWebContext context; private final List<PathSegment> pathSegments; private final String label; private final Set<String> httpMethods; private final Set<String> consumesMediaTypes; private final Set<String> producesMediaTypes; private final SpringController parent; private final Set<RequestParameter> requestParameters; private final ResourceEntityParameter entityParameter; private final Map<String, Object> metaData = new HashMap<String, Object>(); private final List<? extends ResponseCode> statusCodes; private final List<? extends ResponseCode> warnings; private final Map<String, String> responseHeaders = new HashMap<String, String>(); private final ResourceRepresentationMetadata representationMetadata; private final Set<Facet> facets = new TreeSet<Facet>(); public RequestMapping(List<PathSegment> pathSegments, RequestMethod[] methods, String[] consumesInfo, String[] producesInfo, ExecutableElement delegate, SpringController parent, TypeVariableContext variableContext, EnunciateSpringWebContext context) { super(delegate, context.getContext().getProcessingEnvironment()); this.context = context; this.pathSegments = pathSegments; //initialize first with all methods. EnumSet<RequestMethod> httpMethods = EnumSet.allOf(RequestMethod.class); if (methods.length > 0) { httpMethods.retainAll(Arrays.asList(methods)); } httpMethods.retainAll(parent.getApplicableMethods()); if (httpMethods.isEmpty()) { throw new IllegalStateException(parent.getQualifiedName() + "." + getSimpleName() + ": no applicable request methods."); } this.httpMethods = new TreeSet<String>(); for (RequestMethod httpMethod : httpMethods) { this.httpMethods.add(httpMethod.name()); } Set<String> consumes = new TreeSet<String>(); if (consumesInfo != null && consumesInfo.length > 0) { for (String mediaType : consumesInfo) { if (mediaType.startsWith("!")) { continue; } int colonIndex = mediaType.indexOf(';'); if (colonIndex > 0) { mediaType = mediaType.substring(0, colonIndex); } consumes.add(mediaType); } if (consumes.isEmpty()) { consumes.add("*/*"); } } else { consumes = parent.getConsumesMime(); } this.consumesMediaTypes = consumes; Set<String> produces = new TreeSet<String>(); if (producesInfo != null && producesInfo.length > 0) { for (String mediaType : producesInfo) { if (mediaType.startsWith("!")) { continue; } int colonIndex = mediaType.indexOf(';'); if (colonIndex > 0) { mediaType = mediaType.substring(0, colonIndex); } produces.add(mediaType); } if (produces.isEmpty()) { produces.add("*/*"); } } else { produces = parent.getProducesMime(); } this.producesMediaTypes = produces; String label = null; ResourceLabel resourceLabel = delegate.getAnnotation(ResourceLabel.class); if (resourceLabel != null) { label = resourceLabel.value(); if ("##default".equals(label)) { label = null; } } ResourceEntityParameter entityParameter = null; ResourceRepresentationMetadata outputPayload = null; Set<RequestParameter> requestParameters = new TreeSet<RequestParameter>(); ArrayList<ResponseCode> statusCodes = new ArrayList<ResponseCode>(); ArrayList<ResponseCode> warnings = new ArrayList<ResponseCode>(); Set<SpringControllerAdvice> advice = this.context.getAdvice(); for (SpringControllerAdvice controllerAdvice : advice) { List<RequestMappingAdvice> requestAdvice = controllerAdvice.findRequestMappingAdvice(this); for (RequestMappingAdvice mappingAdvice : requestAdvice) { entityParameter = mappingAdvice.getEntityParameter(); outputPayload = mappingAdvice.getRepresentationMetadata(); requestParameters.addAll(mappingAdvice.getRequestParameters()); statusCodes.addAll(mappingAdvice.getStatusCodes()); warnings.addAll(mappingAdvice.getWarnings()); this.responseHeaders.putAll(mappingAdvice.getResponseHeaders()); } } for (VariableElement parameterDeclaration : getParameters()) { if (IgnoreUtils.isIgnored(parameterDeclaration)) { continue; } if (parameterDeclaration.getAnnotation(RequestBody.class) != null) { entityParameter = new ResourceEntityParameter(parameterDeclaration, variableContext, context); } else { requestParameters.addAll(RequestParameterFactory.getRequestParameters(this, parameterDeclaration, this)); } } DecoratedTypeMirror<?> returnType; TypeHint hintInfo = getAnnotation(TypeHint.class); JavaDoc localDoc = new JavaDoc(getDocComment(), null, null, this.env); if (hintInfo != null) { returnType = (DecoratedTypeMirror) TypeHintUtils.getTypeHint(hintInfo, this.env, null); if (returnType != null) { returnType.setDocComment(new ReturnDocComment(this)); } } else { returnType = (DecoratedTypeMirror) getReturnType(); String docComment = returnType.getDocComment(); if (returnType instanceof DecoratedDeclaredType && (returnType.isInstanceOf(Callable.class) || returnType.isInstanceOf("org.springframework.web.context.request.async.DeferredResult") || returnType.isInstanceOf("org.springframework.util.concurrent.ListenableFuture"))) { //attempt unwrap callable and deferred results. List<? extends TypeMirror> typeArgs = ((DecoratedDeclaredType) returnType).getTypeArguments(); returnType = (typeArgs != null && typeArgs.size() == 1) ? (DecoratedTypeMirror<?>) TypeMirrorDecorator.decorate(typeArgs.get(0), this.env) : TypeMirrorUtils.objectType(this.env); } boolean returnsResponseBody = getAnnotation(ResponseBody.class) != null || parent.getAnnotation(ResponseBody.class) != null || parent.getAnnotation(RestController.class) != null; if (returnType instanceof DecoratedDeclaredType && returnType.isInstanceOf("org.springframework.http.HttpEntity")) { DecoratedDeclaredType entity = (DecoratedDeclaredType) returnType; List<? extends TypeMirror> typeArgs = ((DecoratedDeclaredType) entity).getTypeArguments(); returnType = (typeArgs != null && typeArgs.size() == 1) ? (DecoratedTypeMirror<?>) TypeMirrorDecorator.decorate(typeArgs.get(0), this.env) : TypeMirrorUtils.objectType(this.env); } else if (!returnsResponseBody) { //doesn't return response body; no way to tell what's being returned. returnType = TypeMirrorUtils.objectType(this.env); } if (localDoc.get("returnWrapped") != null) { //support jax-doclets. see http://jira.codehaus.org/browse/ENUNCIATE-690 String returnWrapped = localDoc.get("returnWrapped").get(0); String fqn = returnWrapped.substring(0, JavaDoc.indexOfFirstWhitespace(returnWrapped)).trim(); boolean array = false; if (fqn.endsWith("[]")) { array = true; fqn = fqn.substring(0, fqn.length() - 2); } TypeElement type = env.getElementUtils().getTypeElement(fqn); if (type != null) { returnType = (DecoratedTypeMirror) TypeMirrorDecorator.decorate(env.getTypeUtils().getDeclaredType(type), this.env); if (array) { returnType = (DecoratedTypeMirror) TypeMirrorDecorator.decorate(env.getTypeUtils().getArrayType(returnType), this.env); } returnType.setDocComment(new ReturnWrappedDocComment(this, returnWrapped)); } else { getContext().getContext().getLogger().info("Invalid @returnWrapped type: \"%s\" (doesn't resolve to a type).", fqn); } } //now resolve any type variables. returnType = (DecoratedTypeMirror) TypeMirrorDecorator.decorate(variableContext.resolveTypeVariables(returnType, this.env), this.env); returnType.setDocComment(new ReturnDocComment(this)); } outputPayload = returnType == null || returnType.isVoid() ? outputPayload : new ResourceRepresentationMetadata(returnType); JavaDoc.JavaDocTagList doclets = localDoc.get("RequestHeader"); //support jax-doclets. see http://jira.codehaus.org/browse/ENUNCIATE-690 if (doclets != null) { for (String doclet : doclets) { int firstspace = JavaDoc.indexOfFirstWhitespace(doclet); String header = firstspace > 0 ? doclet.substring(0, firstspace) : doclet; String doc = ((firstspace > 0) && (firstspace + 1 < doclet.length())) ? doclet.substring(firstspace + 1) : ""; requestParameters.add(new ExplicitRequestParameter(this, doc, header, ResourceParameterType.HEADER, context)); } } List<JavaDoc.JavaDocTagList> inheritedDoclets = AnnotationUtils.getJavaDocTags("RequestHeader", parent); for (JavaDoc.JavaDocTagList inheritedDoclet : inheritedDoclets) { for (String doclet : inheritedDoclet) { int firstspace = JavaDoc.indexOfFirstWhitespace(doclet); String header = firstspace > 0 ? doclet.substring(0, firstspace) : doclet; String doc = ((firstspace > 0) && (firstspace + 1 < doclet.length())) ? doclet.substring(firstspace + 1) : ""; requestParameters.add(new ExplicitRequestParameter(this, doc, header, ResourceParameterType.HEADER, context)); } } RequestHeaders requestHeaders = getAnnotation(RequestHeaders.class); if (requestHeaders != null) { for (RequestHeader header : requestHeaders.value()) { requestParameters.add(new ExplicitRequestParameter(this, header.description(), header.name(), ResourceParameterType.HEADER, context)); } } List<RequestHeaders> inheritedRequestHeaders = AnnotationUtils.getAnnotations(RequestHeaders.class, parent); for (RequestHeaders inheritedRequestHeader : inheritedRequestHeaders) { for (RequestHeader header : inheritedRequestHeader.value()) { requestParameters.add(new ExplicitRequestParameter(this, header.description(), header.name(), ResourceParameterType.HEADER, context)); } } StatusCodes codes = getAnnotation(StatusCodes.class); if (codes != null) { for (com.webcohesion.enunciate.metadata.rs.ResponseCode code : codes.value()) { ResponseCode rc = new ResponseCode(this); rc.setCode(code.code()); rc.setCondition(code.condition()); for (ResponseHeader header : code.additionalHeaders()) { rc.setAdditionalHeader(header.name(), header.description()); } rc.setType((DecoratedTypeMirror) TypeHintUtils.getTypeHint(code.type(), this.env, null)); statusCodes.add(rc); } } ResponseStatus responseStatus = getAnnotation(ResponseStatus.class); if (responseStatus != null) { HttpStatus code = responseStatus.value(); if (code == HttpStatus.INTERNAL_SERVER_ERROR) { try { code = responseStatus.code(); } catch (IncompleteAnnotationException e) { //fall through; 'responseStatus.code' was added in 4.2. } } ResponseCode rc = new ResponseCode(this); rc.setCode(code.value()); String reason = responseStatus.reason(); if (!reason.isEmpty()) { rc.setCondition(reason); } statusCodes.add(rc); } List<StatusCodes> inheritedStatusCodes = AnnotationUtils.getAnnotations(StatusCodes.class, parent); for (StatusCodes inheritedStatusCode : inheritedStatusCodes) { for (com.webcohesion.enunciate.metadata.rs.ResponseCode code : inheritedStatusCode.value()) { ResponseCode rc = new ResponseCode(this); rc.setCode(code.code()); rc.setCondition(code.condition()); for (ResponseHeader header : code.additionalHeaders()) { rc.setAdditionalHeader(header.name(), header.description()); } rc.setType((DecoratedTypeMirror) TypeHintUtils.getTypeHint(code.type(), this.env, null)); statusCodes.add(rc); } } doclets = localDoc.get("HTTP"); if (doclets != null) { for (String doclet : doclets) { int firstspace = JavaDoc.indexOfFirstWhitespace(doclet); String code = firstspace > 0 ? doclet.substring(0, firstspace) : doclet; String doc = ((firstspace > 0) && (firstspace + 1 < doclet.length())) ? doclet.substring(firstspace + 1) : ""; try { ResponseCode rc = new ResponseCode(this); rc.setCode(Integer.parseInt(code)); rc.setCondition(doc); statusCodes.add(rc); } catch (NumberFormatException e) { //fall through... } } } inheritedDoclets = AnnotationUtils.getJavaDocTags("HTTP", parent); for (JavaDoc.JavaDocTagList inheritedDoclet : inheritedDoclets) { for (String doclet : inheritedDoclet) { int firstspace = JavaDoc.indexOfFirstWhitespace(doclet); String code = firstspace > 0 ? doclet.substring(0, firstspace) : doclet; String doc = ((firstspace > 0) && (firstspace + 1 < doclet.length())) ? doclet.substring(firstspace + 1) : ""; try { ResponseCode rc = new ResponseCode(this); rc.setCode(Integer.parseInt(code)); rc.setCondition(doc); statusCodes.add(rc); } catch (NumberFormatException e) { //fall through... } } } Warnings warningInfo = getAnnotation(Warnings.class); if (warningInfo != null) { for (com.webcohesion.enunciate.metadata.rs.ResponseCode code : warningInfo.value()) { ResponseCode rc = new ResponseCode(this); rc.setCode(code.code()); rc.setCondition(code.condition()); warnings.add(rc); } } List<Warnings> inheritedWarnings = AnnotationUtils.getAnnotations(Warnings.class, parent); for (Warnings inheritedWarning : inheritedWarnings) { for (com.webcohesion.enunciate.metadata.rs.ResponseCode code : inheritedWarning.value()) { ResponseCode rc = new ResponseCode(this); rc.setCode(code.code()); rc.setCondition(code.condition()); warnings.add(rc); } } doclets = localDoc.get("HTTPWarning"); if (doclets != null) { for (String doclet : doclets) { int firstspace = JavaDoc.indexOfFirstWhitespace(doclet); String code = firstspace > 0 ? doclet.substring(0, firstspace) : doclet; String doc = ((firstspace > 0) && (firstspace + 1 < doclet.length())) ? doclet.substring(firstspace + 1) : ""; try { ResponseCode rc = new ResponseCode(this); rc.setCode(Integer.parseInt(code)); rc.setCondition(doc); warnings.add(rc); } catch (NumberFormatException e) { //fall through... } } } inheritedDoclets = AnnotationUtils.getJavaDocTags("HTTPWarning", parent); for (JavaDoc.JavaDocTagList inheritedDoclet : inheritedDoclets) { for (String doclet : inheritedDoclet) { int firstspace = JavaDoc.indexOfFirstWhitespace(doclet); String code = firstspace > 0 ? doclet.substring(0, firstspace) : doclet; String doc = ((firstspace > 0) && (firstspace + 1 < doclet.length())) ? doclet.substring(firstspace + 1) : ""; try { ResponseCode rc = new ResponseCode(this); rc.setCode(Integer.parseInt(code)); rc.setCondition(doc); warnings.add(rc); } catch (NumberFormatException e) { //fall through... } } } ResponseHeaders responseHeaders = getAnnotation(ResponseHeaders.class); if (responseHeaders != null) { for (ResponseHeader header : responseHeaders.value()) { this.responseHeaders.put(header.name(), header.description()); } } List<ResponseHeaders> inheritedResponseHeaders = AnnotationUtils.getAnnotations(ResponseHeaders.class, parent); for (ResponseHeaders inheritedResponseHeader : inheritedResponseHeaders) { for (ResponseHeader header : inheritedResponseHeader.value()) { this.responseHeaders.put(header.name(), header.description()); } } doclets = localDoc.get("ResponseHeader"); if (doclets != null) { for (String doclet : doclets) { int firstspace = JavaDoc.indexOfFirstWhitespace(doclet); String header = firstspace > 0 ? doclet.substring(0, firstspace) : doclet; String doc = ((firstspace > 0) && (firstspace + 1 < doclet.length())) ? doclet.substring(firstspace + 1) : ""; this.responseHeaders.put(header, doc); } } inheritedDoclets = AnnotationUtils.getJavaDocTags("ResponseHeader", parent); for (JavaDoc.JavaDocTagList inheritedDoclet : inheritedDoclets) { for (String doclet : inheritedDoclet) { int firstspace = JavaDoc.indexOfFirstWhitespace(doclet); String header = firstspace > 0 ? doclet.substring(0, firstspace) : doclet; String doc = ((firstspace > 0) && (firstspace + 1 < doclet.length())) ? doclet.substring(firstspace + 1) : ""; this.responseHeaders.put(header, doc); } } this.entityParameter = entityParameter; this.requestParameters = requestParameters; this.label = label; this.parent = parent; this.statusCodes = statusCodes; this.warnings = warnings; this.representationMetadata = outputPayload; this.facets.addAll(Facet.gatherFacets(delegate, context.getContext())); this.facets.addAll(parent.getFacets()); } @Override protected ParamDocComment createParamDocComment(VariableElement param) { return new RSParamDocComment(this, param.getSimpleName().toString()); } public EnunciateSpringWebContext getContext() { return context; } /** * The HTTP methods for invoking the method. * * @return The HTTP methods for invoking the method. */ public Set<String> getHttpMethods() { return httpMethods; } /** * Get the path components for this resource method. * * @return The path components. */ public List<PathSegment> getPathSegments() { return this.pathSegments; } /** * Builds the full URI path to this resource method. * * @return the full URI path to this resource method. */ public String getFullpath() { StringBuilder builder = new StringBuilder(); for (PathSegment pathSegment : getPathSegments()) { builder.append(pathSegment.getValue()); } return builder.toString(); } /** * The servlet pattern that can be applied to access this resource method. * * @return The servlet pattern that can be applied to access this resource method. */ public String getServletPattern() { StringBuilder builder = new StringBuilder(); String fullPath = getFullpath(); Matcher pathParamMatcher = CONTEXT_PARAM_PATTERN.matcher(fullPath); if (pathParamMatcher.find()) { builder.append(fullPath, 0, pathParamMatcher.start()).append("*"); } else { builder.append(fullPath); } return builder.toString(); } /** * The label for this resource method, if it exists. * * @return The subpath for this resource method, if it exists. */ public String getLabel() { return label; } /** * The resource that holds this resource method. * * @return The resource that holds this resource method. */ public SpringController getParent() { return parent; } /** * The MIME types that are consumed by this method. * * @return The MIME types that are consumed by this method. */ public Set<String> getConsumesMediaTypes() { return consumesMediaTypes; } /** * The MIME types that are produced by this method. * * @return The MIME types that are produced by this method. */ public Set<String> getProducesMediaTypes() { return producesMediaTypes; } /** * The list of resource parameters that this method requires to be invoked. * * @return The list of resource parameters that this method requires to be invoked. */ public Set<RequestParameter> getRequestParameters() { return this.requestParameters; } /** * The entity parameter. * * @return The entity parameter, or null if none. */ public ResourceEntityParameter getEntityParameter() { return entityParameter; } /** * The output payload for this resource. * * @return The output payload for this resource. */ public ResourceRepresentationMetadata getRepresentationMetadata() { return this.representationMetadata; } /** * The potential status codes. * * @return The potential status codes. */ public List<? extends ResponseCode> getStatusCodes() { return this.statusCodes; } /** * The potential warnings. * * @return The potential warnings. */ public List<? extends ResponseCode> getWarnings() { return this.warnings; } /** * The metadata associated with this resource. * * @return The metadata associated with this resource. */ public Map<String, Object> getMetaData() { return Collections.unmodifiableMap(this.metaData); } /** * Put metadata associated with this resource. * * @param name The name of the metadata. * @param data The data. */ public void putMetaData(String name, Object data) { this.metaData.put(name, data); } /** * The response headers that are expected on this resource method. * * @return The response headers that are expected on this resource method. */ public Map<String, String> getResponseHeaders() { return responseHeaders; } /** * The facets here applicable. * * @return The facets here applicable. */ public Set<Facet> getFacets() { return facets; } /** * The security roles for this method. * * @return The security roles for this method. */ public Set<String> getSecurityRoles() { TreeSet<String> roles = new TreeSet<String>(); RolesAllowed rolesAllowed = getAnnotation(RolesAllowed.class); if (rolesAllowed != null) { Collections.addAll(roles, rolesAllowed.value()); } SpringController parent = getParent(); if (parent != null) { roles.addAll(parent.getSecurityRoles()); } return roles; } }