/* * Copyright (C) 2015 Sebastian Daschner, sebastian-daschner.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/LICENSE2.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.sebastian_daschner.jaxrs_analyzer.backend.swagger; import com.sebastian_daschner.jaxrs_analyzer.backend.Backend; import com.sebastian_daschner.jaxrs_analyzer.model.rest.*; import com.sebastian_daschner.jaxrs_analyzer.utils.StringUtils; import javax.json.*; import javax.json.stream.JsonGenerator; import javax.ws.rs.core.Response; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import static com.sebastian_daschner.jaxrs_analyzer.backend.ComparatorUtils.mapKeyComparator; import static com.sebastian_daschner.jaxrs_analyzer.backend.ComparatorUtils.parameterComparator; import static java.util.Collections.singletonMap; import static java.util.Comparator.comparing; /** * A backend which produces a Swagger JSON representation of the resources. * * @author Sebastian Daschner */ public class SwaggerBackend implements Backend { private static final String NAME = "Swagger"; private static final String SWAGGER_VERSION = "2.0"; private final Lock lock = new ReentrantLock(); private final SwaggerOptions options = new SwaggerOptions(); private Resources resources; private JsonObjectBuilder builder; private SchemaBuilder schemaBuilder; private String projectName; private String projectVersion; @Override public void configure(final Map<String, String> config) { options.configure(config); } @Override public byte[] render(final Project project) { lock.lock(); try { // initialize fields builder = Json.createObjectBuilder(); resources = project.getResources(); projectName = project.getName(); projectVersion = project.getVersion(); schemaBuilder = new SchemaBuilder(resources.getTypeRepresentations()); final JsonObject output = modifyJson(renderInternal()); return serialize(output); } finally { lock.unlock(); } } private JsonObject modifyJson(final JsonObject json) { if (options.getJsonPatch() == null) return json; return options.getJsonPatch().apply(json); } private JsonObject renderInternal() { appendHeader(); appendPaths(); appendDefinitions(); return builder.build(); } private void appendHeader() { builder.add("swagger", SWAGGER_VERSION).add("info", Json.createObjectBuilder() .add("version", projectVersion).add("title", projectName)) .add("host", options.getDomain() == null ? "" : options.getDomain()).add("basePath", (options.getDomain() != null && !"".equals(options.getDomain().trim()) ? '/' : '/' + projectName + '/') + resources.getBasePath()) .add("schemes", options.getSchemes().stream().map(Enum::name).map(String::toLowerCase).sorted() .collect(Json::createArrayBuilder, JsonArrayBuilder::add, JsonArrayBuilder::add).build()); if (options.isRenderTags()) { final JsonArrayBuilder tags = Json.createArrayBuilder(); resources.getResources().stream() .map(this::extractTag).filter(Objects::nonNull) .distinct().sorted() .map(tag -> Json.createObjectBuilder().add("name", tag)) .forEach(tags::add); builder.add("tags", tags); } } private String extractTag(final String s) { final int offset = options.getTagsPathOffset(); final String[] parts = s.split("/"); if (parts.length > offset && !parts[offset].contains("{")) { return parts[offset]; } return null; } private void appendPaths() { final JsonObjectBuilder paths = Json.createObjectBuilder(); resources.getResources().stream().sorted().forEach(s -> paths.add('/' + s, buildPathDefinition(s))); builder.add("paths", paths); } private JsonObjectBuilder buildPathDefinition(final String s) { final JsonObjectBuilder methods = Json.createObjectBuilder(); resources.getMethods(s).stream() .sorted(comparing(ResourceMethod::getMethod)) .forEach(m -> methods.add(m.getMethod().toString().toLowerCase(), buildForMethod(m, s))); return methods; } private JsonObjectBuilder buildForMethod(final ResourceMethod method, final String s) { final JsonArrayBuilder consumes = Json.createArrayBuilder(); method.getRequestMediaTypes().stream().sorted().forEach(consumes::add); final JsonArrayBuilder produces = Json.createArrayBuilder(); method.getResponseMediaTypes().stream().sorted().forEach(produces::add); final JsonObjectBuilder builder = Json.createObjectBuilder(); if (method.getDescription() != null) builder.add("description", method.getDescription()); builder.add("consumes", consumes).add("produces", produces) .add("parameters", buildParameters(method)).add("responses", buildResponses(method)); if (method.isDeprecated()) builder.add("deprecated", true); if (options.isRenderTags()) Optional.ofNullable(extractTag(s)).ifPresent(t -> builder.add("tags", Json.createArrayBuilder().add(t))); return builder; } private JsonArrayBuilder buildParameters(final ResourceMethod method) { final Set<MethodParameter> parameters = method.getMethodParameters(); final JsonArrayBuilder parameterBuilder = Json.createArrayBuilder(); buildParameters(parameters, ParameterType.PATH, parameterBuilder); buildParameters(parameters, ParameterType.HEADER, parameterBuilder); buildParameters(parameters, ParameterType.QUERY, parameterBuilder); buildParameters(parameters, ParameterType.FORM, parameterBuilder); if (method.getRequestBody() != null) { final JsonObjectBuilder requestBuilder = Json.createObjectBuilder() .add("name", "body") .add("in", "body") .add("required", true) .add("schema", schemaBuilder.build(method.getRequestBody())); if (!StringUtils.isBlank(method.getRequestBodyDescription())) requestBuilder.add("description", method.getRequestBodyDescription()); parameterBuilder.add(requestBuilder); } return parameterBuilder; } private void buildParameters(final Set<MethodParameter> parameters, final ParameterType parameterType, final JsonArrayBuilder builder) { parameters.stream().filter(p -> p.getParameterType() == parameterType) .sorted(parameterComparator()) .forEach(e -> { final String swaggerParameterType = getSwaggerParameterType(parameterType); if (swaggerParameterType != null) { final JsonObjectBuilder paramBuilder = schemaBuilder.build(e.getType()) .add("name", e.getName()) .add("in", swaggerParameterType) .add("required", e.getDefaultValue() == null); if (!StringUtils.isBlank(e.getDescription())) { paramBuilder.add("description", e.getDescription()); } if (!StringUtils.isBlank(e.getDefaultValue())) { paramBuilder.add("default", e.getDefaultValue()); } builder.add(paramBuilder); } }); } private JsonObjectBuilder buildResponses(final ResourceMethod method) { final JsonObjectBuilder responses = Json.createObjectBuilder(); method.getResponses().entrySet().stream().sorted(mapKeyComparator()).forEach(e -> { final JsonObjectBuilder headers = Json.createObjectBuilder(); e.getValue().getHeaders().stream().sorted().forEach(h -> headers.add(h, Json.createObjectBuilder().add("type", "string"))); final JsonObjectBuilder response = Json.createObjectBuilder() .add("description", Optional.ofNullable(Response.Status.fromStatusCode(e.getKey())).map(Response.Status::getReasonPhrase).orElse("")) .add("headers", headers); if (e.getValue().getResponseBody() != null) { final JsonObject schema = schemaBuilder.build(e.getValue().getResponseBody()).build(); if (!schema.isEmpty()) response.add("schema", schema); } responses.add(e.getKey().toString(), response); }); return responses; } private void appendDefinitions() { builder.add("definitions", schemaBuilder.getDefinitions()); } @Override public String getName() { return NAME; } private static String getSwaggerParameterType(final ParameterType parameterType) { switch (parameterType) { case QUERY: return "query"; case PATH: return "path"; case HEADER: return "header"; case FORM: return "formData"; default: // TODO handle others (possible w/ Swagger?) return null; } } private static byte[] serialize(final JsonObject jsonObject) { try (final ByteArrayOutputStream output = new ByteArrayOutputStream()) { final Map<String, ?> config = singletonMap(JsonGenerator.PRETTY_PRINTING, true); final JsonWriter jsonWriter = Json.createWriterFactory(config).createWriter(output); jsonWriter.write(jsonObject); jsonWriter.close(); return output.toByteArray(); } catch (IOException e) { throw new RuntimeException("Could not write Swagger output", e); } } }