Backporting fix to SwaggerToOpenApiConversionUtils from CXF 3.1.16-SNAPSHOT
Project: http://git-wip-us.apache.org/repos/asf/syncope/repo Commit: http://git-wip-us.apache.org/repos/asf/syncope/commit/fa079531 Tree: http://git-wip-us.apache.org/repos/asf/syncope/tree/fa079531 Diff: http://git-wip-us.apache.org/repos/asf/syncope/diff/fa079531 Branch: refs/heads/2_0_X Commit: fa079531e4dc8f40a3ee62479b485394a619613d Parents: 6ecbc88 Author: Francesco Chicchiriccò <[email protected]> Authored: Mon Mar 19 16:14:47 2018 +0100 Committer: Francesco Chicchiriccò <[email protected]> Committed: Mon Mar 19 17:02:57 2018 +0100 ---------------------------------------------------------------------- .../SwaggerToOpenApiConversionFilter.java | 100 +++++ .../SwaggerToOpenApiConversionUtils.java | 405 +++++++++++++++++++ .../src/main/resources/restCXFContext.xml | 2 +- 3 files changed, 506 insertions(+), 1 deletion(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/syncope/blob/fa079531/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/openapi/SwaggerToOpenApiConversionFilter.java ---------------------------------------------------------------------- diff --git a/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/openapi/SwaggerToOpenApiConversionFilter.java b/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/openapi/SwaggerToOpenApiConversionFilter.java new file mode 100644 index 0000000..c07e5fc --- /dev/null +++ b/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/openapi/SwaggerToOpenApiConversionFilter.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.syncope.core.rest.cxf.openapi; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.util.Objects; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.PreMatching; +import javax.ws.rs.ext.Provider; +import javax.ws.rs.ext.WriterInterceptor; +import javax.ws.rs.ext.WriterInterceptorContext; +import org.apache.cxf.common.util.StringUtils; +import org.apache.cxf.helpers.IOUtils; +import org.apache.cxf.io.CachedOutputStream; +import org.apache.cxf.jaxrs.ext.MessageContext; +import org.apache.cxf.jaxrs.swagger.openapi.OpenApiConfiguration; +import org.apache.cxf.jaxrs.utils.JAXRSUtils; + +@Provider +@PreMatching +public final class SwaggerToOpenApiConversionFilter implements ContainerRequestFilter, WriterInterceptor { + + private static final String SWAGGER_PATH = "swagger.json"; + + private static final String OPEN_API_PATH = "openapi.json"; + + private static final String OPEN_API_PROPERTY = "openapi"; + + private OpenApiConfiguration openApiConfig; + + private String openApiJsonPath = OPEN_API_PATH; + + @Override + public void filter(final ContainerRequestContext reqCtx) throws IOException { + String path = reqCtx.getUriInfo().getPath(); + if (path.endsWith(openApiJsonPath)) { + reqCtx.setRequestUri(URI.create(SWAGGER_PATH)); + JAXRSUtils.getCurrentMessage().getExchange().put(OPEN_API_PROPERTY, Boolean.TRUE); + } + + } + + public OpenApiConfiguration getOpenApiConfig() { + return openApiConfig; + } + + public void setOpenApiConfig(final OpenApiConfiguration openApiConfig) { + this.openApiConfig = openApiConfig; + } + + @Override + public void aroundWriteTo(final WriterInterceptorContext context) throws IOException, WebApplicationException { + if (isOpenApiRequested()) { + OutputStream os = context.getOutputStream(); + CachedOutputStream cos = new CachedOutputStream(); + context.setOutputStream(cos); + context.proceed(); + String swaggerJson = IOUtils.readStringFromStream(cos.getInputStream()); + String openApiJson = SwaggerToOpenApiConversionUtils.getOpenApiFromSwaggerJson( + createMessageContext(), swaggerJson, openApiConfig); + os.write(StringUtils.toBytesUTF8(openApiJson)); + os.flush(); + } else { + context.proceed(); + } + } + + private MessageContext createMessageContext() { + return JAXRSUtils.createContextValue( + JAXRSUtils.getCurrentMessage(), null, MessageContext.class); + } + + private boolean isOpenApiRequested() { + return Objects.equals(Boolean.TRUE, JAXRSUtils.getCurrentMessage().getExchange().get(OPEN_API_PROPERTY)); + } + + public void setOpenApiJsonPath(final String openApiJsonPath) { + this.openApiJsonPath = openApiJsonPath; + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/fa079531/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/openapi/SwaggerToOpenApiConversionUtils.java ---------------------------------------------------------------------- diff --git a/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/openapi/SwaggerToOpenApiConversionUtils.java b/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/openapi/SwaggerToOpenApiConversionUtils.java new file mode 100644 index 0000000..87f127b --- /dev/null +++ b/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/openapi/SwaggerToOpenApiConversionUtils.java @@ -0,0 +1,405 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.syncope.core.rest.cxf.openapi; + +import java.io.IOException; +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import javax.ws.rs.core.MediaType; +import org.apache.cxf.common.util.StringUtils; +import org.apache.cxf.helpers.CastUtils; +import org.apache.cxf.jaxrs.ext.MessageContext; +import org.apache.cxf.jaxrs.json.basic.JsonMapObject; +import org.apache.cxf.jaxrs.json.basic.JsonMapObjectReaderWriter; +import org.apache.cxf.jaxrs.swagger.openapi.OpenApiConfiguration; + +public final class SwaggerToOpenApiConversionUtils { + + private static final List<String> SIMPLE_TYPE_RELATED_PROPS = + Arrays.asList("format", "minimum", "maximum", "default"); + + private SwaggerToOpenApiConversionUtils() { + // private constructor for static utility class + } + + public static String getOpenApiFromSwaggerJson( + final MessageContext ctx, + final String json, + final OpenApiConfiguration oac) throws IOException { + + JsonMapObjectReaderWriter readerWriter = new JsonMapObjectReaderWriter(); + JsonMapObject sw2 = readerWriter.fromJsonToJsonObject(json); + JsonMapObject sw3 = new JsonMapObject(); + + // "openapi" + sw3.setProperty("openapi", "3.0.1"); + + // "servers" + setServersProperty(ctx, sw2, sw3); + + // "info" + JsonMapObject infoObject = sw2.getJsonMapProperty("info"); + if (infoObject != null) { + sw3.setProperty("info", infoObject); + } + + // "tags" + List<Map<String, Object>> tagsObject = sw2.getListMapProperty("tags"); + if (tagsObject != null) { + sw3.setProperty("tags", tagsObject); + } + + // paths + Map<String, JsonMapObject> requestBodies = oac != null && oac.isCreateRequestBodies() + ? new LinkedHashMap<String, JsonMapObject>() : null; + setPathsProperty(sw2, sw3, requestBodies); + + // components + setComponentsProperty(sw2, sw3, requestBodies); + + // externalDocs + Object externalDocsObject = sw2.getProperty("externalDocs"); + if (externalDocsObject != null) { + sw3.setProperty("externalDocs", externalDocsObject); + } + + return readerWriter.toJson(sw3); + } + + private static void setComponentsProperty( + final JsonMapObject sw2, + final JsonMapObject sw3, + final Map<String, JsonMapObject> requestBodies) { + + JsonMapObject comps = new JsonMapObject(); + JsonMapObject requestBodiesObj = new JsonMapObject(); + if (requestBodies != null) { + for (Map.Entry<String, JsonMapObject> entry : requestBodies.entrySet()) { + requestBodiesObj.setProperty(entry.getKey(), entry.getValue()); + } + } + comps.setProperty("requestBodies", requestBodiesObj); + + JsonMapObject s2Defs = sw2.getJsonMapProperty("definitions"); + if (s2Defs != null) { + for (Object schema : s2Defs.asMap().values()) { + if (schema instanceof Map) { + @SuppressWarnings("unchecked") + Map<String, Object> schemaMap = (Map<String, Object>) schema; + Object discriminator = schemaMap.get("discriminator"); + if (discriminator != null) { + schemaMap.put("discriminator", new JsonMapObject( + Collections.singletonMap("propertyName", discriminator))); + } + } + } + + comps.setProperty("schemas", s2Defs); + } + JsonMapObject s2SecurityDefs = sw2.getJsonMapProperty("securityDefinitions"); + if (s2SecurityDefs != null) { + comps.setProperty("securitySchemes", s2SecurityDefs); + + for (String property : s2SecurityDefs.asMap().keySet()) { + JsonMapObject securityScheme = s2SecurityDefs.getJsonMapProperty(property); + if ("basic".equals(securityScheme.getStringProperty("type"))) { + securityScheme.setProperty("type", "http"); + securityScheme.setProperty("scheme", "basic"); + } + } + } + + sw3.setProperty("components", comps); + } + + private static void setPathsProperty( + final JsonMapObject sw2, + final JsonMapObject sw3, + final Map<String, JsonMapObject> requestBodies) { + + JsonMapObject sw2Paths = sw2.getJsonMapProperty("paths"); + for (Map.Entry<String, Object> sw2PathEntries : sw2Paths.asMap().entrySet()) { + Map<String, Object> map1 = CastUtils.cast((Map<?, ?>) sw2PathEntries.getValue()); + JsonMapObject sw2PathVerbs = new JsonMapObject(map1); + for (Map.Entry<String, Object> sw2PathVerbEntries : sw2PathVerbs.asMap().entrySet()) { + Map<String, Object> map2 = CastUtils.cast((Map<?, ?>) sw2PathVerbEntries.getValue()); + JsonMapObject sw2PathVerbProps = new JsonMapObject(map2); + + prepareRequestBody(sw2PathVerbProps, requestBodies); + prepareResponses(sw2PathVerbProps); + + } + } + + sw3.setProperty("paths", sw2Paths); + } + + private static void prepareResponses(final JsonMapObject sw2PathVerbProps) { + List<String> sw2PathVerbProduces = CastUtils.cast((List<?>) sw2PathVerbProps.removeProperty("produces")); + + JsonMapObject sw2PathVerbResps = sw2PathVerbProps.getJsonMapProperty("responses"); + if (sw2PathVerbResps != null) { + JsonMapObject sw3PathVerbResps = new JsonMapObject(); + + for (Map.Entry<String, Object> entry : sw2PathVerbResps.asMap().entrySet()) { + JsonMapObject v2Resp = new JsonMapObject(sw2PathVerbResps.getMapProperty(entry.getKey())); + JsonMapObject v3Resp = new JsonMapObject(); + String description = v2Resp.getStringProperty("description"); + if (description != null) { + v3Resp.setProperty("description", description); + } + JsonMapObject schema = v2Resp.getJsonMapProperty("schema"); + if (schema != null) { + JsonMapObject content = prepareContentFromSchema(schema, sw2PathVerbProduces, false); + if (content != null) { + v3Resp.setProperty("content", content); + } + + } + JsonMapObject headers = v2Resp.getJsonMapProperty("headers"); + if (headers != null) { + for (Map.Entry<String, Object> header : headers.asMap().entrySet()) { + JsonMapObject headerObj = new JsonMapObject(headers.getMapProperty(header.getKey())); + String type = headerObj.getStringProperty("type"); + if (type != null) { + JsonMapObject headerSchema = new JsonMapObject(); + headerSchema.setProperty("type", type); + headerObj.removeProperty("type"); + headerObj.setProperty("schema", headerSchema); + } + } + v3Resp.setProperty("headers", headers); + } + sw3PathVerbResps.setProperty(entry.getKey(), v3Resp); + } + + sw2PathVerbProps.setProperty("responses", sw3PathVerbResps); + } + } + + private static void prepareRequestBody( + final JsonMapObject sw2PathVerbProps, + final Map<String, JsonMapObject> requestBodies) { + + List<String> sw2PathVerbConsumes = CastUtils.cast((List<?>) sw2PathVerbProps.removeProperty("consumes")); + + JsonMapObject sw3RequestBody = null; + List<JsonMapObject> sw3formBody = null; + List<Map<String, Object>> sw2PathVerbParamsList = sw2PathVerbProps.getListMapProperty("parameters"); + if (sw2PathVerbParamsList != null) { + for (Iterator<Map<String, Object>> it = sw2PathVerbParamsList.iterator(); it.hasNext();) { + JsonMapObject sw2PathVerbParamMap = new JsonMapObject(it.next()); + sw2PathVerbParamMap.removeProperty("pattern"); + if ("body".equals(sw2PathVerbParamMap.getStringProperty("in"))) { + it.remove(); + + sw3RequestBody = new JsonMapObject(); + String description = sw2PathVerbParamMap.getStringProperty("description"); + if (description != null) { + sw3RequestBody.setProperty("description", description); + } + Boolean required = sw2PathVerbParamMap.getBooleanProperty("required"); + if (required != null) { + sw3RequestBody.setProperty("required", required); + } + JsonMapObject schema = sw2PathVerbParamMap.getJsonMapProperty("schema"); + if (schema != null) { + JsonMapObject content = prepareContentFromSchema(schema, sw2PathVerbConsumes, + requestBodies != null); + if (content != null) { + sw3RequestBody.setProperty("content", content); + } + + } + } else if ("formData".equals(sw2PathVerbParamMap.getStringProperty("in"))) { + it.remove(); + if (sw3formBody == null) { + sw3formBody = new LinkedList<>(); + sw3RequestBody = new JsonMapObject(); + } + sw2PathVerbParamMap.removeProperty("in"); + sw2PathVerbParamMap.removeProperty("required"); + sw3formBody.add(sw2PathVerbParamMap); + } else if ("array".equals(sw2PathVerbParamMap.getStringProperty("type"))) { + sw2PathVerbParamMap.removeProperty("type"); + sw2PathVerbParamMap.removeProperty("collectionFormat"); + sw2PathVerbParamMap.setProperty("explode", true); + JsonMapObject items = sw2PathVerbParamMap.getJsonMapProperty("items"); + sw2PathVerbParamMap.removeProperty("items"); + JsonMapObject schema = new JsonMapObject(); + schema.setProperty("type", "array"); + schema.setProperty("items", items); + sw2PathVerbParamMap.setProperty("schema", schema); + } else { + if ("matrix".equals(sw2PathVerbParamMap.getStringProperty("in"))) { + sw2PathVerbParamMap.removeProperty("in"); + sw2PathVerbParamMap.setProperty("in", "path"); + sw2PathVerbParamMap.setProperty("style", "matrix"); + } + + String type = (String) sw2PathVerbParamMap.removeProperty("type"); + Object enumK = sw2PathVerbParamMap.removeProperty("enum"); + if (type != null) { + JsonMapObject schema = new JsonMapObject(); + schema.setProperty("type", type); + if (enumK != null) { + schema.setProperty("enum", enumK); + } + for (String prop : SIMPLE_TYPE_RELATED_PROPS) { + Object value = sw2PathVerbParamMap.removeProperty(prop); + if (value != null) { + schema.setProperty(prop, value); + } + } + if ("password".equals(sw2PathVerbParamMap.getProperty("name"))) { + schema.setProperty("format", "password"); + } + sw2PathVerbParamMap.setProperty("schema", schema); + } + } + } + } + if (sw2PathVerbParamsList != null && sw2PathVerbParamsList.isEmpty()) { + sw2PathVerbProps.removeProperty("parameters"); + } + if (sw3RequestBody != null && sw3formBody != null) { + sw3RequestBody.setProperty("content", prepareFormContent(sw3formBody, sw2PathVerbConsumes)); + } + if (sw3RequestBody != null) { + if (requestBodies == null || sw3formBody != null) { + sw2PathVerbProps.setProperty("requestBody", sw3RequestBody); + } else { + JsonMapObject content = sw3RequestBody.getJsonMapProperty("content"); + if (content != null) { + String requestBodyName = (String) content.removeProperty("requestBodyName"); + if (requestBodyName != null) { + requestBodies.put(requestBodyName, sw3RequestBody); + String ref = "#/components/requestBodies/" + requestBodyName; + sw2PathVerbProps.setProperty("requestBody", + Collections.singletonMap("$ref", ref)); + } + } + } + } + } + + private static JsonMapObject prepareFormContent( + final List<JsonMapObject> formList, final List<String> mediaTypes) { + + String mediaType = StringUtils.isEmpty(mediaTypes) + ? MediaType.APPLICATION_FORM_URLENCODED : mediaTypes.get(0); + JsonMapObject content = new JsonMapObject(); + JsonMapObject formType = new JsonMapObject(); + JsonMapObject schema = new JsonMapObject(); + schema.setProperty("type", "object"); + JsonMapObject props = new JsonMapObject(); + for (JsonMapObject prop : formList) { + String name = (String) prop.removeProperty("name"); + props.setProperty(name, prop); + if ("file".equals(prop.getProperty("type"))) { + prop.setProperty("type", "string"); + if (!prop.containsProperty("format")) { + prop.setProperty("format", "binary"); + } + } + } + schema.setProperty("properties", props); + formType.setProperty("schema", schema); + content.setProperty(mediaType, formType); + return content; + } + + private static JsonMapObject prepareContentFromSchema( + final JsonMapObject schema, + final List<String> mediaTypes, + final boolean storeModelName) { + + String type = schema.getStringProperty("type"); + String modelName = null; + boolean isArray = false; + if (!"object".equals(type) || !"string".equals(type)) { + String ref; + JsonMapObject items = null; + if ("array".equals(type)) { + isArray = true; + items = schema.getJsonMapProperty("items"); + ref = (String) items.getProperty("$ref"); + } else { + ref = schema.getStringProperty("$ref"); + } + if (ref != null) { + int index = ref.lastIndexOf("/"); + modelName = ref.substring(index + 1); + if (items == null) { + schema.setProperty("$ref", "#/components/schemas/" + modelName); + } else { + items.setProperty("$ref", "#/components/schemas/" + modelName); + } + } + } + + JsonMapObject content = new JsonMapObject(); + + for (String mediaType : mediaTypes == null ? Arrays.asList(MediaType.APPLICATION_JSON) : mediaTypes) { + content.setProperty(mediaType, Collections.singletonMap("schema", schema)); + } + + if (modelName != null && storeModelName) { + content.setProperty("requestBodyName", isArray ? modelName + "Array" : modelName); + } + // pass the model name via the content object + return content; + } + + private static void setServersProperty( + final MessageContext ctx, + final JsonMapObject sw2, + final JsonMapObject sw3) { + + URI requestURI = ctx == null ? null : URI.create(ctx.getHttpServletRequest().getRequestURL().toString()); + + List<String> sw2Schemes = sw2.getListStringProperty("schemes"); + String sw2Scheme; + if (StringUtils.isEmpty(sw2Schemes)) { + if (requestURI == null) { + sw2Scheme = "https"; + } else { + sw2Scheme = requestURI.getScheme(); + } + } else { + sw2Scheme = sw2Schemes.get(0); + } + + String sw2Host = sw2.getStringProperty("host"); + if (sw2Host == null && requestURI != null) { + sw2Host = requestURI.getHost() + ":" + requestURI.getPort(); + } + + String sw2BasePath = sw2.getStringProperty("basePath"); + + String sw3ServerUrl = sw2Scheme + "://" + sw2Host + sw2BasePath; + sw3.setProperty("servers", Arrays.asList(Collections.singletonMap("url", sw3ServerUrl))); + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/fa079531/core/rest-cxf/src/main/resources/restCXFContext.xml ---------------------------------------------------------------------- diff --git a/core/rest-cxf/src/main/resources/restCXFContext.xml b/core/rest-cxf/src/main/resources/restCXFContext.xml index a31ec25..667c876 100644 --- a/core/rest-cxf/src/main/resources/restCXFContext.xml +++ b/core/rest-cxf/src/main/resources/restCXFContext.xml @@ -125,7 +125,7 @@ under the License. </map> </property> </bean> - <bean id="sw2OpenAPI" class="org.apache.cxf.jaxrs.swagger.openapi.SwaggerToOpenApiConversionFilter"/> + <bean id="sw2OpenAPI" class="org.apache.syncope.core.rest.cxf.openapi.SwaggerToOpenApiConversionFilter"/> <jaxrs:server id="restContainer" address="/" basePackages="org.apache.syncope.common.rest.api.service, org.apache.syncope.core.rest.cxf.service"
