/**
* 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.element.DecoratedTypeElement;
import com.webcohesion.enunciate.javac.decorations.type.TypeVariableContext;
import com.webcohesion.enunciate.modules.spring_web.EnunciateSpringWebContext;
import com.webcohesion.enunciate.util.IgnoreUtils;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.annotation.security.RolesAllowed;
import javax.lang.model.element.*;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import java.lang.annotation.IncompleteAnnotationException;
import java.util.*;
/**
* A Spring web controller.
*
* @author Ryan Heaton
*/
public class SpringController extends DecoratedTypeElement implements HasFacets {
private final EnunciateSpringWebContext context;
private final Set<String> paths;
private final Set<String> consumesMime;
private final Set<String> producesMime;
private final org.springframework.web.bind.annotation.RequestMapping mappingInfo;
private final List<RequestMapping> requestMappings;
private final Set<Facet> facets = new TreeSet<Facet>();
public SpringController(TypeElement delegate, EnunciateSpringWebContext context) {
this(delegate, delegate.getAnnotation(org.springframework.web.bind.annotation.RequestMapping.class), context);
}
private SpringController(TypeElement delegate, org.springframework.web.bind.annotation.RequestMapping mappingInfo, EnunciateSpringWebContext context) {
this(delegate, loadPaths(mappingInfo), mappingInfo, context);
}
private static Set<String> loadPaths(org.springframework.web.bind.annotation.RequestMapping mappingInfo) {
TreeSet<String> paths = new TreeSet<String>();
if (mappingInfo != null) {
try {
paths.addAll(Arrays.asList(mappingInfo.path()));
}
catch (IncompleteAnnotationException e) {
//fall through; 'mappingInfo.path' was added in 4.2.
}
paths.addAll(Arrays.asList(mappingInfo.value()));
}
if (paths.isEmpty()) {
paths.add("");
}
return paths;
}
private SpringController(TypeElement delegate, Set<String> paths, org.springframework.web.bind.annotation.RequestMapping mappingInfo, EnunciateSpringWebContext context) {
super(delegate, context.getContext().getProcessingEnvironment());
this.context = context;
this.paths = paths;
this.mappingInfo = mappingInfo;
Set<String> consumes = new TreeSet<String>();
if (mappingInfo != null && mappingInfo.consumes().length > 0) {
for (String mt : mappingInfo.consumes()) {
if (mt.startsWith("!")) {
continue;
}
int colonIndex = mt.indexOf(';');
if (colonIndex > 0) {
mt = mt.substring(0, colonIndex);
}
consumes.add(mt);
}
}
else {
consumes.add("*/*");
}
this.consumesMime = Collections.unmodifiableSet(consumes);
Set<String> produces = new TreeSet<String>();
if (mappingInfo != null && mappingInfo.produces().length > 0) {
for (String mt : mappingInfo.produces()) {
if (mt.startsWith("!")) {
continue;
}
int colonIndex = mt.indexOf(';');
if (colonIndex > 0) {
mt = mt.substring(0, colonIndex);
}
produces.add(mt);
}
}
else {
produces.add("*/*");
}
this.producesMime = Collections.unmodifiableSet(produces);
this.facets.addAll(Facet.gatherFacets(delegate, context.getContext()));
this.requestMappings = Collections.unmodifiableList(getRequestMappings(delegate, new TypeVariableContext(), context));
}
/**
* Get all the resource methods for the specified type.
*
* @param delegate The type.
* @param context The context
* @return The resource methods.
*/
protected List<RequestMapping> getRequestMappings(final TypeElement delegate, TypeVariableContext variableContext, EnunciateSpringWebContext context) {
if (delegate == null || delegate.getQualifiedName().toString().equals(Object.class.getName())) {
return Collections.emptyList();
}
ArrayList<RequestMapping> requestMappings = new ArrayList<RequestMapping>();
for (ExecutableElement method : ElementFilter.methodsIn(delegate.getEnclosedElements())) {
if (IgnoreUtils.isIgnored(method)) {
continue;
}
RequestMethod[] requestMethods = findRequestMethods(method);
if (requestMethods != null) {
String[] consumes = findConsumes(method);
String[] produces = findProduces(method);
Set<String> subpaths = findSubpaths(method);
if (subpaths.isEmpty()) {
subpaths.add("");
}
for (String path : getPaths()) {
for (String subpath : subpaths) {
if (!path.endsWith("/") && !subpath.startsWith("/")) {
path = path + "/";
}
requestMappings.add(new RequestMapping(extractPathComponents(path + subpath), requestMethods, consumes, produces, method, this, variableContext, context));
}
}
if (requestMappings.isEmpty()) {
requestMappings.add(new RequestMapping(new ArrayList<PathSegment>(), requestMethods, consumes, produces, method, this, variableContext, context));
}
}
}
//some methods may be specified by a superclass and/or implemented interface. But the annotations on the current class take precedence.
for (TypeMirror interfaceType : delegate.getInterfaces()) {
if (interfaceType instanceof DeclaredType) {
DeclaredType declared = (DeclaredType) interfaceType;
TypeElement element = (TypeElement) declared.asElement();
List<RequestMapping> interfaceMethods = getRequestMappings(element, variableContext.push(element.getTypeParameters(), declared.getTypeArguments()), context);
for (RequestMapping interfaceMethod : interfaceMethods) {
if (!isOverridden(interfaceMethod, requestMappings)) {
requestMappings.add(interfaceMethod);
}
}
}
}
if (delegate.getKind() == ElementKind.CLASS) {
TypeMirror superclass = delegate.getSuperclass();
if (superclass instanceof DeclaredType && ((DeclaredType)superclass).asElement() != null) {
DeclaredType declared = (DeclaredType) superclass;
TypeElement element = (TypeElement) declared.asElement();
List<RequestMapping> superMethods = getRequestMappings(element, variableContext.push(element.getTypeParameters(), declared.getTypeArguments()), context);
for (RequestMapping superMethod : superMethods) {
if (!isOverridden(superMethod, requestMappings)) {
requestMappings.add(superMethod);
}
}
}
}
return requestMappings;
}
private RequestMethod[] findRequestMethods(ExecutableElement method) {
org.springframework.web.bind.annotation.RequestMapping requestMapping = method.getAnnotation(org.springframework.web.bind.annotation.RequestMapping.class);
if (requestMapping != null) {
return requestMapping.method();
}
else {
List<? extends AnnotationMirror> annotations = method.getAnnotationMirrors();
if (annotations != null) {
for (AnnotationMirror annotation : annotations) {
DeclaredType annotationType = annotation.getAnnotationType();
if (annotationType != null) {
Element annotationElement = annotationType.asElement();
if (annotationElement != null) {
requestMapping = annotationElement.getAnnotation(org.springframework.web.bind.annotation.RequestMapping.class);
if (requestMapping != null) {
return requestMapping.method();
}
}
}
}
}
}
return null;
}
private String[] findConsumes(ExecutableElement method) {
org.springframework.web.bind.annotation.RequestMapping requestMapping = method.getAnnotation(org.springframework.web.bind.annotation.RequestMapping.class);
if (requestMapping != null) {
return requestMapping.consumes();
}
else {
List<? extends AnnotationMirror> annotations = method.getAnnotationMirrors();
if (annotations != null) {
for (AnnotationMirror annotation : annotations) {
DeclaredType annotationType = annotation.getAnnotationType();
if (annotationType != null) {
Element annotationElement = annotationType.asElement();
if (annotationElement != null) {
requestMapping = annotationElement.getAnnotation(org.springframework.web.bind.annotation.RequestMapping.class);
if (requestMapping != null) {
Map<? extends ExecutableElement, ? extends AnnotationValue> elementValues = annotation.getElementValues();
for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : elementValues.entrySet()) {
if (entry.getKey().getSimpleName().contentEquals("consumes")) {
Object value = entry.getValue().getValue();
if (value instanceof List) {
String[] consumes = new String[((List)value).size()];
for (int i = 0; i < ((List) value).size(); i++) {
AnnotationValue valueItem = (AnnotationValue) ((List) value).get(i);
consumes[i] = String.valueOf(valueItem.getValue());
}
return consumes;
}
}
}
}
}
}
}
}
}
return null;
}
private Set<String> findSubpaths(ExecutableElement method) {
org.springframework.web.bind.annotation.RequestMapping requestMapping = method.getAnnotation(org.springframework.web.bind.annotation.RequestMapping.class);
if (requestMapping != null) {
Set<String> subpaths = new TreeSet<String>();
try {
subpaths.addAll(Arrays.asList(requestMapping.path()));
}
catch (IncompleteAnnotationException e) {
//fall through; 'mappingInfo.path' was added in 4.2.
}
subpaths.addAll(Arrays.asList(requestMapping.value()));
return subpaths;
}
else {
List<? extends AnnotationMirror> annotations = method.getAnnotationMirrors();
if (annotations != null) {
for (AnnotationMirror annotation : annotations) {
DeclaredType annotationType = annotation.getAnnotationType();
if (annotationType != null) {
Element annotationElement = annotationType.asElement();
if (annotationElement != null) {
requestMapping = annotationElement.getAnnotation(org.springframework.web.bind.annotation.RequestMapping.class);
if (requestMapping != null) {
Set<String> subpaths = new TreeSet<String>();
Map<? extends ExecutableElement, ? extends AnnotationValue> elementValues = annotation.getElementValues();
for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : elementValues.entrySet()) {
if (entry.getKey().getSimpleName().contentEquals("value") || entry.getKey().getSimpleName().contentEquals("path")) {
Object value = entry.getValue().getValue();
if (value instanceof List) {
for (int i = 0; i < ((List) value).size(); i++) {
AnnotationValue valueItem = (AnnotationValue) ((List) value).get(i);
subpaths.add(String.valueOf(valueItem.getValue()));
}
}
}
}
return subpaths;
}
}
}
}
}
}
return null;
}
private String[] findProduces(ExecutableElement method) {
org.springframework.web.bind.annotation.RequestMapping requestMapping = method.getAnnotation(org.springframework.web.bind.annotation.RequestMapping.class);
if (requestMapping != null) {
return requestMapping.produces();
}
else {
List<? extends AnnotationMirror> annotations = method.getAnnotationMirrors();
if (annotations != null) {
for (AnnotationMirror annotation : annotations) {
DeclaredType annotationType = annotation.getAnnotationType();
if (annotationType != null) {
Element annotationElement = annotationType.asElement();
if (annotationElement != null) {
requestMapping = annotationElement.getAnnotation(org.springframework.web.bind.annotation.RequestMapping.class);
if (requestMapping != null) {
Map<? extends ExecutableElement, ? extends AnnotationValue> elementValues = annotation.getElementValues();
for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : elementValues.entrySet()) {
if (entry.getKey().getSimpleName().contentEquals("produces")) {
Object value = entry.getValue().getValue();
if (value instanceof List) {
String[] produces = new String[((List)value).size()];
for (int i = 0; i < ((List) value).size(); i++) {
AnnotationValue valueItem = (AnnotationValue) ((List) value).get(i);
produces[i] = String.valueOf(valueItem.getValue());
}
return produces;
}
}
}
}
}
}
}
}
}
return null;
}
/**
* Extracts out the components of a path.
*
* @param path The path.
*/
protected static List<PathSegment> extractPathComponents(String path) {
List<PathSegment> components = new ArrayList<PathSegment>();
if (path != null) {
StringBuilder value = new StringBuilder();
if (!path.startsWith("/")) {
value.append("/");//first path segment should always start with "/"
}
StringBuilder variable = new StringBuilder();
StringBuilder regexp = new StringBuilder();
int inBrace = 0;
boolean definingRegexp = false;
for (int i = 0; i < path.length(); i++) {
char ch = path.charAt(i);
if (ch == '{') {
inBrace++;
if (inBrace == 1) {
//outer brace defines new path segment
if (value.length() > 0) {
components.add(new PathSegment(value.toString(), variable.length() > 0 ? variable.toString() : null, regexp.length() > 0 ? regexp.toString() : null));
}
value = new StringBuilder();
variable = new StringBuilder();
regexp = new StringBuilder();
}
}
else if (ch == '}') {
inBrace--;
if (inBrace == 0) {
definingRegexp = false;
}
}
else if (inBrace == 1 && ch == ':') {
definingRegexp = true;
continue;
}
else if (!definingRegexp && !Character.isWhitespace(ch) && inBrace > 0) {
variable.append(ch);
}
if (definingRegexp) {
regexp.append(ch);
}
else if (!Character.isWhitespace(ch)) {
value.append(ch);
}
}
if (value.length() > 0) {
components.add(new PathSegment(value.toString(), variable.length() > 0 ? variable.toString() : null, regexp.length() > 0 ? regexp.toString() : null));
}
}
return components;
}
/**
* Whether the specified method is overridden by any of the methods in the specified list.
*
* @param method The method.
* @param resourceMethods The method list.
* @return If the methdo is overridden by any of the methods in the list.
*/
protected boolean isOverridden(ExecutableElement method, ArrayList<? extends ExecutableElement> resourceMethods) {
Elements decls = this.env.getElementUtils();
for (ExecutableElement resourceMethod : resourceMethods) {
if (decls.overrides(resourceMethod, method, (TypeElement) resourceMethod.getEnclosingElement())) {
return true;
}
}
return false;
}
public EnunciateSpringWebContext getContext() {
return context;
}
/**
* The path to this resource.
*
* @return The path to this resource.
*/
public final Set<String> getPaths() {
return this.paths;
}
/**
* The MIME types that the methods on this resource consumes (possibly overridden).
*
* @return The MIME types that the methods on this resource consumes.
*/
public Set<String> getConsumesMime() {
return consumesMime;
}
/**
* The MIME types that the methods on this resource consumes (possibly overridden).
*
* @return The MIME types that the methods on this resource consumes.
*/
public Set<String> getProducesMime() {
return producesMime;
}
/**
* The resource methods.
*
* @return The resource methods.
*/
public List<RequestMapping> getRequestMappings() {
return requestMappings;
}
/**
* The facets here applicable.
*
* @return The facets here applicable.
*/
public Set<Facet> getFacets() {
return facets;
}
/**
* Get the request methods applicable to this controller.
*
* @return The request methods applicable to this controller.
*/
public Set<RequestMethod> getApplicableMethods() {
EnumSet<RequestMethod> applicableMethods = EnumSet.allOf(RequestMethod.class);
if (this.mappingInfo != null) {
RequestMethod[] methods = this.mappingInfo.method();
if (methods.length > 0) {
applicableMethods.retainAll(Arrays.asList(methods));
}
}
return applicableMethods;
}
/**
* The security roles for this resource.
*
* @return The security roles for this resource.
*/
public Set<String> getSecurityRoles() {
TreeSet<String> roles = new TreeSet<String>();
RolesAllowed rolesAllowed = getAnnotation(RolesAllowed.class);
if (rolesAllowed != null) {
Collections.addAll(roles, rolesAllowed.value());
}
return roles;
}
}