/*
* Copyright (c) 2015 EMC Corporation
* All Rights Reserved
*/
package com.emc.apidocs;
import com.emc.apidocs.differencing.DifferenceEngine;
import com.emc.apidocs.model.ApiDifferences;
import com.emc.apidocs.model.ApiErrorCode;
import com.emc.apidocs.model.ApiMethod;
import com.emc.apidocs.model.ApiService;
import com.emc.apidocs.processing.*;
import com.emc.apidocs.generating.*;
import com.emc.apidocs.tools.MetaData;
import com.emc.storageos.svcs.errorhandling.resources.ServiceCode;
import com.google.common.collect.Lists;
import com.sun.javadoc.*;
import org.apache.commons.io.IOUtils;
import java.io.*;
import java.util.*;
/**
* Doclet to process the ViPR API annotations and comments
*/
public class ApiDoclet {
private static final String PAGE_TITLE_PROPERTY = "title:";
private static final String OUTPUT_OPTION = "-d";
private static final String CONTENT_OPTION = "-c";
private static final String BUILD_OPTION = "-build";
private static final String PORTAL_SRC_OPTION = "-portalsrc";
private static final String ROOT_DIRECTORY = "-rootDirectory";
private static final String INTERNAL_PATH = "internal";
private static final List<String> DATASERVICES_CLASSES = Lists.newArrayList("S3Service", "AtmosService", "SwiftService");
private static final String SYSTEM_SERVIES_PACKAGE = "com.emc.storageos.systemservices";
private static String buildNumber = null;
private static String rootDirectory = null;
private static String portalSource = null;
private static String outputDirectory;
private static String contentDirectory;
private static List<String> serviceBlackList = Lists.newArrayList();
private static List<String> methodBlackList = Lists.newArrayList();
/** MAIN Entry Point into the Doclet */
public static boolean start(RootDoc root) {
KnownPaths.init(contentDirectory, outputDirectory);
init();
loadServiceBlackList();
loadMethodBlackList();
List<ApiService> apiServices = findApiServices(root.classes());
List<ApiErrorCode> errorCodes = findErrorCodes(root.classes());
cleanupMethods(apiServices);
saveMetaData(apiServices);
ApiDifferences apiDifferences = calculateDifferences(apiServices);
generateFiles(apiDifferences, apiServices, errorCodes);
return true;
}
/** Required by Doclet, otherwise it does not process Generics correctly */
public static LanguageVersion languageVersion() {
return LanguageVersion.JAVA_1_5;
}
/** Required by Doclet to check command line options */
public static int optionLength(String option) {
if (option.equals(OUTPUT_OPTION)) {
return 2;
}
if (option.equals(CONTENT_OPTION)) {
return 2;
}
if (option.equals(BUILD_OPTION)) {
return 2;
}
if (option.equals(PORTAL_SRC_OPTION)) {
return 2;
}
if (option.equals(ROOT_DIRECTORY)) {
return 2;
}
return 1;
}
/** Required by Doclet to process the command line options */
public static synchronized boolean validOptions(String options[][],
DocErrorReporter reporter) {
DocReporter.init(reporter);
DocReporter.printWarning("Processing Options");
boolean valid = true;
boolean contentOptionFound = false;
boolean outputOptionFound = false;
boolean portalsrcOptionFound = false;
// Make sure we have an OUTPUT and TEMPLATES option
for (int i = 0; i < options.length; i++) {
if (options[i][0].equals(OUTPUT_OPTION)) {
outputOptionFound = true;
valid = checkOutputOption(options[i][1], reporter);
}
else if (options[i][0].equals(CONTENT_OPTION)) {
contentOptionFound = true;
valid = checkContentOption(options[i][1], reporter);
} else if (options[i][0].equals(PORTAL_SRC_OPTION)) {
portalsrcOptionFound = true;
valid = checkPortalSourceOption(options[i][1], reporter);
} else if (options[i][0].equals(ROOT_DIRECTORY)) {
rootDirectory = options[i][1];
reporter.printWarning(rootDirectory);
} else if (options[i][0].equals(BUILD_OPTION)) {
buildNumber = options[i][1];
reporter.printWarning("Build " + buildNumber);
}
}
if (!contentOptionFound) {
reporter.printError("Content dir option " + CONTENT_OPTION + " not specified");
}
if (!outputOptionFound) {
reporter.printError("Output dir option " + OUTPUT_OPTION + " not specified");
}
if (!portalsrcOptionFound) {
reporter.printError("Portal Source option " + PORTAL_SRC_OPTION + " not specified");
}
DocReporter.printWarning("Finished Processing Options");
return valid && contentOptionFound && outputOptionFound && portalsrcOptionFound;
}
/** Processes the list of classes looking for ones that represent an API Service, and parsing them if found */
private static synchronized List<ApiService> findApiServices(ClassDoc[] classes) {
List<ApiService> apiServices = new ArrayList<ApiService>();
for (ClassDoc classDoc : classes) {
if (DATASERVICES_CLASSES.contains(classDoc.name())) {
if (!classDoc.name().equals("AtmosService")) {
// Data Service service, so treat it slightly differently since it's actually split over operation classes
String baseURL = AnnotationUtils.getAnnotationValue(classDoc, KnownAnnotations.Path_Annotation,
KnownAnnotations.Value_Element, "");
for (ClassDoc operationClassDoc : findDataServiceOperations(classDoc)) {
apiServices.add(processClass(operationClassDoc, baseURL, true));
}
}
}
else if (AnnotationUtils.hasAnnotation(classDoc, KnownAnnotations.Path_Annotation) &&
!serviceBlackList.contains(classDoc.qualifiedName()) &&
!serviceBlackList.contains(classDoc.name())) {
String baseURL = AnnotationUtils.getAnnotationValue(classDoc, KnownAnnotations.Path_Annotation,
KnownAnnotations.Value_Element, "");
if (!isInternalPath(baseURL)) {
apiServices.add(processClass(classDoc, baseURL, false));
}
}
}
// Add All Services from the Portal API
apiServices.addAll(PlayRoutesParser.getPortalServices(portalSource));
return apiServices;
}
private static synchronized List<ApiErrorCode> findErrorCodes(ClassDoc[] classes) {
// Find ServiceCode Class
ClassDoc serviceCodeClass = null;
for (ClassDoc classDoc : classes) {
if (classDoc.qualifiedName().equals(ServiceCode.class.getCanonicalName())) {
serviceCodeClass = classDoc;
break;
}
}
if (serviceCodeClass == null) {
throw new RuntimeException("Unable to find ServiceCode Class");
}
// Extract ServiceCode information
List<ApiErrorCode> errorCodes = Lists.newArrayList();
for (FieldDoc field : serviceCodeClass.enumConstants()) {
ApiErrorCode errorCode = new ApiErrorCode(ServiceCode.valueOf(field.name()));
if (AnnotationUtils.hasAnnotation(field, KnownAnnotations.Deprecated_Annotation)) {
errorCode.setDeprecated(true);
}
errorCodes.add(errorCode);
}
Collections.sort(errorCodes, new Comparator<ApiErrorCode>() {
@Override
public int compare(ApiErrorCode o1, ApiErrorCode o2) {
return Integer.valueOf(o1.getCode()).compareTo(o2.getCode());
}
});
return errorCodes;
}
private static synchronized void cleanupMethods(List<ApiService> apiServices) {
// Cleanup
for (ApiService apiService : apiServices) {
for (ApiMethod apiMethod : apiService.methods) {
TemporaryCleanup.applyCleanups(apiMethod);
}
}
applyServiceTitleChanges(apiServices);
}
private static synchronized void saveMetaData(List<ApiService> apiServices) {
MetaData.save(KnownPaths.getOutputFile("meta_data.json"), apiServices);
}
private static synchronized void generateFiles(ApiDifferences apiDifferences, List<ApiService> apiServices, List<ApiErrorCode> errorCodes) {
PageGenerator pageGenerator = new PageGenerator(buildNumber);
pageGenerator.generatePages(apiDifferences, apiServices, errorCodes);
}
private static synchronized ApiDifferences calculateDifferences(List<ApiService> apiServices) {
Properties prop = new Properties();
try {
FileInputStream fileInput = new FileInputStream(rootDirectory+"gradle.properties");
prop.load(fileInput);
}
catch (IOException e) {
throw new RuntimeException("Unable to load Gradle properties file", e);
}
String docsMetaVersion = prop.getProperty("apidocsComparisionVersion");
List<ApiService> oldServices = MetaData.load(KnownPaths.getMetaDataFile("MetaData-"+docsMetaVersion+".json"));
DifferenceEngine differenceEngine = new DifferenceEngine();
return differenceEngine.calculateDifferences(oldServices, apiServices);
}
/** Process a JAXRS Class into an API Service */
public static synchronized ApiService processClass(ClassDoc classDoc, String baseUrl, boolean isDataService) {
ApiService apiService = new ApiService();
apiService.packageName = classDoc.containingPackage().name();
apiService.javaClassName = classDoc.name();
apiService.description = classDoc.commentText();
apiService.path = baseUrl;
addDefaultPermissions(classDoc, apiService);
addDeprecated(classDoc, apiService);
TemporaryCleanup.applyCleanups(apiService);
// Process ALL methods on EMC classes, including super classes
List<String> methodsAdded = Lists.newArrayList();
ClassDoc currentClass = classDoc;
while (currentClass != null && currentClass.containingPackage().name().startsWith("com.emc")) {
for (MethodDoc method : currentClass.methods()) {
if (isApiMethod(method) &&
!isInternalMethod(method) &&
!methodBlackList.contains(apiService.getFqJavaClassName() + "::" + method.name()) &&
!methodBlackList.contains(apiService.javaClassName + "::" + method.name())) {
ApiMethod apiMethod = MethodProcessor.processMethod(apiService, method, apiService.path, isDataService);
// Some methods are marked internal via brief comments, but we only know that after processing it
if (!apiMethod.brief.toLowerCase().startsWith("internal")) {
apiService.addMethod(apiMethod);
}
}
methodsAdded.add(method.name());
}
currentClass = currentClass.superclass();
}
return apiService;
}
public static synchronized void addDefaultPermissions(ClassDoc classDoc, ApiService apiService) {
AnnotationDesc defaultPermissions = AnnotationUtils.getAnnotation(classDoc, KnownAnnotations.DefaultPermissions_Annotation);
if (defaultPermissions != null) {
for (AnnotationDesc.ElementValuePair pair : defaultPermissions.elementValues()) {
if (pair.element().name().equals("readRoles")) {
for (AnnotationValue value : (AnnotationValue[]) pair.value().value()) {
apiService.addReadRole(((FieldDoc) value.value()).name());
}
}
else if (pair.element().name().equals("writeRoles")) {
for (AnnotationValue value : (AnnotationValue[]) pair.value().value()) {
apiService.addWriteRole(((FieldDoc) value.value()).name());
}
}
else if (pair.element().name().equals("readAcls")) {
for (AnnotationValue value : (AnnotationValue[]) pair.value().value()) {
apiService.addReadAcl(((FieldDoc) value.value()).name());
}
}
else if (pair.element().name().equals("writeAcls")) {
for (AnnotationValue value : (AnnotationValue[]) pair.value().value()) {
apiService.addWriteAcl(((FieldDoc) value.value()).name());
}
}
}
}
}
private static synchronized List<ClassDoc> findDataServiceOperations(ClassDoc dataService) {
List<ClassDoc> operations = new ArrayList<ClassDoc>();
for (FieldDoc field : dataService.fields(false)) {
if (field.name().endsWith("Operations") || field.name().endsWith("Operation")) {
operations.add(field.type().asClassDoc());
}
}
return operations;
}
public static boolean isApiMethod(MethodDoc method) {
return AnnotationUtils.hasAnnotation(method, "javax.ws.rs.POST") ||
AnnotationUtils.hasAnnotation(method, "javax.ws.rs.GET") ||
AnnotationUtils.hasAnnotation(method, "javax.ws.rs.PUT") ||
AnnotationUtils.hasAnnotation(method, "javax.ws.rs.DELETE");
}
public static boolean isInternalMethod(MethodDoc method) {
return isInternalPath(AnnotationUtils.getAnnotationValue(method, KnownAnnotations.Path_Annotation, KnownAnnotations.Value_Element,
""));
}
public static boolean isInternalPath(String path) {
return path.startsWith("/" + INTERNAL_PATH) || path.startsWith(INTERNAL_PATH + "/");
}
/**
* Allows users to change titles of services, rather than using the default JavaClassName
*/
private static synchronized void applyServiceTitleChanges(List<ApiService> services) {
Properties titleChanges = new Properties();
try {
titleChanges.load(new FileInputStream(KnownPaths.getReferenceFile("ServiceTitleChanges.txt")));
for (ApiService service : services) {
if (titleChanges.containsKey(service.getFqJavaClassName())) {
service.titleOverride = titleChanges.get(service.getFqJavaClassName()).toString();
} else if (titleChanges.containsKey(service.javaClassName)) {
service.titleOverride = titleChanges.get(service.javaClassName).toString();
}
}
} catch (IOException e) {
throw new RuntimeException("Unable to load Title Changes file", e);
}
}
private static synchronized boolean checkOutputOption(String value, DocErrorReporter reporter) {
File file = new File(value);
if (!file.exists()) {
reporter.printError("Output directory (" + OUTPUT_OPTION + ") not found :" + file.getAbsolutePath());
return false;
}
if (!file.isDirectory()) {
reporter.printError("Output directory (" + OUTPUT_OPTION + ") is not a directory :" + file.getAbsolutePath());
return false;
}
outputDirectory = value;
if (!outputDirectory.endsWith("/")) {
outputDirectory = outputDirectory + "/";
}
reporter.printWarning("Output Directory " + outputDirectory);
return true;
}
private static synchronized boolean checkPortalSourceOption(String value, DocErrorReporter reporter) {
File file = new File(value);
if (!file.exists()) {
reporter.printError("Portal Source directory (" + PORTAL_SRC_OPTION + ") not found :" + file.getAbsolutePath());
return false;
}
portalSource = value;
reporter.printWarning("Portal Source Directory " + portalSource);
return true;
}
private static synchronized boolean checkContentOption(String contentDir, DocErrorReporter reporter) {
File contentDirFile = new File(contentDir);
if (!contentDirFile.exists()) {
reporter.printError("Content directory (" + CONTENT_OPTION + ") not found :" + contentDirFile.getAbsolutePath());
return false;
}
if (!contentDirFile.isDirectory()) {
reporter.printError("Content directory (" + CONTENT_OPTION + ") is not a directory :" + contentDirFile.getAbsolutePath());
return false;
}
contentDirectory = contentDir;
if (!contentDirectory.endsWith("/")) {
contentDirectory = contentDirectory + "/";
}
reporter.printWarning("Content Directory " + contentDirectory);
return true;
}
private static synchronized void init() {
KnownPaths.getHTMLDir().mkdirs();
}
private static synchronized void loadServiceBlackList() {
try {
serviceBlackList = IOUtils.readLines(new FileInputStream(KnownPaths.getReferenceFile("ServiceBlacklist.txt")));
} catch (IOException e) {
throw new RuntimeException("Unable to load Service blacklist", e);
}
}
private static synchronized void loadMethodBlackList() {
try {
methodBlackList = IOUtils.readLines(new FileInputStream(KnownPaths.getReferenceFile("MethodBlackList.txt")));
} catch (IOException e) {
throw new RuntimeException("Unable to load Method blacklist", e);
}
}
public static synchronized void addDeprecated(ClassDoc method, ApiService apiService) {
if (AnnotationUtils.hasAnnotation(method, KnownAnnotations.Deprecated_Annotation)) {
apiService.isDeprecated = true;
Tag[] deprecatedTags = method.tags("@deprecated");
if (deprecatedTags.length > 0) {
apiService.deprecatedMessage = deprecatedTags[0].text();
}
}
}
}