/******************************************************************************* * Copyright (C) 2014 The Calrissian Authors * * 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.calrissian.restdoclet.writer.swagger; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import org.calrissian.restdoclet.Configuration; import org.calrissian.restdoclet.model.*; import org.calrissian.restdoclet.writer.Writer; import org.calrissian.restdoclet.writer.swagger.model.*; import java.io.*; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import static java.util.Map.Entry; import static org.calrissian.restdoclet.util.CommonUtils.*; import static org.calrissian.restdoclet.writer.swagger.TypeUtils.*; public class SwaggerWriter implements Writer { public static final String OUTPUT_OPTION_NAME = "swagger"; private static final String SWAGGER_DEFAULT_HTML = "swagger/index.html"; private static final String SWAGGER_CALLABLE_HTML = "swagger/index-callable.html"; private static final String SWAGGER_UI_ARTIFACT = "swagger/swagger-ui.zip"; private static final String SWAGGER_VERSION = "1.2"; private static final String RESOURCE_DOC = "./api-docs"; private static final String API_DOC_DIR = "apis"; private static ObjectMapper mapper = new ObjectMapper() .configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false) .setSerializationInclusion(JsonInclude.Include.NON_NULL); @Override public void write(Collection<ClassDescriptor> classDescriptors, Configuration config) throws IOException { Map<String, Collection<Endpoint>> resources = new LinkedHashMap<String, Collection<Endpoint>>(); for (ClassDescriptor classDescriptor : classDescriptors) { for (Endpoint endpoint : classDescriptor.getEndpoints()) { String resourceName = getResource(classDescriptor.getContextPath(), endpoint); if (resources.containsKey(resourceName)) { resources.get(resourceName).add(endpoint); } else { Collection<Endpoint> tmp = new ArrayList<Endpoint>(); tmp.add(endpoint); resources.put(resourceName, tmp); } } } writeResource(resources, config); copyIndex(config); copySwagger(); } private static void writeResource(Map<String, Collection<Endpoint>> resources, Configuration config) throws IOException { ResourceListing resourceListing = new ResourceListing(SWAGGER_VERSION, config.getApiVersion(), config.getDocumentTitle()); for (Entry<String, Collection<Endpoint>> entry : resources.entrySet()) { resourceListing.addApi("/../" + API_DOC_DIR + entry.getKey(), ""); writeApi(entry.getKey(), entry.getValue(), config); } mapper.writerWithDefaultPrettyPrinter().writeValue(new FileOutputStream(RESOURCE_DOC), resourceListing); } private static void writeApi(String resource, Collection<Endpoint> endpoints, Configuration config) throws IOException { Map<String, Collection<Endpoint>> pathGroups = groupPaths(endpoints); File apiFile = new File("./" + API_DOC_DIR , resource); apiFile.getParentFile().mkdirs(); Collection<Api> apis = new ArrayList<Api>(pathGroups.size()); for (Entry<String, Collection<Endpoint>> entry : pathGroups.entrySet()) apis.add(new Api(entry.getKey(), "", getOperations(entry.getValue()))); mapper.writerWithDefaultPrettyPrinter().writeValue(new FileOutputStream(apiFile), new ApiListing(SWAGGER_VERSION, config.getPath(), resource, config.getApiVersion(), apis) ); } private static Collection<Operation> getOperations(Collection<Endpoint> endpoints) { Collection<Operation> operations = new ArrayList<Operation>(endpoints.size()); for (Endpoint endpoint : endpoints) { Collection<Parameter> params = new ArrayList<Parameter>(); for (PathVar pathVar : endpoint.getPathVars()) params.add(getParameter(pathVar)); for (QueryParam queryParam : endpoint.getQueryParams()) params.add(getParameter(queryParam)); if (endpoint.getRequestBody() != null) params.add(getParameter(endpoint.getRequestBody())); operations.add( new Operation( endpoint.getHttpMethod(), "nickname", endpoint.getShortDescription(), endpoint.getDescription(), dataType(endpoint.getType()), endpoint.getProduces(), endpoint.getConsumes(), params ) ); } return operations; } private static Parameter getParameter(PathVar pathVar) { return new Parameter( "path", pathVar.getName(), pathVar.getDescription(), basicType(pathVar.getType()), null, true, false, allowableValues(pathVar.getType()) ); } private static Parameter getParameter(QueryParam queryParam) { //If it is a container type then allow multiple but use the underlying type. boolean container = isContainer(queryParam.getType()); return new Parameter( "query", queryParam.getName(), queryParam.getDescription(), (container ? internalContainerType(queryParam.getType()) : basicType(queryParam.getType())), null, queryParam.isRequired(), container, allowableValues(queryParam.getType()) ); } private static Parameter getParameter(RequestBody requestBody) { return new Parameter( "body", requestBody.getName(), requestBody.getDescription(), dataType(requestBody.getType()), null, true, false, allowableValues(requestBody.getType()) ); } private static Map<String, Collection<Endpoint>> groupPaths (Collection<Endpoint> endpoints) { Map<String, Collection<Endpoint>> paths = new LinkedHashMap<String, Collection<Endpoint>>(); for (Endpoint endpoint : endpoints) { if (paths.containsKey(endpoint.getPath())) { paths.get(endpoint.getPath()).add(endpoint); } else { Collection<Endpoint> tmp = new ArrayList<Endpoint>(); tmp.add(endpoint); paths.put(endpoint.getPath(), tmp); } } return paths; } /** * Will get the first path segment that follows the context path. Will return the partial path as the resource id. */ private static String getResource(String contextPath, Endpoint endpoint) { if (endpoint == null || isEmpty(endpoint.getPath())) return "/"; //Shouldn't need to do this, but being safe. String tmp = fixPath(endpoint.getPath()); //First normalize the path then, if not part of the path then simply ignore it. contextPath = fixPath(contextPath); contextPath = (!tmp.startsWith(contextPath) ? "" : contextPath); //remove the context path for evaluation tmp = tmp.substring(contextPath.length()); if (tmp.indexOf("/", 1) > 0) tmp = tmp.substring(0, tmp.indexOf("/", 1)); return contextPath + tmp; } private static void copyIndex(Configuration config) throws IOException { InputStream in = null; OutputStream out = null; try { if (config.isCallable()) in = Thread.currentThread().getContextClassLoader().getResourceAsStream(SWAGGER_CALLABLE_HTML); else in = Thread.currentThread().getContextClassLoader().getResourceAsStream(SWAGGER_DEFAULT_HTML); out = new FileOutputStream(new File(".", "index.html")); copy(in, out); } finally { close(in, out); } } private static void copySwagger() throws IOException { ZipInputStream swaggerZip = null; FileOutputStream out = null; try{ swaggerZip = new ZipInputStream(Thread.currentThread().getContextClassLoader().getResourceAsStream(SWAGGER_UI_ARTIFACT)); ZipEntry entry; while ((entry = swaggerZip.getNextEntry()) != null) { final File swaggerFile = new File(".", entry.getName()); if (entry.isDirectory()) { if (!swaggerFile.isDirectory() && !swaggerFile.mkdirs()) { throw new RuntimeException("Unable to create directory: " + swaggerFile); } } else { copy(swaggerZip, new FileOutputStream(swaggerFile)); } } } finally { close(swaggerZip, out); } } }