/**
* 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.swagger;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.webcohesion.enunciate.EnunciateConfiguration;
import com.webcohesion.enunciate.EnunciateContext;
import com.webcohesion.enunciate.EnunciateException;
import com.webcohesion.enunciate.api.ApiRegistrationContext;
import com.webcohesion.enunciate.api.ApiRegistry;
import com.webcohesion.enunciate.api.InterfaceDescriptionFile;
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.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 java.io.*;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.*;
/**
* <h1>Swagger Module</h1>
* @author Ryan Heaton
*/
public class SwaggerDeploymentModule extends BasicGeneratingModule implements ApiFeatureProviderModule, ApiRegistryAwareModule, ApiRegistryProviderModule {
private ApiRegistry apiRegistry;
/**
* @return "swagger"
*/
@Override
public String getName() {
return "swagger";
}
@Override
public void setApiRegistry(ApiRegistry registry) {
this.apiRegistry = registry;
}
@Override
public List<DependencySpec> getDependencySpecifications() {
return Arrays.asList((DependencySpec) new DependencySpec() {
@Override
public boolean accept(EnunciateModule module) {
return !getName().equals(module.getName()) && module instanceof ApiRegistryProviderModule;
}
@Override
public boolean isFulfilled() {
return true;
}
@Override
public String toString() {
return "all api registry provider modules";
}
});
}
/**
* The URL to "swagger.fmt".
*
* @return The URL to "swagger.fmt".
*/
protected URL getTemplateURL() throws MalformedURLException {
String template = getFreemarkerProcessingTemplate();
if (template != null) {
return this.enunciate.getConfiguration().resolveFile(template).toURI().toURL();
}
else {
return SwaggerDeploymentModule.class.getResource("swagger.fmt");
}
}
@Override
public void call(EnunciateContext context) {
//no-op; work happens with the swagger interface description.
}
@Override
public ApiRegistry getApiRegistry() {
return new ApiRegistry() {
@Override
public List<ServiceApi> getServiceApis(ApiRegistrationContext context) {
return Collections.emptyList();
}
@Override
public List<ResourceApi> getResourceApis(ApiRegistrationContext context) {
return Collections.emptyList();
}
@Override
public Set<Syntax> getSyntaxes(ApiRegistrationContext context) {
return Collections.emptySet();
}
@Override
public InterfaceDescriptionFile getSwaggerUI(ApiRegistrationContext context) {
List<ResourceApi> resourceApis = apiRegistry.getResourceApis(context);
if (resourceApis == null || resourceApis.isEmpty()) {
info("No resource APIs registered: Swagger UI will not be generated.");
}
return new SwaggerInterfaceDescription(resourceApis, context);
}
};
}
private class SwaggerInterfaceDescription implements InterfaceDescriptionFile {
private final List<ResourceApi> resourceApis;
private final ApiRegistrationContext context;
public SwaggerInterfaceDescription(List<ResourceApi> resourceApis, ApiRegistrationContext context) {
this.resourceApis = resourceApis;
this.context = context;
}
@Override
public String getHref() {
return getDocsSubdir() + "/index.html";
}
@Override
public void writeTo(File srcDir) throws IOException {
srcDir.mkdirs();
String subdir = getDocsSubdir();
if (subdir != null) {
srcDir = new File(srcDir, subdir);
srcDir.mkdirs();
}
Map<String, Object> model = new HashMap<String, Object>();
model.put("apis", this.resourceApis);
model.put("syntaxes", apiRegistry.getSyntaxes(this.context));
model.put("file", new FileDirective(srcDir, SwaggerDeploymentModule.this.enunciate.getLogger()));
model.put("projectVersion", enunciate.getConfiguration().getVersion());
model.put("projectTitle", enunciate.getConfiguration().getTitle());
model.put("projectDescription", enunciate.getConfiguration().readDescription(SwaggerDeploymentModule.this.context, true));
model.put("termsOfService", enunciate.getConfiguration().getTerms());
List<EnunciateConfiguration.Contact> contacts = enunciate.getConfiguration().getContacts();
model.put("contact", contacts == null || contacts.isEmpty() ? null : contacts.get(0));
model.put("license", enunciate.getConfiguration().getApiLicense());
model.put("baseDatatypeNameFor", new BaseDatatypeNameForMethod());
model.put("referencedDatatypeNameFor", new ReferencedDatatypeNameForMethod());
model.put("dataFormatNameFor", new DataFormatNameForMethod());
model.put("constraintsFor", new ConstraintsForMethod());
model.put("uniqueMediaTypesFor", new UniqueMediaTypesForMethod());
model.put("jsonExamplesFor", new JsonExamplesForMethod());
model.put("jsonExampleFor", new JsonExampleForMethod());
model.put("responsesOf", new ResponsesOfMethod());
model.put("findBestDataType", new FindBestDataTypeMethod());
model.put("validParametersOf", new ValidParametersMethod());
model.put("host", getHost());
model.put("schemes", getSchemes());
model.put("basePath", getBasePath());
buildBase(srcDir);
try {
processTemplate(getTemplateURL(), model);
}
catch (TemplateException e) {
throw new EnunciateException(e);
}
Set<File> jsonFilesToValidate = new HashSet<File>();
gatherJsonFiles(jsonFilesToValidate, srcDir);
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();
}
}
FileArtifact swaggerArtifact = new FileArtifact(getName(), "swagger", srcDir);
swaggerArtifact.setPublic(false);
SwaggerDeploymentModule.this.enunciate.addArtifact(swaggerArtifact);
}
}
protected String getHost() {
String host = this.config.getString("[@host]", null);
if (host == null) {
String root = enunciate.getConfiguration().getApplicationRoot();
if (root != null) {
try {
URI uri = URI.create(root);
host = uri.getHost();
if (uri.getPort() > 0) {
host += ":" + uri.getPort();
}
}
catch (IllegalArgumentException e) {
host = null;
}
}
}
return host;
}
protected String[] getSchemes() {
return this.config.getStringArray("scheme");
}
protected String getBasePath() {
String basePath = this.config.getString("[@basePath]", null);
if (basePath == null) {
String root = enunciate.getConfiguration().getApplicationRoot();
if (root != null) {
try {
URI uri = URI.create(root);
basePath = uri.getPath();
}
catch (IllegalArgumentException e) {
basePath = null;
}
}
while (basePath != null && basePath.endsWith("/")) {
basePath = basePath.substring(0, basePath.length() - 1);
}
}
return basePath;
}
/**
* Processes the specified template with the given model.
*
* @param templateURL The template URL.
* @param model The root model.
*/
public String 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.setObjectWrapper(new SwaggerUIObjectWrapper());
Template template = configuration.getTemplate(templateURL.toString());
StringWriter unhandledOutput = new StringWriter();
template.process(model, unhandledOutput);
unhandledOutput.close();
return unhandledOutput.toString();
}
/**
* Builds the base output directory.
*/
protected void buildBase(File buildDir) throws IOException {
String base = getBase();
if (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.unzip(loadDefaultBase(), buildDir);
String css = getCss();
if (css != null) {
enunciate.copyFile(enunciate.getConfiguration().resolveFile(css), new File(new File(buildDir, "css"), "screen.css"));
}
}
else {
debug("Discovered documentation base at /META-INF/enunciate/swagger-base.zip");
enunciate.unzip(discoveredBase, buildDir);
}
}
else {
File baseFile = enunciate.getConfiguration().resolveFile(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.unzip(new FileInputStream(baseFile), buildDir);
}
}
}
private void gatherJsonFiles(Set<File> bucket, File buildDir) {
File[] files = buildDir.listFiles();
if (files != null) {
for (File file : files) {
if (file.getName().endsWith(".json")) {
bucket.add(file);
}
else if (file.isDirectory()) {
gatherJsonFiles(bucket, file);
}
}
}
}
/**
* 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");
}
/**
* 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]", null);
}
public String getFreemarkerProcessingTemplate() {
return this.config.getString("[@freemarkerProcessingTemplate]", null);
}
/**
* 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 this.config.getString("[@base]", null);
}
public Set<String> getFacetIncludes() {
List<Object> includes = this.config.getList("facets.include[@name]");
Set<String> facetIncludes = new TreeSet<String>();
for (Object include : includes) {
facetIncludes.add(String.valueOf(include));
}
return facetIncludes;
}
public Set<String> getFacetExcludes() {
List<Object> excludes = this.config.getList("facets.exclude[@name]");
Set<String> facetExcludes = new TreeSet<String>();
for (Object exclude : excludes) {
facetExcludes.add(String.valueOf(exclude));
}
return facetExcludes;
}
public String getDocsSubdir() {
return this.config.getString("[@docsSubdir]", "ui");
}
}