/** * Copyright (C) 2014-2016 LinkedIn Corp. (pinot-core@linkedin.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.linkedin.pinot.common.restlet.swagger; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.linkedin.pinot.common.restlet.PinotRestletApplication; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.restlet.Restlet; import org.restlet.engine.header.Header; import org.restlet.representation.Representation; import org.restlet.representation.StringRepresentation; import org.restlet.resource.Finder; import org.restlet.resource.Get; import org.restlet.resource.ServerResource; import org.restlet.routing.Filter; import org.restlet.routing.Route; import org.restlet.routing.Router; import org.restlet.routing.TemplateRoute; import org.restlet.util.RouteList; import org.restlet.util.Series; /** * Resource that returns a Swagger definition of the API */ public class SwaggerResource extends ServerResource { @Get @Override public Representation get() { try { // Info JSONObject info = new JSONObject(); info.put("title", "Pinot Controller"); info.put("version", "0.1"); // Paths JSONObject paths = new JSONObject(); Router router = PinotRestletApplication.getRouter(); RouteList routeList = router.getRoutes(); for (Route route : routeList) { if (route instanceof TemplateRoute) { TemplateRoute templateRoute = (TemplateRoute) route; JSONObject pathObject = new JSONObject(); String routePath = templateRoute.getTemplate().getPattern(); // Check which methods are present Restlet routeTarget = templateRoute.getNext(); if (routeTarget instanceof Finder) { Finder finder = (Finder) routeTarget; generateSwaggerForFinder(pathObject, routePath, finder); } else if (routeTarget instanceof Filter) { do { Filter filter = (Filter) routeTarget; routeTarget = filter.getNext(); } while (routeTarget instanceof Filter); if (routeTarget instanceof Finder) { Finder finder = (Finder) routeTarget; generateSwaggerForFinder(pathObject, routePath, finder); } } if (pathObject.keys().hasNext()) { paths.put(routePath, pathObject); } } } // Tags JSONArray tags = new JSONArray(); addTag(tags, "tenant", "Tenant-related operations"); addTag(tags, "instance", "Instance-related operations"); addTag(tags, "table", "Table-related operations"); addTag(tags, "segment", "Segment-related operations"); addTag(tags, "schema", "Schema-related operations"); addTag(tags, "version", "Version-related operations"); // Swagger JSONObject swagger = new JSONObject(); swagger.put("swagger", "2.0"); swagger.put("info", info); swagger.put("paths", paths); swagger.put("tags", tags); StringRepresentation representation = new StringRepresentation(swagger.toString()); // Set up CORS Series<Header> responseHeaders = (Series<Header>) getResponse().getAttributes().get("org.restlet.http.headers"); if (responseHeaders == null) { responseHeaders = new Series(Header.class); getResponse().getAttributes().put("org.restlet.http.headers", responseHeaders); } responseHeaders.add(new Header("Access-Control-Allow-Origin", "*")); return representation; } catch (JSONException e) { return new StringRepresentation(e.toString()); } } private void generateSwaggerForFinder(JSONObject pathObject, String routePath, Finder finder) throws JSONException { Class<? extends ServerResource> targetClass = finder.getTargetClass(); for (Method method : targetClass.getDeclaredMethods()) { String httpVerb = null; Annotation annotationInstance = method.getAnnotation(HttpVerb.class); if (annotationInstance != null) { httpVerb = ((HttpVerb) annotationInstance).value().toLowerCase(); } HashSet<String> methodPaths = new HashSet<String>(); annotationInstance = method.getAnnotation(Paths.class); if (annotationInstance != null) { methodPaths.addAll(Arrays.asList(((Paths) annotationInstance).value())); } if (httpVerb != null && methodPaths.contains(routePath) && !routePath.endsWith("/")) { JSONObject operation = new JSONObject(); pathObject.put(httpVerb, operation); annotationInstance = method.getAnnotation(Summary.class); if (annotationInstance != null) { operation.put(Summary.class.getSimpleName().toLowerCase(), ((Summary) annotationInstance).value()); } annotationInstance = method.getAnnotation(Description.class); if (annotationInstance != null) { operation.put(Description.class.getSimpleName().toLowerCase(), ((Description) annotationInstance).value()); } annotationInstance = method.getAnnotation(Tags.class); if (annotationInstance != null) { operation.put(Tags.class.getSimpleName().toLowerCase(), ((Tags) annotationInstance).value()); } annotationInstance = method.getAnnotation(Responses.class); if (annotationInstance != null) { Responses responsesAnnotation = (Responses) annotationInstance; JSONObject responses = new JSONObject(); for (Response responseAnnotation : responsesAnnotation.value()) { JSONObject response = new JSONObject(); response.put("description", responseAnnotation.description()); responses.put(responseAnnotation.statusCode(), response); } operation.put(Responses.class.getSimpleName().toLowerCase(), responses); } operation.put("operationId", method.getName()); ArrayList<JSONObject> parameters = new ArrayList<JSONObject>(); Annotation[][] parameterAnnotations = method.getParameterAnnotations(); Class<?>[] parameterTypes = method.getParameterTypes(); for (int i = 0; i < parameterTypes.length; i++) { Class<?> parameterType = parameterTypes[i]; Annotation[] annotations = parameterAnnotations[i]; if (annotations.length != 0) { JSONObject parameter = new JSONObject(); for (Annotation annotation : annotations) { if (annotation instanceof Parameter) { Parameter parameterAnnotation = (Parameter) annotation; parameter.put("name", parameterAnnotation.name()); parameter.put("in", parameterAnnotation.in()); if (parameterAnnotation.description() != null) { parameter.put("description", parameterAnnotation.description()); } parameter.put("required", parameterAnnotation.required()); if (parameterType.equals(String.class)) { parameter.put("type", "string"); } else if (parameterType.equals(Boolean.class) || parameterType.equals(Boolean.TYPE)) { parameter.put("type", "boolean"); } else if (parameterType.equals(Integer.class) || parameterType.equals(Integer.TYPE)) { parameter.put("type", "integer"); } else if (parameterType.equals(Long.class) || parameterType.equals(Long.TYPE)) { // Long maps to integer type in http://swagger.io/specification/#dataTypeFormat parameter.put("type", "integer"); } else if (parameterType.equals(Float.class) || parameterType.equals(Float.TYPE)) { parameter.put("type", "boolean"); } else if (parameterType.equals(Double.class) || parameterType.equals(Double.TYPE)) { parameter.put("type", "double"); } else if (parameterType.equals(Byte.class) || parameterType.equals(Byte.TYPE)) { // Byte maps to string type in http://swagger.io/specification/#dataTypeFormat parameter.put("type", "string"); } else if (isDocumentableType(parameterType)) { parameter.put("schema", schemaForType(parameterType)); } else { parameter.put("type", "string"); } } } if(parameter.keys().hasNext()) { parameters.add(parameter); } } } operation.put("parameters", parameters.toArray(new JSONObject[parameters.size()])); } } } private JSONObject schemaForType(Class<?> type) { try { JSONObject schema = new JSONObject(); schema.put("type", "object"); schema.put("title", type.getSimpleName()); Example example = type.getAnnotation(Example.class); if (example != null) { schema.put("example", new JSONObject(example.value())); } for (Constructor<?> constructor : type.getConstructors()) { if (constructor.isAnnotationPresent(JsonCreator.class)) { JSONObject properties = new JSONObject(); JSONArray required = new JSONArray(); Annotation[][] parameterAnnotations = constructor.getParameterAnnotations(); Class<?>[] parameterTypes = constructor.getParameterTypes(); for (int i = 0; i < parameterTypes.length; i++) { Class<?> parameterType = parameterTypes[i]; Annotation[] annotations = parameterAnnotations[i]; if (annotations.length != 0) { for (Annotation annotation : annotations) { if (annotation instanceof JsonProperty) { JsonProperty jsonPropertyAnnotation = (JsonProperty) annotation; JSONObject parameter = new JSONObject(); properties.put(jsonPropertyAnnotation.value(), parameter); if (parameterType.equals(String.class)) { parameter.put("type", "string"); } else if (parameterType.equals(Boolean.class) || parameterType.equals(Boolean.TYPE)) { parameter.put("type", "boolean"); } else if (parameterType.equals(Integer.class) || parameterType.equals(Integer.TYPE)) { parameter.put("type", "integer"); } else if (parameterType.equals(Long.class) || parameterType.equals(Long.TYPE)) { // Long maps to integer type in http://swagger.io/specification/#dataTypeFormat parameter.put("type", "integer"); } else if (parameterType.equals(Float.class) || parameterType.equals(Float.TYPE)) { parameter.put("type", "boolean"); } else if (parameterType.equals(Double.class) || parameterType.equals(Double.TYPE)) { parameter.put("type", "double"); } else if (parameterType.equals(Byte.class) || parameterType.equals(Byte.TYPE)) { // Byte maps to string type in http://swagger.io/specification/#dataTypeFormat parameter.put("type", "string"); } else { parameter.put("type", "string"); } if (jsonPropertyAnnotation.required()) { required.put(jsonPropertyAnnotation.value()); } } } } } if (required.length() != 0) { schema.put("required", required); } schema.put("properties", properties); break; } } return schema; } catch (Exception e) { return new JSONObject(); } } private boolean isDocumentableType(Class<?> clazz) { Constructor<?>[] constructors = clazz.getDeclaredConstructors(); for (Constructor<?> constructor : constructors) { Annotation[] constructorAnnotations = constructor.getDeclaredAnnotations(); for (Annotation constructorAnnotation : constructorAnnotations) { if (constructorAnnotation instanceof JsonCreator) { return true; } } } return false; } private void addTag(JSONArray tags, String tagName, String description) throws JSONException { JSONObject tag = new JSONObject(); tag.put("name", tagName); tag.put("description", description); tags.put(tag); } }