/** * */ package org.minnal.instrument.resource.creator; import java.io.StringWriter; import java.util.ArrayList; import java.util.List; import java.util.Properties; import java.util.Set; import javassist.CannotCompileException; import javassist.CtClass; import javassist.CtMethod; import javassist.NotFoundException; import javassist.bytecode.ConstPool; import javassist.bytecode.annotation.Annotation; import javassist.bytecode.annotation.AnnotationMemberValue; import javassist.bytecode.annotation.ArrayMemberValue; import javassist.bytecode.annotation.BooleanMemberValue; import javassist.bytecode.annotation.ClassMemberValue; import javassist.bytecode.annotation.IntegerMemberValue; import javassist.bytecode.annotation.StringMemberValue; import javax.annotation.security.RolesAllowed; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import org.apache.velocity.Template; import org.apache.velocity.VelocityContext; import org.apache.velocity.app.VelocityEngine; import org.apache.velocity.runtime.RuntimeConstants; import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader; import org.javalite.common.Inflector; import org.minnal.instrument.entity.metadata.CollectionMetaData; import org.minnal.instrument.entity.metadata.PermissionMetaData; import org.minnal.instrument.resource.ResourceWrapper.ResourcePath; import org.minnal.instrument.resource.metadata.ResourceMetaData; import org.minnal.instrument.resource.metadata.ResourceMethodMetaData; import org.minnal.instrument.util.JavassistUtils; import org.minnal.utils.http.HttpUtil; import org.minnal.utils.route.RoutePattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.wordnik.swagger.annotations.ApiImplicitParam; import com.wordnik.swagger.annotations.ApiImplicitParams; import com.wordnik.swagger.annotations.ApiOperation; import com.wordnik.swagger.annotations.ApiResponse; import com.wordnik.swagger.annotations.ApiResponses; /** * @author ganeshs * */ public abstract class AbstractMethodCreator { protected final static VelocityEngine engine; static { Properties properties = new Properties(); properties.put("runtime.log.logsystem.class", "org.minnal.utils.Slf4jLogChute"); engine = new VelocityEngine(properties); engine.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath"); engine.setProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName()); } private CtClass ctClass; private ResourceMetaData resource; private ResourcePath resourcePath; private String basePath; private static final Logger logger = LoggerFactory.getLogger(AbstractMethodCreator.class); /** * @param ctClass * @param resource * @param resourcePath * @param basePath */ public AbstractMethodCreator(CtClass ctClass, ResourceMetaData resource, ResourcePath resourcePath, String basePath) { this.ctClass = ctClass; this.resource = resource; this.resourcePath = resourcePath; this.basePath = basePath; } /** * @return */ public CtClass getCtClass() { return ctClass; } /** * @return */ public ResourcePath getResourcePath() { return resourcePath; } /** * @return the resource */ public ResourceMetaData getResource() { return resource; } /** * @return the basePath */ public String getBasePath() { return basePath; } /** * Checks if the method already exists in the class * * @return */ protected boolean shouldCreate() { if (resource == null) { return true; } RoutePattern pattern = getRoutePattern(); for (ResourceMethodMetaData resourceMethod : resource.getAllResourceMethods()) { if (resourceMethod.getPattern().equals(pattern) && resourceMethod.getHttpMethod().equalsIgnoreCase(getHttpMethod())) { return false; } } return true; } protected Class<?>[] getParams(CtMethod ctMethod) throws NotFoundException, CannotCompileException { Class<?>[] params = new Class<?>[ctMethod.getParameterTypes().length]; for (int i = 0; i < ctMethod.getParameterTypes().length; i++) { params[i] = ctClass.getClassPool().toClass(ctMethod.getParameterTypes()[i]); } return params; } /** * Creates the method * * @return * @throws CannotCompileException */ public CtMethod create() throws CannotCompileException { if (! shouldCreate()) { return null; } VelocityContext context = new VelocityContext(); String methodBody = createMethodBody(context); return makeMethod(methodBody); } protected RoutePattern getRoutePattern() { if (resourcePath.isBulk()) { return new RoutePattern(resourcePath.getBulkPath()); } return new RoutePattern(resourcePath.getSinglePath()); } /** * Creates the method body * * @param context * @return */ protected String createMethodBody(VelocityContext context) { context.put("inflector", Inflector.class); context.put("path", resourcePath.getNodePath()); context.put("param_names", getRoutePattern().getParameterNames()); Template template = getTemplate(); StringWriter writer = new StringWriter(); logger.debug("Creating the method body with context {} and template {} for the resource path {} and method {}", context, template.getName(), resourcePath, getHttpMethod()); template.merge(context, writer); return writer.toString(); } /** * Returns the relative path of this method from the root * * @return */ protected String getRelativePath() { String path = resourcePath.isAction() ? resourcePath.getActionPath() : resourcePath.isBulk() ? resourcePath.getBulkPath() : resourcePath.getSinglePath(); return HttpUtil.deriveRelativePath(this.basePath, path); } /** * Create the method from the method body * * @param body * @return * @throws CannotCompileException */ protected CtMethod makeMethod(String body) throws CannotCompileException { logger.trace("Adding the method {} to the class {}", body, ctClass); CtMethod ctMethod = CtMethod.make(body, ctClass); addAnnotations(ctMethod); addParamAnnotations(ctMethod); getCtClass().addMethod(ctMethod); return ctMethod; } /** * Adds the annotations to the method parameters * * @param ctMethod */ protected abstract void addParamAnnotations(CtMethod ctMethod); /** * Adds the annotations to the method * * @param ctMethod */ protected void addAnnotations(CtMethod ctMethod) { JavassistUtils.addMethodAnnotations(ctMethod, getMethodAnnotation(), getPathAnnotation(), getApiOperationAnnotation(), getApiParamAnnotations(), getSecurityAnnotation(), getApiResponsesAnnotation(), getProducesAnnotation()); } /** * Returns the #{@link GET} / #{@link POST} / #{@link PUT} / #{@link DELETE} annotation * * @return */ protected Annotation getMethodAnnotation() { return new Annotation(getHttpAnnotation().getCanonicalName(), ctClass.getClassFile().getConstPool()); } /** * Returns the #{@link Path} annotation * * @return */ protected Annotation getPathAnnotation() { String relativePath = getRelativePath(); if (Strings.isNullOrEmpty(relativePath)) { return null; } ConstPool constPool = ctClass.getClassFile().getConstPool(); Annotation pathAnnotation = new Annotation(Path.class.getCanonicalName(), constPool); pathAnnotation.addMemberValue("value", new StringMemberValue(relativePath, constPool)); return pathAnnotation; } /** * Returns the #{@link ApiOperation} annotation * * @return */ protected abstract Annotation getApiOperationAnnotation(); /** * Returns the api path parameter annotations * * @return */ protected List<Annotation> getApiPathParamAnnotations() { List<Annotation> annotations = new ArrayList<Annotation>(); List<String> parameters = getRoutePattern().getParameterNames(); for (int i = 0; i < parameters.size(); i++) { Annotation annotation = new Annotation(ApiImplicitParam.class.getCanonicalName(), ctClass.getClassFile().getConstPool()); annotation.addMemberValue("name", new StringMemberValue(parameters.get(i), ctClass.getClassFile().getConstPool())); annotation.addMemberValue("paramType", new StringMemberValue("path", ctClass.getClassFile().getConstPool())); annotation.addMemberValue("dataType", new StringMemberValue(String.class.getCanonicalName(), ctClass.getClassFile().getConstPool())); annotation.addMemberValue("value", new StringMemberValue("The " + getResourcePath().getNodePath().get(i).getEntityMetaData().getName() + " identifier", ctClass.getClassFile().getConstPool())); annotation.addMemberValue("required", new BooleanMemberValue(true, ctClass.getClassFile().getConstPool())); if (i == parameters.size() - 1) { annotation.addMemberValue("allowMultiple", new BooleanMemberValue(true, ctClass.getClassFile().getConstPool())); } annotations.add(annotation); } return annotations; } /** * Returns the api query parameter annotations * * @return */ protected List<Annotation> getApiQueryParamAnnotations() { return Lists.newArrayList(); } protected List<Annotation> getApiAdditionalParamAnnotations() { List<Annotation> annotations = new ArrayList<Annotation>(); // Exclude params Annotation annotation = new Annotation(ApiImplicitParam.class.getCanonicalName(), ctClass.getClassFile().getConstPool()); annotation.addMemberValue("name", new StringMemberValue("exclude", ctClass.getClassFile().getConstPool())); annotation.addMemberValue("paramType", new StringMemberValue("query", ctClass.getClassFile().getConstPool())); annotation.addMemberValue("dataType", new StringMemberValue(String.class.getCanonicalName(), ctClass.getClassFile().getConstPool())); annotation.addMemberValue("value", new StringMemberValue("Comma seperated fields to exclude from the response", ctClass.getClassFile().getConstPool())); annotation.addMemberValue("allowMultiple", new BooleanMemberValue(true, ctClass.getClassFile().getConstPool())); annotations.add(annotation); // Exclude params annotation = new Annotation(ApiImplicitParam.class.getCanonicalName(), ctClass.getClassFile().getConstPool()); annotation.addMemberValue("name", new StringMemberValue("include", ctClass.getClassFile().getConstPool())); annotation.addMemberValue("paramType", new StringMemberValue("query", ctClass.getClassFile().getConstPool())); annotation.addMemberValue("dataType", new StringMemberValue(String.class.getCanonicalName(), ctClass.getClassFile().getConstPool())); annotation.addMemberValue("value", new StringMemberValue("Comma seperated fields to include in the response", ctClass.getClassFile().getConstPool())); annotation.addMemberValue("allowMultiple", new BooleanMemberValue(true, ctClass.getClassFile().getConstPool())); annotations.add(annotation); return annotations; } /** * Returns the api parameter annotations * * @return */ protected Annotation getApiParamAnnotations() { Annotation implicitParams = new Annotation(ApiImplicitParams.class.getCanonicalName(), ctClass.getClassFile().getConstPool()); List<AnnotationMemberValue> annotationMemberValues = new ArrayList<AnnotationMemberValue>(); List<Annotation> annotations = Lists.newArrayList(); annotations.addAll(getApiPathParamAnnotations()); annotations.addAll(getApiQueryParamAnnotations()); annotations.addAll(getApiAdditionalParamAnnotations()); for (Annotation annotation : annotations) { annotationMemberValues.add(new AnnotationMemberValue(annotation, ctClass.getClassFile().getConstPool())); } ArrayMemberValue values = new ArrayMemberValue(ctClass.getClassFile().getConstPool()); values.setValue(annotationMemberValues.toArray(new AnnotationMemberValue[0])); implicitParams.addMemberValue("value", values); return implicitParams; } /** * Returns the security annotation * * @return */ protected Annotation getSecurityAnnotation() { Set<String> permissions = getPermissions(); if (permissions == null || permissions.isEmpty()) { return null; } Annotation rolesAllowed = new Annotation(RolesAllowed.class.getCanonicalName(), ctClass.getClassFile().getConstPool()); ArrayMemberValue values = new ArrayMemberValue(ctClass.getClassFile().getConstPool()); List<StringMemberValue> memberValues = new ArrayList<StringMemberValue>(); for (String permission : permissions) { memberValues.add(new StringMemberValue(permission, ctClass.getClassFile().getConstPool())); } values.setValue(memberValues.toArray(new StringMemberValue[0])); rolesAllowed.addMemberValue("value", values); return rolesAllowed; } protected Annotation getApiResponsesAnnotation() { Annotation apiResponses = new Annotation(ApiResponses.class.getCanonicalName(), ctClass.getClassFile().getConstPool()); ArrayMemberValue values = new ArrayMemberValue(ctClass.getClassFile().getConstPool()); List<AnnotationMemberValue> memberValues = new ArrayList<AnnotationMemberValue>(); for (Annotation annotation : getApiResponseAnnotations()) { memberValues.add(new AnnotationMemberValue(annotation, ctClass.getClassFile().getConstPool())); } values.setValue(memberValues.toArray(new AnnotationMemberValue[0])); apiResponses.addMemberValue("value", values); return apiResponses; } protected Annotation getProducesAnnotation() { Annotation annotation = new Annotation(Produces.class.getCanonicalName(), getCtClass().getClassFile().getConstPool()); ArrayMemberValue values = new ArrayMemberValue(getCtClass().getClassFile().getConstPool()); StringMemberValue json = new StringMemberValue(MediaType.APPLICATION_JSON, getCtClass().getClassFile().getConstPool()); StringMemberValue xml = new StringMemberValue(MediaType.APPLICATION_XML, getCtClass().getClassFile().getConstPool()); values.setValue(new StringMemberValue[]{json, xml}); annotation.addMemberValue("value", values); return annotation; } protected abstract List<Annotation> getApiResponseAnnotations(); protected abstract String getHttpMethod(); protected abstract Class<?> getHttpAnnotation(); /** * Returns the permissions required to access this method * * @return */ protected abstract Set<String> getPermissions(); /** * Returns the permissions for the given http method * * @return */ protected Set<String> getPermissions(String httpMethod) { Set<PermissionMetaData> permissions = Sets.newHashSet(); if (resourcePath.getNodePath().size() == 1) { permissions = resourcePath.getNodePath().get(0).getEntityMetaData().getPermissionMetaData(); } else { CollectionMetaData source = resourcePath.getNodePath().get(resourcePath.getNodePath().size() - 1).getSource(); if (source != null) { permissions = source.getPermissionMetaData(); } } for (PermissionMetaData permission : permissions) { if (permission.getMethod().equalsIgnoreCase(httpMethod)) { return permission.getPermissions(); } } return Sets.newHashSet(); } /** * Returns the template that will be used to create the method * * @return */ protected abstract Template getTemplate(); /** * Returns the 200 ok response annotation * * @param responseClass * @return */ protected Annotation getOkResponseAnnotation(Class<?> responseClass) { ConstPool constPool = ctClass.getClassFile().getConstPool(); Annotation annotation = new Annotation(ApiResponse.class.getCanonicalName(), constPool); IntegerMemberValue code = new IntegerMemberValue(constPool); code.setValue(Response.Status.OK.getStatusCode()); annotation.addMemberValue("code", code); annotation.addMemberValue("message", new StringMemberValue(Response.Status.OK.getReasonPhrase(), constPool)); annotation.addMemberValue("response", new ClassMemberValue(responseClass.getCanonicalName(), constPool)); return annotation; } /** * Returns the 404 not found response annotation * * @param responseClass * @return */ protected Annotation getNotFoundResponseAnnotation() { ConstPool constPool = ctClass.getClassFile().getConstPool(); Annotation annotation = new Annotation(ApiResponse.class.getCanonicalName(), constPool); IntegerMemberValue code = new IntegerMemberValue(constPool); code.setValue(Response.Status.NOT_FOUND.getStatusCode()); annotation.addMemberValue("code", code); annotation.addMemberValue("message", new StringMemberValue(Response.Status.NOT_FOUND.getReasonPhrase(), constPool)); return annotation; } /** * Returns the 204 no content response annotation * * @param responseClass * @return */ protected Annotation getNoContentResponseAnnotation() { ConstPool constPool = ctClass.getClassFile().getConstPool(); Annotation annotation = new Annotation(ApiResponse.class.getCanonicalName(), constPool); IntegerMemberValue code = new IntegerMemberValue(constPool); code.setValue(Response.Status.NO_CONTENT.getStatusCode()); annotation.addMemberValue("code", code); annotation.addMemberValue("message", new StringMemberValue(Response.Status.NO_CONTENT.getReasonPhrase(), constPool)); return annotation; } /** * Returns the 400 bad request response annotation * * @param responseClass * @return */ protected Annotation getBadRequestResponseAnnotation() { ConstPool constPool = ctClass.getClassFile().getConstPool(); Annotation annotation = new Annotation(ApiResponse.class.getCanonicalName(), constPool); IntegerMemberValue code = new IntegerMemberValue(constPool); code.setValue(Response.Status.BAD_REQUEST.getStatusCode()); annotation.addMemberValue("code", code); annotation.addMemberValue("message", new StringMemberValue(Response.Status.BAD_REQUEST.getReasonPhrase(), constPool)); return annotation; } }