/*
* Copyright 2006-2008 Web Cohesion
*
* 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.codehaus.enunciate.modules.swagger;
import freemarker.template.TemplateException;
import org.apache.commons.digester.RuleSet;
import org.codehaus.enunciate.apt.EnunciateFreemarkerModel;
import org.codehaus.enunciate.contract.Facet;
import org.codehaus.enunciate.contract.jaxrs.ResourceMethod;
import org.codehaus.enunciate.contract.jaxrs.RootResource;
import org.codehaus.enunciate.contract.validation.Validator;
import org.codehaus.enunciate.main.Enunciate;
import org.codehaus.enunciate.main.FileArtifact;
import org.codehaus.enunciate.main.webapp.BaseWebAppFragment;
import org.codehaus.enunciate.main.webapp.WebAppComponent;
import org.codehaus.enunciate.modules.FacetAware;
import org.codehaus.enunciate.modules.FreemarkerDeploymentModule;
import org.codehaus.enunciate.modules.swagger.config.SwaggerRuleSet;
import org.codehaus.enunciate.template.freemarker.UniqueContentTypesMethod;
import org.codehaus.enunciate.util.FacetFilter;
import org.codehaus.jackson.JsonProcessingException;
import org.codehaus.jackson.map.ObjectMapper;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
/**
* <h1>Swagger Module</h1>
*
* <p>The Swagger deployment module generates a <a href="https://developers.helloreverb.com/swagger/">Swagger</a> UI.</a>.
*
* <ul>
* <li><a href="#steps">steps</a></li>
* <li><a href="#config">configuration</a></li>
* <li><a href="#artifacts">artifacts</a></li>
* </ul>
*
* <h1><a name="steps">Steps</a></h1>
*
* <h3>generate</h3>
*
* <p>The only significant step in the Swagger deployment module is the "generate" step. This step generates the
* Swagger UI.</p>
*
* <h1><a name="config">Configuration</a></h1>
*
* <p>The configuration for the XML deployment module is specified by the "swagger" child element of the "modules"
* element of the enunciate configuration file.</p>
*
* <h3>attributes</h3>
*
* <ul>
* <li>The "<b>dir</b>" attribute is used to specify the directory name for the Swagger UI in the deployed application. Default: "ui"</p>
* <li>The "<b>css</b>" attribute is used to specify the file to be used as the cascading stylesheet for the HTML.
* If one isn't supplied, a default will be provided.</p>
* <li>The "<b>base</b>" attribute specifies a gzipped file or a directory to use as the swagger ui base. If none is supplied,
* a default base will be provided.
* <li>The "<b>groupRestResources</b>" attribute specifies the name of a facet by which resources are grouped together. Default: "org.codehaus.enunciate.contract.jaxrs.Resource".</li>
* <li>The "<b>applyBaseUriFilter</b>" attribute specifies whether there should be a servlet filter applied that dynamically configures the Swagger base uri according to where the Swagger UI is deployed.</li>
* <li>The "<b>defaultNamespace</b>" attribute specifies the default namespace for data types. This effects the name of the data type—most data type names have a namespace prefixed to avoid naming collisions.
* If a <code>defaultNamespace</code> is provided, the prefix will be ommitted on data types in that namespace.</li>
* <li>The "<b>freemarkerProcessingTemplate</b>" attribute specifies the file that is the freemarker processing template
* to use to generate swagger JSON. If none is supplied, a default one will be used.</li>
* <li>The "<b>freemarkerProcessingTemplateURL</b>" attribute specifies the URL to the freemarker processing template
* to use to generate swagger JSON. If none is supplied, a default one will be used.</li>
* </ul>
*
* <h3>The "facets" element</h3>
*
* <p>The "facets" element is applicable to the Swagger module to configure which facets are to be included/excluded from the Swagger artifacts. For
* more information, see <a href="http://docs.codehaus.org/display/ENUNCIATE/Enunciate+API+Facets">API Facets</a></p>
*
* <h1><a name="artifacts">Artifacts</a></h1>
*
* <p>The Swagger deployment module exports an artifact named "swagger" that contains the swagger UI.</p>
*
* @author Ryan Heaton
* @docFileName module_swagger.html
*/
public class SwaggerDeploymentModule extends FreemarkerDeploymentModule implements FacetAware {
private String dir = "ui";
private String base;
private String css;
private String groupRestResources = "org.codehaus.enunciate.contract.jaxrs.Resource";
private String defaultNamespace = null;
private boolean applyBaseUriFilter = true;
private String freemarkerProcessingTemplate;
private URL freemarkerProcessingTemplateURL;
private Set<String> facetIncludes = new TreeSet<String>();
private Set<String> facetExcludes = new TreeSet<String>();
/**
* @return "swagger"
*/
@Override
public String getName() {
return "swagger";
}
/**
* The URL to "swagger.fmt".
*
* @return The URL to "swagger.fmt".
*/
protected URL getTemplateURL() throws MalformedURLException {
URL freemarkerProcessingTemplateURL = this.freemarkerProcessingTemplateURL;
if (freemarkerProcessingTemplateURL == null) {
if (this.freemarkerProcessingTemplate != null) {
freemarkerProcessingTemplateURL = getEnunciate().resolvePath(this.freemarkerProcessingTemplate).toURI().toURL();
}
else {
freemarkerProcessingTemplateURL = SwaggerDeploymentModule.class.getResource("swagger.fmt");
}
}
return freemarkerProcessingTemplateURL;
}
@Override
public void doFreemarkerGenerate() throws IOException, TemplateException {
EnunciateFreemarkerModel model = getModel();
File artifactDir = new File(getGenerateDir(), getDir());
model.setFileOutputDirectory(artifactDir);
boolean upToDate = isUpToDate(artifactDir);
if (!upToDate) {
Map<String, Map<String, List<ResourceMethod>>> facetsToResourceMethods = new TreeMap<String, Map<String, List<ResourceMethod>>>();
Map<String, String> facetsToDocs = new TreeMap<String, String>();
for (RootResource rootResource : getModelInternal().getRootResources()) {
for (ResourceMethod resourceMethod : rootResource.getResourceMethods(true)) {
if (FacetFilter.accept(resourceMethod)) {
Set<Facet> facets = resourceMethod.getFacets();
if (facets != null) {
for (Facet facet : facets) {
if (facet.getName().equals(getGroupRestResources())) {
Map<String, List<ResourceMethod>> group = facetsToResourceMethods.get(facet.getValue());
if (group == null) {
group = new TreeMap<String, List<ResourceMethod>>();
facetsToResourceMethods.put(facet.getValue(), group);
}
String subcontext = (String) resourceMethod.getMetaData().get("defaultSubcontext");
if (subcontext == null) {
subcontext = "/rest";
}
String fullpath = subcontext + resourceMethod.getFullpath();
List<ResourceMethod> methods = group.get(fullpath);
if (methods == null) {
methods = new ArrayList<ResourceMethod>();
group.put(fullpath, methods);
}
methods.add(resourceMethod);
facetsToDocs.put(facet.getValue(), facet.getDocumentation());
}
}
}
}
}
}
String defaultNamespace = getDefaultNamespace();
if (defaultNamespace == null && model.getNamespacesToSchemas().size() == 1) {
defaultNamespace = model.getNamespacesToSchemas().keySet().iterator().next();
}
model.setVariable("uniqueContentTypes", new UniqueContentTypesMethod());
model.put("datatypeNameFor", new DatatypeNameForMethod(model, defaultNamespace));
model.put("facetsToResourceMethods", facetsToResourceMethods);
model.put("facetsToDocs", facetsToDocs);
model.put("swaggerDir", artifactDir.getName());
buildBase(artifactDir);
processTemplate(getTemplateURL(), model);
}
else {
info("Skipping generation of Swagger since everything appears up-to-date...");
}
Set<File> jsonFilesToValidate = new HashSet<File>();
gatherJsonFiles(jsonFilesToValidate, artifactDir);
ObjectMapper mapper = new ObjectMapper();
for (File file : jsonFilesToValidate) {
FileReader reader = new FileReader(file);
try {
mapper.readTree(reader);
}
catch (JsonProcessingException e) {
warn("Error processing %s.", file.getAbsolutePath());
throw e;
}
finally {
reader.close();
}
}
//add the webapp fragment...
BaseWebAppFragment webAppFragment = new BaseWebAppFragment(getName());
webAppFragment.setBaseDir(getGenerateDir());
ArrayList<WebAppComponent> filters = new ArrayList<WebAppComponent>();
if (isApplyBaseUriFilter()) {
WebAppComponent swaggerFilter = new WebAppComponent();
swaggerFilter.setName("swagger-ui-filter");
swaggerFilter.setClassname("org.codehaus.enunciate.webapp.IDLFilter");
HashMap<String, String> initParams = new HashMap<String, String>();
initParams.put("assumed-base-address", getModel().getBaseDeploymentAddress());
initParams.put("match-prefix", "\"");
initParams.put("content-type", "application/json");
swaggerFilter.setInitParams(initParams);
TreeSet<String> paths = new TreeSet<String>();
for (File json : jsonFilesToValidate) {
paths.add("/" + artifactDir.getName() + "/" + json.getName());
}
swaggerFilter.setUrlMappings(paths);
filters.add(swaggerFilter);
}
webAppFragment.setFilters(filters);
getEnunciate().addWebAppFragment(webAppFragment);
getEnunciate().addArtifact(new FileArtifact(getName(), "swagger", artifactDir));
}
private void gatherJsonFiles(Set<File> bucket, File buildDir) {
File[] files = buildDir.listFiles();
for (File file : files) {
if (file.getName().endsWith(".json")) {
bucket.add(file);
}
else if (file.isDirectory()) {
gatherJsonFiles(bucket, file);
}
}
}
/**
* Builds the base output directory.
*/
protected void buildBase(File buildDir) throws IOException {
Enunciate enunciate = getEnunciate();
buildDir.mkdirs();
if (this.base == null) {
InputStream discoveredBase = SwaggerDeploymentModule.class.getResourceAsStream("/META-INF/enunciate/swagger-base.zip");
if (discoveredBase == null) {
debug("Default base to be used for swagger base.");
enunciate.extractBase(loadDefaultBase(), buildDir);
if (this.css != null) {
enunciate.copyFile(enunciate.resolvePath(this.css), new File(new File(buildDir, "css"), "screen.css"));
}
}
else {
debug("Discovered documentation base at /META-INF/enunciate/docs-base.zip");
enunciate.extractBase(discoveredBase, buildDir);
}
}
else {
File baseFile = enunciate.resolvePath(this.base);
if (baseFile.isDirectory()) {
debug("Directory %s to be used as the documentation base.", baseFile);
enunciate.copyDir(baseFile, buildDir);
}
else {
debug("Zip file %s to be extracted as the documentation base.", baseFile);
enunciate.extractBase(new FileInputStream(baseFile), buildDir);
}
}
}
/**
* Loads the default base for the swagger ui.
*
* @return The default base for the swagger ui.
*/
protected InputStream loadDefaultBase() {
return SwaggerDeploymentModule.class.getResourceAsStream("/swagger-ui.zip");
}
/**
* Whether the artifact directory is up-to-date.
*
* @param artifactDir The artifact directory.
* @return Whether the artifact directory is up-to-date.
*/
protected boolean isUpToDate(File artifactDir) {
return enunciate.isUpToDateWithSources(artifactDir);
}
@Override
public RuleSet getConfigurationRules() {
return new SwaggerRuleSet();
}
@Override
public Validator getValidator() {
return new SwaggerValidator();
}
/**
* 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 css;
}
/**
* The cascading stylesheet to use instead of the default. This is ignored if the 'base' is also set.
*
* @param css The cascading stylesheet to use instead of the default.
*/
public void setCss(String css) {
this.css = css;
}
/**
* The swagger "base". The swagger base is the initial contents of the directory
* where the swagger ui will be output. Can be a zip file or a directory.
*
* @return The documentation "base".
*/
public String getBase() {
return base;
}
/**
* The swagger "base".
*
* @param base The swagger "base".
*/
public void setBase(String base) {
this.base = base;
}
/**
* The set of facets to include.
*
* @return The set of facets to include.
*/
public Set<String> getFacetIncludes() {
return facetIncludes;
}
/**
* Add a facet include.
*
* @param name The name.
*/
public void addFacetInclude(String name) {
if (name != null) {
this.facetIncludes.add(name);
}
}
/**
* The set of facets to exclude.
*
* @return The set of facets to exclude.
*/
public Set<String> getFacetExcludes() {
return facetExcludes;
}
/**
* Add a facet exclude.
*
* @param name The name.
*/
public void addFacetExclude(String name) {
if (name != null) {
this.facetExcludes.add(name);
}
}
/**
* How to group the REST resources together.
*
* @return How to group the REST resources together.
*/
public String getGroupRestResources() {
return groupRestResources;
}
/**
* How to group the REST resources together.
*
* @param groupRestResources How to group the REST resources together.
*/
public void setGroupRestResources(String groupRestResources) {
this.groupRestResources = groupRestResources;
}
public String getDir() {
return dir;
}
public void setDir(String dir) {
this.dir = dir;
}
public boolean isApplyBaseUriFilter() {
return applyBaseUriFilter;
}
public void setApplyBaseUriFilter(boolean applyBaseUriFilter) {
this.applyBaseUriFilter = applyBaseUriFilter;
}
public String getDefaultNamespace() {
return defaultNamespace;
}
public void setDefaultNamespace(String defaultNamespace) {
this.defaultNamespace = defaultNamespace;
}
public void setFreemarkerProcessingTemplate(String freemarkerProcessingTemplate) throws MalformedURLException {
this.freemarkerProcessingTemplate = freemarkerProcessingTemplate;
}
public URL getFreemarkerProcessingTemplateURL() {
return freemarkerProcessingTemplateURL;
}
public void setFreemarkerProcessingTemplateURL(URL freemarkerProcessingTemplateURL) {
this.freemarkerProcessingTemplateURL = freemarkerProcessingTemplateURL;
}
}