Java tutorial
/******************************************************************************* * Copyright (c) 2012 GigaSpaces Technologies Ltd. All rights reserved * * 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.cloudifysource.restDoclet.generation; import java.io.File; import java.io.FileWriter; import java.io.StringWriter; import java.io.Writer; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.SortedMap; import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.lang.StringUtils; import org.apache.velocity.Template; import org.apache.velocity.VelocityContext; import org.apache.velocity.app.Velocity; import org.apache.velocity.runtime.RuntimeConstants; import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader; import org.cloudifysource.restDoclet.constants.RestDocConstants; import org.cloudifysource.restDoclet.docElements.DocAnnotation; import org.cloudifysource.restDoclet.docElements.DocController; import org.cloudifysource.restDoclet.docElements.DocHttpMethod; import org.cloudifysource.restDoclet.docElements.DocJsonRequestExample; import org.cloudifysource.restDoclet.docElements.DocJsonResponseExample; import org.cloudifysource.restDoclet.docElements.DocMethod; import org.cloudifysource.restDoclet.docElements.DocParameter; import org.cloudifysource.restDoclet.docElements.DocRequestMappingAnnotation; import org.cloudifysource.restDoclet.docElements.DocReturnDetails; import org.cloudifysource.restDoclet.exampleGenerators.DefaultRequestBodyParameterFilter; import org.cloudifysource.restDoclet.exampleGenerators.DocDefaultExampleGenerator; import org.cloudifysource.restDoclet.exampleGenerators.IDocExampleGenerator; import org.cloudifysource.restDoclet.exampleGenerators.IRequestBodyParamFilter; import com.sun.javadoc.AnnotationDesc; import com.sun.javadoc.ClassDoc; import com.sun.javadoc.MethodDoc; import com.sun.javadoc.Parameter; import com.sun.javadoc.RootDoc; import com.sun.javadoc.Tag; import com.sun.javadoc.Type; /** * Generates REST API documentation in an HTML form. <br /> * Uses velocity template to generate an HTML file that contains the * documentation. * <ul> * <li>To specify your sources change the values of * {@link RestDocConstants#SOURCE_PATH} and * {@link RestDocConstants#CONTROLLERS_PACKAGE}.</li> * <li>To specify different template path change the value * {@link RestDocConstants#VELOCITY_TEMPLATE_PATH}.</li> * <li>To specify the destination path of the result HTML change the value * {@link RestDocConstants#DOC_DEST_PATH}.</li> * </ul> * In default the Generator uses the velocity template * {@link RestDocConstants#VELOCITY_TEMPLATE_PATH} and writes the result to * {@link RestDocConstants#DOC_DEST_PATH}. * * @author yael * */ public class Generator { private static final Logger logger = Logger.getLogger(Generator.class.getName()); private static final String REQUEST_HAS_NO_BODY_MSG = "request has no body"; private static final String RESPONSE_HAS_NO_BODY_MSG = "response has no body"; private static final String LINE_SEPARATOR = System.getProperty("line.separator"); private final RootDoc documentation; private String velocityTemplatePath; private String velocityTemplateFileName; private boolean isUserDefineTemplatePath = false; private String docPath; private String version; private String docCssPath; private static String requestExampleGeneratorName; private static String responseExampleGeneratorName; private static IDocExampleGenerator requestExampleGenerator; private static IDocExampleGenerator responseExampleGenerator; private static String requestBodyParamFilterName; private static IRequestBodyParamFilter requestBodyParamFilter; /** * * @param rootDoc */ public Generator(final RootDoc rootDoc) { documentation = rootDoc; setFlags(documentation.options()); } /** * * @param args . * <p>This class uses the annotationType() method of class DocAnnotation, * so if there is an annotation in the source with its class not in the class path, * a ClassCastException will be thrown. * <br>For example, in order to use the PreAuthorize annotation, * the spring-security-core JAR needs to be added to the class path. * <br><a href="http://stackoverflow.com/questions/5314738/javadoc-annotations-from-third-party-libraries"> * related question in stackoverflow</a> */ public static void main(final String[] args) { /** * This class uses the annotationType() method of class DocAnnotation, * so if there is an annotation in the source which its class is not in the class path, * a ClassCastException will be thrown. * For example, to use the PreAuthorize annotation, * the spring-security-core JAR need to be added to the class path. * See <a href="http://stackoverflow.com/questions/5314738/javadoc-annotations-from-third-party-libraries"> * related question in stackoverflow</a> **/ com.sun.tools.javadoc.Main.execute(new String[] { RestDocConstants.DOCLET_FLAG, RestDoclet.class.getName(), RestDocConstants.SOURCE_PATH_FLAG, RestDocConstants.SOURCES_PATH, RestDocConstants.CONTROLLERS_PACKAGE, RestDocConstants.VELOCITY_TEMPLATE_PATH_FLAG, RestDocConstants.VELOCITY_TEMPLATE_PATH, RestDocConstants.DOC_DEST_PATH_FLAG, RestDocConstants.DOC_DEST_PATH, RestDocConstants.DOC_CSS_PATH_FLAG, RestDocConstants.DOC_CSS_PATH, RestDocConstants.VERSION_FLAG, RestDocConstants.VERSION }); } /** * * @param options */ private void setFlags(final String[][] options) { int flagPos = 0; int contentPos = 1; for (int i = 0; i < options.length; i++) { String flagName = options[i][flagPos]; String flagValue = null; if (options[i].length > 1) { flagValue = options[i][contentPos]; } if (RestDocConstants.VELOCITY_TEMPLATE_PATH_FLAG.equals(flagName)) { velocityTemplatePath = flagValue; logger.log(Level.INFO, "Updating flag " + flagName + " value = " + flagValue); } else if (RestDocConstants.DOC_DEST_PATH_FLAG.equals(flagName)) { docPath = flagValue; logger.log(Level.INFO, "Updating flag " + flagName + " value = " + flagValue); } else if (RestDocConstants.VERSION_FLAG.equals(flagName)) { version = flagValue; logger.log(Level.INFO, "Updating flag " + flagName + " value = " + flagValue); } else if (RestDocConstants.DOC_CSS_PATH_FLAG.equals(flagName)) { docCssPath = flagValue; logger.log(Level.INFO, "Updating flag " + flagName + " value = " + flagValue); } else if (RestDocConstants.REQUEST_EXAMPLE_GENERATOR_CLASS_FLAG.equals(flagName)) { requestExampleGeneratorName = flagValue; logger.log(Level.INFO, "Updating flag " + flagName + " value = " + flagValue); } else if (RestDocConstants.RESPONSE_EXAMPLE_GENERATOR_CLASS_FLAG.equals(flagName)) { responseExampleGeneratorName = flagValue; logger.log(Level.INFO, "Updating flag " + flagName + " value = " + flagValue); } else if (RestDocConstants.REQUEST_BODY_PARAM_FILTER_CLASS_FLAG.equals(flagName)) { requestBodyParamFilterName = flagValue; logger.log(Level.INFO, "Updating flag " + flagName + " value = " + flagValue); } } if (!StringUtils.isBlank(velocityTemplatePath)) { isUserDefineTemplatePath = true; int fileNameIndex = velocityTemplatePath.lastIndexOf(File.separator) + 1; velocityTemplateFileName = velocityTemplatePath.substring(fileNameIndex); velocityTemplatePath = velocityTemplatePath.substring(0, fileNameIndex - 1); } else { velocityTemplateFileName = RestDocConstants.VELOCITY_TEMPLATE_FILE_NAME; velocityTemplatePath = this.getClass().getClassLoader().getResource(velocityTemplateFileName).getPath(); } if (StringUtils.isBlank(docPath)) { docPath = RestDocConstants.DOC_DEST_PATH; } if (StringUtils.isBlank(version)) { version = RestDocConstants.VERSION; } if (StringUtils.isBlank(docCssPath)) { docCssPath = RestDocConstants.DOC_CSS_PATH; } initRequestExampleGenerator(requestExampleGeneratorName); logger.log(Level.INFO, "Updating request example generator class to " + requestExampleGenerator.getClass().getName()); initResponseExampleGenerator(responseExampleGeneratorName); logger.log(Level.INFO, "Updating response example generator class to " + responseExampleGenerator.getClass().getName()); initRequestBodyParamFilter(); logger.log(Level.INFO, "Updating request body parameter filter class to " + requestBodyParamFilter.getClass().getName()); } private void initRequestBodyParamFilter() { if (StringUtils.isBlank(requestBodyParamFilterName)) { requestBodyParamFilter = new DefaultRequestBodyParameterFilter(); } else { try { Class<?> clazz = Class.forName(requestBodyParamFilterName); requestBodyParamFilter = (IRequestBodyParamFilter) clazz.newInstance(); } catch (Exception e) { logger.log(Level.WARNING, "Cought " + e.getClass().getName() + " when tried to load and instantiate class " + requestBodyParamFilterName + ". Using a default filter class instead."); requestBodyParamFilter = new DefaultRequestBodyParameterFilter(); } } } private void initRequestExampleGenerator(final String exampleGeneratorName) { IDocExampleGenerator exampleGeneratorClass = getExampleGeneratorClass(IDocExampleGenerator.class, exampleGeneratorName, "request"); if (exampleGeneratorClass == null) { requestExampleGenerator = new DocDefaultExampleGenerator(); } else { requestExampleGenerator = exampleGeneratorClass; } } private void initResponseExampleGenerator(final String exampleGeneratorName) { IDocExampleGenerator exampleGeneratorClass = getExampleGeneratorClass(IDocExampleGenerator.class, exampleGeneratorName, "response"); if (exampleGeneratorClass == null) { responseExampleGenerator = new DocDefaultExampleGenerator(); } else { responseExampleGenerator = exampleGeneratorClass; } } private <T> T getExampleGeneratorClass(final Class<T> expectedInterface, final String exampleGeneratorName, final String exampleType) { if (StringUtils.isBlank(exampleGeneratorName)) { logger.log(Level.INFO, "No custom example generator given, using a default " + exampleType + " example generator instead."); return null; } Class<?> reqExGenClass; try { reqExGenClass = Class.forName(exampleGeneratorName); } catch (ClassNotFoundException e) { logger.log(Level.WARNING, "Cought ClassNotFoundException when tried to load the " + exampleType + " example generator class - " + exampleGeneratorName + ". Using a default generator instead."); return null; } if (!expectedInterface.isAssignableFrom(reqExGenClass)) { logger.log(Level.WARNING, "The given " + exampleType + " example generator class [" + exampleGeneratorName + "] does not implement " + expectedInterface.getName() + ". Using a default generator instead."); return null; } try { return expectedInterface.cast(reqExGenClass.newInstance()); } catch (Exception e) { logger.log(Level.WARNING, "Cought exception - " + e.getClass().getName() + " when tried to instantiate the " + exampleType + " example generator class [ " + exampleGeneratorName + "]. Using a default generator instead."); return null; } } /** * * @throws Exception . */ public void run() throws Exception { // GENERATE DOCUMENTATIONS IN DOC CLASSES ClassDoc[] classes = documentation.classes(); List<DocController> controllers = generateControllers(classes); logger.log(Level.INFO, "Generated " + controllers.size() + " controlles, creating HTML documentation using velocity template."); // TRANSLATE DOC CLASSES INTO HTML DOCUMENTATION USING VELOCITY TEMPLATE String generatedHtml = generateHtmlDocumentation(controllers); // WRITE GENERATED HTML TO A FILE FileWriter velocityfileWriter = null; try { File file = new File(docPath); File parentFile = file.getParentFile(); if (parentFile != null) { if (parentFile.mkdirs()) { logger.log(Level.FINEST, "The directory " + parentFile.getAbsolutePath() + " was created, along with all necessary parent directories."); } } logger.log(Level.INFO, "Write generated velocity to " + file.getAbsolutePath()); velocityfileWriter = new FileWriter(file); velocityfileWriter.write(generatedHtml); } finally { if (velocityfileWriter != null) { velocityfileWriter.close(); } } } /** * Creates the REST API documentation in HTML form, using the controllers' * data and the velocity template. * * @param controllers . * @return string that contains the documentation in HTML form. * @throws Exception . */ public String generateHtmlDocumentation(final List<DocController> controllers) throws Exception { logger.log(Level.INFO, "Generate velocity using template: " + velocityTemplatePath + (isUserDefineTemplatePath ? File.separator + velocityTemplateFileName + " (got template path from user)" : "(default template path)")); Properties p = new Properties(); p.setProperty("directive.set.null.allowed", "true"); if (isUserDefineTemplatePath) { p.setProperty("file.resource.loader.path", velocityTemplatePath); } else { p.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath"); p.setProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName()); } Velocity.init(p); VelocityContext ctx = new VelocityContext(); ctx.put("controllers", controllers); ctx.put("version", version); ctx.put("docCssPath", docCssPath); Writer writer = new StringWriter(); Template template = Velocity.getTemplate(velocityTemplateFileName); template.merge(ctx, writer); return writer.toString(); } private static List<DocController> generateControllers(final ClassDoc[] classes) throws Exception { List<DocController> controllersList = new LinkedList<DocController>(); for (ClassDoc classDoc : classes) { List<DocController> controllers = generateControllers(classDoc); if (controllers == null || controllers.isEmpty()) { continue; } controllersList.addAll(controllers); } return controllersList; } private static List<DocController> generateControllers(final ClassDoc classDoc) throws Exception { List<DocController> controllers = new LinkedList<DocController>(); List<DocAnnotation> annotations = generateAnnotations(classDoc.annotations()); if (Utils.filterOutControllerClass(classDoc, annotations)) { return null; } String controllerClassName = classDoc.typeName(); DocRequestMappingAnnotation requestMappingAnnotation = Utils.getRequestMappingAnnotation(annotations); if (requestMappingAnnotation == null) { throw new IllegalArgumentException( "controller class " + controllerClassName + " is missing request mapping annotation"); } String[] uriArray = requestMappingAnnotation.getValue(); if (uriArray == null || uriArray.length == 0) { throw new IllegalArgumentException("controller class " + controllerClassName + " is missing request mapping annotation's value (uri)."); } for (String uri : uriArray) { DocController controller = new DocController(controllerClassName); SortedMap<String, DocMethod> generatedMethods = generateMethods(classDoc.methods()); if (generatedMethods.isEmpty()) { throw new IllegalArgumentException( "controller class " + controller.getName() + " doesn't have methods."); } controller.setMethods(generatedMethods); controller.setUri(uri); controller.setDescription(classDoc.commentText()); controllers.add(controller); } return controllers; } private static List<DocAnnotation> generateAnnotations(final AnnotationDesc[] annotations) { List<DocAnnotation> docAnnotations = new LinkedList<DocAnnotation>(); for (AnnotationDesc annotationDesc : annotations) { docAnnotations.add(Utils.createNewAnnotation(annotationDesc)); } return docAnnotations; } private static SortedMap<String, DocMethod> generateMethods(final MethodDoc[] methods) throws Exception { SortedMap<String, DocMethod> docMethods = new TreeMap<String, DocMethod>(); for (MethodDoc methodDoc : methods) { List<DocAnnotation> annotations = generateAnnotations(methodDoc.annotations()); // Does not handle methods without a RequestMapping annotation. if (Utils.filterOutMethod(methodDoc, annotations)) { continue; } // get all HTTP methods DocRequestMappingAnnotation requestMappingAnnotation = Utils.getRequestMappingAnnotation(annotations); String[] methodArray = requestMappingAnnotation.getMethod(); DocHttpMethod[] docHttpMethodArray = new DocHttpMethod[methodArray.length]; for (int i = 0; i < methodArray.length; i++) { docHttpMethodArray[i] = generateHttpMethod(methodDoc, methodArray[i], annotations); } // get all URIs String[] uriArray = requestMappingAnnotation.getValue(); if (uriArray == null || uriArray.length == 0) { uriArray = new String[1]; uriArray[0] = ""; } for (String uri : uriArray) { DocMethod docMethod = docMethods.get(uri); // If method with that uri already exist, // add the current httpMethod to the existing method. // There can be several httpMethods (GET, POST, DELETE) for each // uri. if (docMethod != null) { docMethod.addHttpMethods(docHttpMethodArray); } else { docMethod = new DocMethod(docHttpMethodArray); docMethod.setUri(uri); } docMethods.put(uri, docMethod); } } return docMethods; } private static DocHttpMethod generateHttpMethod(final MethodDoc methodDoc, final String httpMethodName, final List<DocAnnotation> annotations) throws Exception { DocHttpMethod httpMethod = new DocHttpMethod(methodDoc.name(), httpMethodName); httpMethod.setDescription(methodDoc.commentText()); httpMethod.setParams(generateParameters(methodDoc)); httpMethod.setReturnDetails(generateReturnDetails(methodDoc)); generateExamples(httpMethod, annotations); httpMethod.setPossibleResponseStatuses(Utils.getPossibleResponseStatusesAnnotation(annotations)); if (StringUtils.isBlank(httpMethod.getHttpMethodName())) { throw new IllegalArgumentException("method " + methodDoc.name() + " is missing request mapping annotation's method (http method)."); } return httpMethod; } private static void generateExamples(final DocHttpMethod httpMethod, final List<DocAnnotation> annotations) throws Exception { DocJsonResponseExample jsonResponseExampleAnnotation = Utils.getJsonResponseExampleAnnotation(annotations); DocJsonRequestExample jsonRequestExampleAnnotation = Utils.getJsonRequestExampleAnnotation(annotations); String requestExample; if (jsonRequestExampleAnnotation != null) { httpMethod.setJsonRequesteExample(jsonRequestExampleAnnotation); requestExample = jsonRequestExampleAnnotation.generateJsonRequestBody(); } else { requestExample = generateRequestExmple(httpMethod); } httpMethod.setRequestExample(requestExample); String responseExample; if (jsonResponseExampleAnnotation != null) { httpMethod.setJsonResponseExample(jsonResponseExampleAnnotation); responseExample = jsonResponseExampleAnnotation.generateJsonResponseBody(); } else { responseExample = generateResponseExample(httpMethod); } httpMethod.setResponseExample(responseExample); } private static String generateRequestExmple(final DocHttpMethod httpMethod) { List<DocParameter> params = httpMethod.getParams(); Type type = null; for (DocParameter docParameter : params) { if (requestBodyParamFilter.filter(httpMethod, docParameter)) { type = docParameter.getType(); break; } } if (type == null) { return REQUEST_HAS_NO_BODY_MSG; } String generateExample = null; try { generateExample = requestExampleGenerator.generateExample(type); generateExample = Utils.getIndentJson(generateExample); } catch (Exception e) { logger.warning("Could not generate request example for method: " + httpMethod.getMethodSignatureName() + " with the request parameter type " + type.qualifiedTypeName() + ". Exception was: " + e); generateExample = RestDocConstants.FAILED_TO_CREATE_REQUEST_EXAMPLE + "." + LINE_SEPARATOR + "Parameter type: " + type.qualifiedTypeName() + "." + LINE_SEPARATOR + "The exception caught was " + e; } return generateExample; } private static String generateResponseExample(final DocHttpMethod httpMethod) { Type returnType = httpMethod.getReturnDetails().getReturnType(); String typeName = returnType.qualifiedTypeName(); if (typeName.equals(void.class.getName())) { return RESPONSE_HAS_NO_BODY_MSG; } String generateExample = null; try { generateExample = responseExampleGenerator.generateExample(returnType); generateExample = Utils.getIndentJson(generateExample); } catch (Exception e) { logger.warning("Could not generate response example for method: " + httpMethod.getMethodSignatureName() + " with the return value type [" + typeName + "]. Exception was: " + e); generateExample = RestDocConstants.FAILED_TO_CREATE_RESPONSE_EXAMPLE + LINE_SEPARATOR + "Return value type: " + typeName + "." + LINE_SEPARATOR + "The exception caught was " + e; } return generateExample; } private static List<DocParameter> generateParameters(final MethodDoc methodDoc) { List<DocParameter> paramsList = new LinkedList<DocParameter>(); for (Parameter parameter : methodDoc.parameters()) { String name = parameter.name(); DocParameter docParameter = new DocParameter(name, parameter.type()); docParameter.setAnnotations(generateAnnotations(parameter.annotations())); Map<String, String> paramTagsComments = Utils.getParamTagsComments(methodDoc); String description = paramTagsComments.get(name); if (description == null) { logger.warning("Missing description of parameter " + name + " of method " + methodDoc.name()); description = ""; } docParameter.setDescription(description); paramsList.add(docParameter); } return paramsList; } private static DocReturnDetails generateReturnDetails(final MethodDoc methodDoc) { DocReturnDetails returnDetails = new DocReturnDetails(methodDoc.returnType()); Tag[] returnTags = methodDoc.tags("return"); if (returnTags.length > 0) { returnDetails.setDescription(returnTags[0].text()); } return returnDetails; } }