/**
* 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;
import com.webcohesion.enunciate.EnunciateContext;
import com.webcohesion.enunciate.EnunciateException;
import com.webcohesion.enunciate.api.ApiRegistry;
import com.webcohesion.enunciate.module.*;
import com.webcohesion.enunciate.modules.spring_web.model.*;
import com.webcohesion.enunciate.util.PathSortStrategy;
import org.reflections.adapters.MetadataAdapter;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.RestController;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import java.util.*;
/**
* @author Ryan Heaton
*/
@SuppressWarnings ( "unchecked" )
public class SpringWebModule extends BasicProviderModule implements TypeDetectingModule, ApiRegistryProviderModule, ApiFeatureProviderModule {
private DataTypeDetectionStrategy defaultDataTypeDetectionStrategy;
private final List<MediaTypeDefinitionModule> mediaTypeModules = new ArrayList<MediaTypeDefinitionModule>();
private EnunciateSpringWebContext springContext;
static final String NAME = "spring-web";
private PathSortStrategy defaultSortStrategy = PathSortStrategy.breadth_first;
@Override
public String getName() {
return NAME;
}
@Override
public List<DependencySpec> getDependencySpecifications() {
return Arrays.asList((DependencySpec) new MediaTypeDependencySpec());
}
public DataTypeDetectionStrategy getDataTypeDetectionStrategy() {
String dataTypeDetection = this.config.getString("[@datatype-detection]", null);
if (dataTypeDetection != null) {
try {
return DataTypeDetectionStrategy.valueOf(dataTypeDetection);
}
catch (IllegalArgumentException e) {
//fall through...
}
}
if (this.defaultDataTypeDetectionStrategy != null) {
return this.defaultDataTypeDetectionStrategy;
}
if (this.enunciate.getIncludePatterns().isEmpty()) {
//if there are no configured include patterns, then we'll just stick with "local" detection so we don't include too much.
return DataTypeDetectionStrategy.local;
}
else {
//otherwise, we'll assume the user knows what (s)he's doing and aggressively include everything.
return DataTypeDetectionStrategy.aggressive;
}
}
public void setDefaultDataTypeDetectionStrategy(DataTypeDetectionStrategy strategy) {
this.defaultDataTypeDetectionStrategy = strategy;
}
public PathSortStrategy getPathSortStrategy() {
PathSortStrategy strategy = defaultSortStrategy;
try {
strategy = PathSortStrategy.valueOf(this.config.getString("[@path-sort-strategy]", this.defaultSortStrategy.name()));
} catch (IllegalArgumentException e) {
// Ignore? Log?
}
return strategy;
}
public boolean isDisableExamples() {
return this.config.getBoolean("[@disableExamples]", false);
}
public void setDefaultSortStrategy(PathSortStrategy defaultSortStrategy) {
this.defaultSortStrategy = defaultSortStrategy;
}
public EnunciateSpringWebContext getSpringWebContext() {
return springContext;
}
@Override
public ApiRegistry getApiRegistry() {
return new SpringWebApiRegistry(this.springContext);
}
@Override
public void call(EnunciateContext context) {
springContext = new EnunciateSpringWebContext(context, isDisableExamples());
DataTypeDetectionStrategy detectionStrategy = getDataTypeDetectionStrategy();
if (detectionStrategy != DataTypeDetectionStrategy.passive) {
Set<? extends Element> elements = detectionStrategy == DataTypeDetectionStrategy.local ? context.getLocalApiElements() : context.getApiElements();
for (Element declaration : elements) {
//first loop through and gather all the controller advice.
if (declaration instanceof TypeElement) {
TypeElement element = (TypeElement) declaration;
Controller controllerInfo = declaration.getAnnotation(Controller.class);
RestController restControllerInfo = declaration.getAnnotation(RestController.class);
ControllerAdvice controllerAdvice = declaration.getAnnotation(ControllerAdvice.class);
if (controllerInfo != null || restControllerInfo != null || controllerAdvice != null) {
springContext.add(new SpringControllerAdvice(element, springContext));
}
}
}
for (Element declaration : elements) {
if (declaration instanceof TypeElement) {
TypeElement element = (TypeElement) declaration;
Controller controllerInfo = declaration.getAnnotation(Controller.class);
RestController restControllerInfo = declaration.getAnnotation(RestController.class);
if (controllerInfo != null || restControllerInfo != null) {
//add root resource.
SpringController springController = new SpringController(element, springContext);
LinkedList<Element> contextStack = new LinkedList<Element>();
contextStack.push(springController);
try {
List<RequestMapping> requestMappings = springController.getRequestMappings();
if (!requestMappings.isEmpty()) {
springContext.add(springController);
for (RequestMapping requestMapping : requestMappings) {
addReferencedDataTypeDefinitions(requestMapping, contextStack);
}
}
}
finally {
contextStack.pop();
}
}
}
}
}
//tidy up the application path.
String relativeContextPath = this.config.getString("application[@path]", "");
while (relativeContextPath.startsWith("/")) {
relativeContextPath = relativeContextPath.substring(1);
}
while (relativeContextPath.endsWith("/")) {
//trim off any leading slashes
relativeContextPath = relativeContextPath.substring(0, relativeContextPath.length() - 1);
}
springContext.setRelativeContextPath(relativeContextPath);
springContext.setGroupingStrategy(getGroupingStrategy());
springContext.setPathSortStrategy(getPathSortStrategy());
}
/**
* Add the referenced type definitions for the specified resource method.
*
* @param requestMapping The resource method.
*/
protected void addReferencedDataTypeDefinitions(RequestMapping requestMapping, LinkedList<Element> contextStack) {
ResourceEntityParameter ep = requestMapping.getEntityParameter();
if (ep != null) {
Set<String> consumes = requestMapping.getConsumesMediaTypes();
contextStack.push(ep.getDelegate());
TypeMirror type = ep.getType();
contextStack.push(requestMapping);
try {
for (MediaTypeDefinitionModule mediaTypeModule : this.mediaTypeModules) {
mediaTypeModule.addDataTypeDefinitions(type, consumes, contextStack);
}
}
finally {
contextStack.pop();
}
}
ResourceRepresentationMetadata outputPayload = requestMapping.getRepresentationMetadata();
if (outputPayload != null) {
TypeMirror type = outputPayload.getDelegate();
Set<String> produces = requestMapping.getProducesMediaTypes();
contextStack.push(requestMapping);
try {
for (MediaTypeDefinitionModule mediaTypeModule : this.mediaTypeModules) {
mediaTypeModule.addDataTypeDefinitions(type, produces, contextStack);
}
}
finally {
contextStack.pop();
}
}
List<? extends ResponseCode> statusCodes = requestMapping.getStatusCodes();
if (statusCodes != null) {
for (ResponseCode statusCode : statusCodes) {
TypeMirror type = statusCode.getType();
if (type != null) {
Set<String> produces = requestMapping.getProducesMediaTypes();
contextStack.push(requestMapping);
try {
for (MediaTypeDefinitionModule mediaTypeModule : this.mediaTypeModules) {
mediaTypeModule.addDataTypeDefinitions(type, produces, contextStack);
}
}
finally {
contextStack.pop();
}
}
}
}
}
public EnunciateSpringWebContext.GroupingStrategy getGroupingStrategy() {
String groupBy = this.config.getString("[@groupBy]", "class");
if ("class".equals(groupBy)) {
return EnunciateSpringWebContext.GroupingStrategy.resource_class;
}
else if ("path".equals(groupBy)) {
return EnunciateSpringWebContext.GroupingStrategy.path;
}
else if ("annotation".equals(groupBy)) {
return EnunciateSpringWebContext.GroupingStrategy.annotation;
}
else {
throw new EnunciateException("Unknown grouping strategy: " + groupBy);
}
}
@Override
public boolean typeDetected(Object type, MetadataAdapter metadata) {
String classname = metadata.getClassName(type);
if (classname.startsWith("org.springframework")) {
//don't accept spring system-specific types
return false;
}
List<String> classAnnotations = metadata.getClassAnnotationNames(type);
if (classAnnotations != null) {
for (String classAnnotation : classAnnotations) {
if ((Controller.class.getName().equals(classAnnotation))
|| (RestController.class.getName().equals(classAnnotation))) {
return true;
}
}
}
return false;
}
public class MediaTypeDependencySpec implements DependencySpec {
@Override
public boolean accept(EnunciateModule module) {
if (module instanceof MediaTypeDefinitionModule) {
MediaTypeDefinitionModule definitionModule = (MediaTypeDefinitionModule) module;
mediaTypeModules.add(definitionModule);
// suggest to the media type definition module that it should take a passive approach to detecting data types
// because this module will be aggressively adding the data type definitions to it.
definitionModule.setDefaultDataTypeDetectionStrategy(DataTypeDetectionStrategy.passive);
return true;
}
return false;
}
@Override
public boolean isFulfilled() {
// this spec is always fulfilled.
return true;
}
@Override
public String toString() {
return "media type definition modules";
}
}
}