/*
* Copyright 2011 TaskDock, Inc.
*
* 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.versly.rest.wsdoc;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.internal.Lists;
import freemarker.template.Configuration;
import freemarker.template.DefaultObjectWrapper;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import org.versly.rest.wsdoc.impl.RestDocumentation;
import org.versly.rest.wsdoc.impl.Utils;
import java.io.*;
import java.util.*;
import java.util.jar.JarFile;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
public class RestDocAssembler {
private final String _outputFileName;
private final String _outputTemplate;
public static void main(String... args)
throws IOException, ClassNotFoundException, TemplateException {
Arguments arguments = new Arguments();
new JCommander(arguments, args);
Utils.addTemplateValue(DocumentationRestApi.MOUNT_TEMPLATE, arguments.mountTemplateValue);
Utils.addTemplateValue(DocumentationRestApi.ID_TEMPLATE, arguments.idTemplateValue);
Utils.addTemplateValue(DocumentationRestApi.TITLE_TEMPLATE, arguments.titleTemplateValue);
Utils.addTemplateValue(DocumentationRestApi.VERSION_TEMPLATE, arguments.versionTemplateValue);
List<RestDocumentation> docs = new LinkedList<RestDocumentation>();
for (String input : arguments.inputs) {
File inputFile = new File(input);
if (inputFile.isDirectory()) {
System.err.println("adding web service docs from classes directory " + input);
File resourceFile = new File(inputFile, Utils.SERIALIZED_RESOURCE_LOCATION);
docs.add(RestDocumentation.fromStream(new FileInputStream(resourceFile))); // TODO resource management
} else if (input.toLowerCase().endsWith(".war")) {
System.err.println("adding web service docs from WAR " + input);
JarFile jar = new JarFile(input);
ZipEntry e = jar.getEntry("WEB-INF/classes/" + Utils.SERIALIZED_RESOURCE_LOCATION);
docs.add(RestDocumentation.fromStream(jar.getInputStream(e)));
jar.close();
} else {
System.err.println("adding web service docs from serialized input " + input);
docs.add(RestDocumentation.fromStream(new FileInputStream(inputFile))); // TODO resource management
}
}
if (docs.size() > 0) {
List<Pattern> excludePatterns = new ArrayList<Pattern>();
for (String pattern : arguments.excludes)
excludePatterns.add(Pattern.compile(pattern));
new RestDocAssembler(arguments.outputFileName, arguments.outputFormat)
.writeDocumentation(docs, excludePatterns, arguments.scope);
}
}
public RestDocAssembler(String outputFileName, String outputFormat) {
_outputFileName = outputFileName;
if (outputFormat.equalsIgnoreCase("raml"))
{
_outputTemplate = "RamlDocumentation.ftl";
}
else
{
_outputTemplate = "RestDocumentation.ftl";
}
}
public RestDocAssembler(String outputFileName) {
this(outputFileName, "html");
}
/**
* combine APIs from the REST docs into one map, merging those APIs with matching identifiers
*/
private Collection<RestDocumentation.RestApi> mergeApis(List<RestDocumentation> docs) {
Map<String,RestDocumentation.RestApi> aggregatedApis = new LinkedHashMap<String,RestDocumentation.RestApi>();
for (RestDocumentation doc : docs) {
for (RestDocumentation.RestApi api : doc.getApis()) {
if (!aggregatedApis.containsKey(api.getIdentifier())) {
aggregatedApis.put(api.getIdentifier(), api);
}
else {
aggregatedApis.get(api.getIdentifier()).merge(api);
}
}
}
return aggregatedApis.values();
}
/**
* Filter out APIs based on user provided exclude patterns and selected scope (scope of 'all' implies no filtering).
*/
private Collection<RestDocumentation.RestApi> filterApis(
Collection<RestDocumentation.RestApi> apis, Iterable<Pattern> excludePatterns, String scope) {
// filter doc objects by client provided exclude patterns
Collection<RestDocumentation.RestApi> filteredApis = null;
if (excludePatterns != null) {
filteredApis = new LinkedList<RestDocumentation.RestApi>();
for (RestDocumentation.RestApi api : apis)
filteredApis.add(api.filter(excludePatterns));
} else {
filteredApis = apis;
}
// use command-line --scope value to filter the generated documentation
if (!scope.equals("all")) {
HashSet<String> requestedScopes = new HashSet<String>(Arrays.asList(new String[]{scope}));
// ugly old-style iterating because we need to be able to remove elements as we go
Iterator<RestDocumentation.RestApi> apiIter = filteredApis.iterator();
while (apiIter.hasNext()) {
RestDocumentation.RestApi api = apiIter.next();
Iterator<RestDocumentation.RestApi.Resource> resIter = api.getResources().iterator();
while (resIter.hasNext()) {
RestDocumentation.RestApi.Resource resource = resIter.next();
Iterator<RestDocumentation.RestApi.Resource.Method> methIter = resource.getRequestMethodDocs().iterator();
while (methIter.hasNext()) {
HashSet<String> scopes = methIter.next().getDocScopes();
scopes.retainAll(requestedScopes);
if (scopes.isEmpty()) {
methIter.remove();
}
}
if (resource.getRequestMethodDocs().isEmpty()) {
resIter.remove();
}
}
if (api.getResources().isEmpty()) {
apiIter.remove();
}
}
}
return filteredApis;
}
/**
* derive the common base URI for all resources in each API and declare that the API mount point.
*/
private void deriveBaseURIs(Collection<RestDocumentation.RestApi> apis) {
for (RestDocumentation.RestApi api : apis) {
String basePath = null;
for (RestDocumentation.RestApi.Resource resource : api.getResources()) {
String resourcePath = resource.getPath();
if (null != resourcePath) {
if (null == basePath) {
basePath = resourcePath;
} else {
String[] baseParts = basePath.split("/");
String[] resourceParts = resourcePath.split("/");
basePath = "";
int smallerLength = Math.min(baseParts.length, resourceParts.length);
for (int i = 0; i < smallerLength && baseParts[i].equals(resourceParts[i]); ++i) {
if (baseParts[i].length() > 0) {
basePath += "/" + baseParts[i];
}
}
}
}
}
api.setMount(basePath);
}
}
List<String> writeDocumentation(List<RestDocumentation> docs, Iterable<Pattern> excludePatterns, String scope)
throws IOException, ClassNotFoundException, TemplateException {
List<String> filesWritten = new ArrayList<String>();
// combine APIs from the REST docs into one map, merging those with matching identifiers
Collection<RestDocumentation.RestApi> apis = mergeApis(docs);
// filter out APIs based on exclude patterns and selected publishing scope
apis = filterApis(apis, excludePatterns, scope);
// derive the common base URI for all resources of each API and declare that the API mount
deriveBaseURIs(apis);
Configuration conf = new Configuration();
conf.setClassForTemplateLoading(RestDocAssembler.class, "");
conf.setObjectWrapper(new DefaultObjectWrapper());
Writer out = null;
try {
for (RestDocumentation.RestApi api : apis) {
Template template = conf.getTemplate(_outputTemplate);
Map<String, RestDocumentation.RestApi> root = new HashMap<String, RestDocumentation.RestApi>();
root.put("api", api);
String fileName = getOutputFileName(api);
filesWritten.add(fileName);
File file = new File(fileName);
out = new FileWriter(file);
template.process(root, out);
out.flush();
System.err.printf("Wrote REST docs to %s\n", file.getAbsolutePath());
}
} finally {
if (out != null) {
try {
out.close();
} catch (IOException ignored) {
// ignored
}
}
}
return filesWritten;
}
String getOutputFileName(RestDocumentation.RestApi api) {
String outputFileName = _outputFileName;
if (!api.getIdentifier().equals(RestDocumentation.RestApi.DEFAULT_IDENTIFIER)) {
StringBuilder constructedName = new StringBuilder(_outputFileName);
int identifierIndex = constructedName.lastIndexOf(".");
if (identifierIndex < 0) {
identifierIndex = constructedName.length();
}
constructedName.insert(identifierIndex, "-" + api.getIdentifier());
outputFileName = constructedName.toString();
}
return outputFileName;
}
static class Arguments {
@Parameter
List<String> inputs = Lists.newArrayList();
@Parameter(names = { "-o", "--out" }, description = "File to write HTML documentation to")
String outputFileName = "web-service-api.html";
@Parameter(names = { "--exclude" }, description = "Endpoint pattern to exclude from the generated docs")
List<String> excludes = Lists.newArrayList();
@Parameter(names = { "-f", "--format" }, description = "Format for output: html or raml")
String outputFormat = "html";
@Parameter(names = { "-s", "--scope" }, description = "Publication scope for output (e.g. public, private, etc) or \"all\"")
String scope = "all";
@Parameter(names = { "--template-mount" }, description = "Mount point to use when filling templates.")
String mountTemplateValue = "";
@Parameter(names = { "--template-id" }, description = "Id to use when filling templates.")
String idTemplateValue = "";
@Parameter(names = { "--template-title" }, description = "Title to use when filling templates.")
String titleTemplateValue = "";
@Parameter(names = { "--template-version" }, description = "Version to use when filling templates.")
String versionTemplateValue = "";
}
}