/**
* 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.docs;
import com.webcohesion.enunciate.EnunciateContext;
import com.webcohesion.enunciate.EnunciateException;
import com.webcohesion.enunciate.api.*;
import com.webcohesion.enunciate.api.datatype.Namespace;
import com.webcohesion.enunciate.api.datatype.Syntax;
import com.webcohesion.enunciate.api.resources.ResourceApi;
import com.webcohesion.enunciate.api.services.ServiceApi;
import com.webcohesion.enunciate.api.services.ServiceGroup;
import com.webcohesion.enunciate.artifacts.Artifact;
import com.webcohesion.enunciate.artifacts.ClientLibraryArtifact;
import com.webcohesion.enunciate.artifacts.ClientLibraryJavaArtifact;
import com.webcohesion.enunciate.artifacts.FileArtifact;
import com.webcohesion.enunciate.module.*;
import com.webcohesion.enunciate.util.freemarker.FileDirective;
import freemarker.cache.URLTemplateLoader;
import freemarker.core.Environment;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.TemplateExceptionHandler;
import org.apache.commons.configuration.HierarchicalConfiguration;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.*;
public class DocsModule extends BasicGeneratingModule implements ApiRegistryAwareModule, DocumentationProviderModule {
private File defaultDocsDir;
private String defaultDocsSubdir;
private ApiRegistry apiRegistry;
/**
* @return "docs"
*/
@Override
public String getName() {
return "docs";
}
@Override
public List<DependencySpec> getDependencySpecifications() {
//documentation depends on any module that provides something to the api registry.
return Arrays.asList((DependencySpec) new DependencySpec() {
@Override
public boolean accept(EnunciateModule module) {
return module instanceof ApiFeatureProviderModule;
}
@Override
public boolean isFulfilled() {
return true;
}
@Override
public String toString() {
return "all api feature provider modules";
}
});
}
/**
* The configured list of downloads to add to the documentation.
*
* @return The configured list of downloads to add to the documentation.
*/
public Collection<ExplicitDownloadConfig> getExplicitDownloads() {
List<HierarchicalConfiguration> downloads = this.config.configurationsAt("download");
ArrayList<ExplicitDownloadConfig> downloadConfigs = new ArrayList<ExplicitDownloadConfig>(downloads.size());
for (HierarchicalConfiguration download : downloads) {
ExplicitDownloadConfig downloadConfig = new ExplicitDownloadConfig();
downloadConfig.setArtifact(download.getString("[@artifact]"));
downloadConfig.setDescription(download.getString("[@description]"));
downloadConfig.setFile(download.getString("[@file]"));
downloadConfig.setName(download.getString("[@name]"));
downloadConfig.setShowLink(download.getString("[@showLink]"));
downloadConfigs.add(downloadConfig);
}
return downloadConfigs;
}
/**
* The additional css files.
*
* @return The additional css files.
*/
public List<String> getAdditionalCss() {
LinkedList<String> additionalCss = new LinkedList<String>();
List<HierarchicalConfiguration> additionalCsses = this.config.configurationsAt("additional-css");
for (HierarchicalConfiguration additional : additionalCsses) {
String file = additional.getString("[@file]");
if (file != null) {
additionalCss.add(file);
}
}
return additionalCss;
}
/**
* The url to the freemarker XML processing template that will be used to transforms the docs.xml to the site documentation. For more
* information, see http://freemarker.sourceforge.net/docs/xgui.html
*
* @return The url to the freemarker XML processing template.
*/
public File getFreemarkerTemplateFile() {
String templatePath = this.config.getString("[@freemarkerTemplate]");
return templatePath == null ? null : resolveFile(templatePath);
}
/**
* The URL to the Freemarker template for processing the base documentation xml file.
*
* @return The URL to the Freemarker template for processing the base documentation xml file.
*/
protected URL getDocsTemplateURL() throws MalformedURLException {
File templateFile = getFreemarkerTemplateFile();
if (templateFile != null && !templateFile.exists()) {
warn("Unable to use freemarker template at %s: file doesn't exist!", templateFile);
templateFile = null;
}
if (templateFile != null) {
return templateFile.toURI().toURL();
}
else {
return DocsModule.class.getResource("docs.fmt");
}
}
/**
* The cascading stylesheet to use instead of the default. This is ignored if the 'base' is also set.
*
* @return The cascading stylesheet to use.
*/
public String getCss() {
return this.config.getString("[@css]");
}
/**
* The documentation "base". The documentation base is the initial contents of the directory
* where the documentation will be output. Can be a zip file or a directory.
*
* @return The documentation "base".
*/
public File getBase() {
String base = this.config.getString("[@base]");
return base == null ? null : resolveFile(base);
}
/**
* The subdirectory in the web application where the documentation will be put.
*
* @return The subdirectory in the web application where the documentation will be put.
*/
public File getDocsDir() {
String docsDir = this.config.getString("[@docsDir]");
return docsDir != null ? resolveFile(docsDir) : this.defaultDocsDir != null ? this.defaultDocsDir : new File(this.enunciate.getBuildDir(), getName());
}
public String getDocsSubdir() {
return this.config.getString("[@docsSubdir]", this.defaultDocsSubdir);
}
public boolean isDisableResourceLinks() {
return this.config.getBoolean("[@disableResourceLinks]", false);
}
@Override
public void setDefaultDocsDir(File docsDir) {
this.defaultDocsDir = docsDir;
}
@Override
public void setDefaultDocsSubdir(String defaultDocsSubdir) {
this.defaultDocsSubdir = defaultDocsSubdir;
}
/**
* The name of the index page.
*
* @return The name of the index page.
*/
public String getIndexPageName() {
return this.config.getString("[@indexPageName]", "index.html");
}
/**
* Whether to disable the REST mountpoint documentation.
*
* @return Whether to disable the REST mountpoint documentation.
*/
public boolean isDisableRestMountpoint() {
return this.config.getBoolean("[@disableRestMountpoint]", false);
}
/**
* URI to the favicon for the generated documentation.
*
* @return URI to the favicon for the generated documentation.
*/
public String getFavicon() {
return this.config.getString("[@faviconUri]", null);
}
@Override
public void setApiRegistry(ApiRegistry registry) {
this.apiRegistry = registry;
}
@Override
public void call(EnunciateContext context) {
try {
File docsDir = getDocsDir();
String subDir = getDocsSubdir();
if (subDir != null) {
docsDir = new File(docsDir, subDir);
}
if (!isUpToDateWithSources(docsDir)) {
ApiRegistrationContext registrationContext = new ApiDocsRegistrationContext(this.apiRegistry);
List<ResourceApi> resourceApis = this.apiRegistry.getResourceApis(registrationContext);
Set<Syntax> syntaxes = this.apiRegistry.getSyntaxes(registrationContext);
List<ServiceApi> serviceApis = this.apiRegistry.getServiceApis(registrationContext);
Set<Artifact> documentationArtifacts = findDocumentationArtifacts();
if (syntaxes.isEmpty() && serviceApis.isEmpty() && resourceApis.isEmpty() && documentationArtifacts.isEmpty()) {
warn("No documentation generated: there are no data types, services, or resources to document.");
return;
}
docsDir.mkdirs();// make sure the docs dir exists.
Map<String, Object> model = new HashMap<String, Object>();
String intro = this.enunciate.getConfiguration().readDescription(context, false);
if (intro != null) {
model.put("apiDoc", intro);
}
String copyright = this.enunciate.getConfiguration().getCopyright();
if (copyright != null) {
model.put("copyright", copyright);
}
String title = this.enunciate.getConfiguration().getTitle();
model.put("title", title == null ? "Web Service API" : title);
//extract out the documentation base
String cssPath = buildBase(docsDir);
if (cssPath != null) {
model.put("cssFile", cssPath);
}
model.put("file", new FileDirective(docsDir, this.enunciate.getLogger()));
model.put("apiRelativePath", getRelativePathToRootDir());
model.put("includeApplicationPath", isIncludeApplicationPath());
model.put("favicon", getFavicon());
//iterate through schemas and make sure the schema is copied to the docs dir
for (Syntax syntax : syntaxes) {
for (Namespace namespace : syntax.getNamespaces()) {
if (namespace.getSchemaFile() != null) {
namespace.getSchemaFile().writeTo(docsDir);
}
}
}
model.put("data", syntaxes);
for (ResourceApi resourceApi : resourceApis) {
if (resourceApi.getWadlFile() != null) {
resourceApi.getWadlFile().writeTo(docsDir);
}
}
model.put("resourceApis", resourceApis);
ApiRegistrationContext swaggerRegistrationContext = new DefaultRegistrationContext();
InterfaceDescriptionFile swaggerUI = this.apiRegistry.getSwaggerUI(swaggerRegistrationContext);
if (swaggerUI != null) {
swaggerUI.writeTo(docsDir);
model.put("swaggerUI", swaggerUI);
}
//iterate through wsdls and make sure the wsdl is copied to the docs dir
for (ServiceApi serviceApi : serviceApis) {
for (ServiceGroup serviceGroup : serviceApi.getServiceGroups()) {
if (serviceGroup.getWsdlFile() != null) {
serviceGroup.getWsdlFile().writeTo(docsDir);
}
}
}
model.put("serviceApis", serviceApis);
model.put("downloads", copyDocumentationArtifacts(documentationArtifacts, docsDir));
model.put("indexPageName", getIndexPageName());
model.put("disableMountpoint", isDisableRestMountpoint());
model.put("additionalCssFiles", getAdditionalCss());
model.put("disableResourceLinks", isDisableResourceLinks());
processTemplate(getDocsTemplateURL(), model);
}
else {
info("Skipping documentation source generation as everything appears up-to-date...");
}
this.enunciate.addArtifact(new FileArtifact(getName(), "docs", docsDir));
}
catch (IOException e) {
throw new EnunciateException(e);
}
catch (TemplateException e) {
throw new EnunciateException(e);
}
}
private boolean isIncludeApplicationPath() {
return this.config.getBoolean("[@includeApplicationPath]", false);
}
/**
* Processes the specified template with the given model.
*
* @param templateURL The template URL.
* @param model The root model.
*/
public void processTemplate(URL templateURL, Object model) throws IOException, TemplateException {
debug("Processing template %s.", templateURL);
Configuration configuration = new Configuration(Configuration.VERSION_2_3_22);
configuration.setTemplateLoader(new URLTemplateLoader() {
protected URL getURL(String name) {
try {
return new URL(name);
}
catch (MalformedURLException e) {
return null;
}
}
});
configuration.setTemplateExceptionHandler(new TemplateExceptionHandler() {
public void handleTemplateException(TemplateException templateException, Environment environment, Writer writer) throws TemplateException {
throw templateException;
}
});
configuration.setLocalizedLookup(false);
configuration.setDefaultEncoding("UTF-8");
configuration.setURLEscapingCharset("UTF-8");
Template template = configuration.getTemplate(templateURL.toString());
StringWriter unhandledOutput = new StringWriter();
template.process(model, unhandledOutput);
debug("Freemarker processing output:\n%s", unhandledOutput);
}
protected String buildBase(File outputDir) throws IOException {
File baseFile = getBase();
if (baseFile == null) {
InputStream discoveredBase = DocsModule.class.getResourceAsStream("/META-INF/enunciate/docs-base.zip");
if (discoveredBase == null) {
debug("Default base to be used for documentation base.");
this.enunciate.unzip(loadDefaultBase(), outputDir);
String configuredCss = getCss();
URL discoveredCss = DocsModule.class.getResource("/META-INF/enunciate/css/style.css");
if (discoveredCss != null) {
this.enunciate.copyResource(discoveredCss, new File(new File(outputDir, "css"), "style.css"));
}
else if (configuredCss != null) {
try {
if (URI.create(configuredCss).isAbsolute()) {
return configuredCss;
}
}
catch (IllegalArgumentException e) {
//fall through...
}
this.enunciate.copyFile(resolveFile(configuredCss), new File(new File(outputDir, "css"), "style.css"));
}
return "css/style.css";
}
else {
debug("Discovered documentation base at /META-INF/enunciate/docs-base.zip");
this.enunciate.unzip(discoveredBase, outputDir);
return null;
}
}
else if (baseFile.isDirectory()) {
debug("Directory %s to be used as the documentation base.", baseFile);
this.enunciate.copyDir(baseFile, outputDir);
return null;
}
else {
debug("Zip file %s to be extracted as the documentation base.", baseFile);
this.enunciate.unzip(new FileInputStream(baseFile), outputDir);
return null;
}
}
protected List<Download> copyDocumentationArtifacts(Set<Artifact> artifacts, File outputDir) throws IOException {
ArrayList<Download> downloads = new ArrayList<Download>();
for (Artifact artifact : artifacts) {
debug("Exporting %s to directory %s.", artifact.getId(), outputDir);
artifact.exportTo(outputDir, this.enunciate);
if (artifact instanceof SpecifiedArtifact && !((SpecifiedArtifact)artifact).isShowLink()) {
continue;
}
Download download = new Download();
download.setSlug("artifact_" + artifact.getId().replace('.', '_'));
download.setName(artifact.getName());
download.setDescription(artifact.getDescription());
download.setCreated(artifact.getCreated());
if (artifact instanceof ClientLibraryJavaArtifact) {
download.setGroupId(((ClientLibraryJavaArtifact)artifact).getGroupId());
download.setArtifactId(((ClientLibraryJavaArtifact)artifact).getArtifactId());
download.setVersion(((ClientLibraryJavaArtifact)artifact).getVersion());
}
Collection<? extends Artifact> childArtifacts = (artifact instanceof ClientLibraryArtifact) ? ((ClientLibraryArtifact) artifact).getArtifacts() : (artifact instanceof SpecifiedArtifact) ? Arrays.asList(((SpecifiedArtifact) artifact).getFile()) : Arrays.asList(artifact);
ArrayList<DownloadFile> downloadFiles = new ArrayList<DownloadFile>();
for (Artifact childArtifact : childArtifacts) {
DownloadFile downloadFile = new DownloadFile();
downloadFile.setDescription(childArtifact.getDescription());
downloadFile.setName(childArtifact.getName());
downloadFile.setSize(getDisplaySize(childArtifact.getSize()));
downloadFiles.add(downloadFile);
}
download.setFiles(downloadFiles);
downloads.add(download);
}
return downloads;
}
private TreeSet<Artifact> findDocumentationArtifacts() {
HashSet<String> explicitArtifacts = new HashSet<String>();
TreeSet<Artifact> artifacts = new TreeSet<Artifact>();
for (ExplicitDownloadConfig download : getExplicitDownloads()) {
if (download.getArtifact() != null) {
explicitArtifacts.add(download.getArtifact());
}
else if (download.getFile() != null) {
File downloadFile = resolveFile(download.getFile());
debug("File %s to be added as an extra download.", downloadFile.getAbsolutePath());
SpecifiedArtifact artifact = new SpecifiedArtifact(getName(), downloadFile.getName(), downloadFile);
if (download.getName() != null) {
artifact.setName(download.getName());
}
if (download.getDescription() != null) {
artifact.setDescription(download.getDescription());
}
artifact.setShowLink(!"false".equals(download.getShowLink()));
artifacts.add(artifact);
}
}
for (Artifact artifact : this.enunciate.getArtifacts()) {
if (artifact.isPublic() || explicitArtifacts.contains(artifact.getId())) {
artifacts.add(artifact);
debug("Artifact %s to be added as an extra download.", artifact.getId());
explicitArtifacts.remove(artifact.getId());
}
}
if (explicitArtifacts.size() > 0) {
for (String artifactId : explicitArtifacts) {
warn("WARNING: Unknown artifact '%s'. Will not be available for download.", artifactId);
}
}
return artifacts;
}
public String getDisplaySize(long sizeInBytes) {
String units = "bytes";
float unitSize = 1;
if ((sizeInBytes / 1024) > 0) {
units = "K";
unitSize = 1024;
}
if ((sizeInBytes / 1048576) > 0) {
units = "M";
unitSize = 1048576;
}
if ((sizeInBytes / 1073741824) > 0) {
units = "G";
unitSize = 1073741824;
}
return String.format("%.2f%s", ((float) sizeInBytes) / unitSize, units);
}
/**
* Get the relative path to the root directory from the docs directory.
*
* @return the relative path to the root directory.
*/
protected String getRelativePathToRootDir() {
String relativePath = "./";
String docsSubdir = getDocsSubdir();
if (docsSubdir != null) {
StringBuilder builder = new StringBuilder();
StringTokenizer pathTokens = new StringTokenizer(docsSubdir.replace(File.separatorChar, '/'), "/");
if (pathTokens.hasMoreTokens()) {
while (pathTokens.hasMoreTokens()) {
builder.append("../");
pathTokens.nextToken();
}
relativePath = builder.toString();
}
}
return this.config.getString("[@apiRelativePath]", relativePath);
}
/**
* Loads the default base for the documentation.
*
* @return The default base for the documentation.
*/
protected InputStream loadDefaultBase() {
return DocsModule.class.getResourceAsStream("/docs.base.zip");
}
}