/* * JBoss, Home of Professional Open Source. * See the COPYRIGHT.txt file distributed with this work for information * regarding copyright ownership. Some portions may be licensed * to Red Hat, Inc. under one or more contributor license agreements. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA. */ package org.teiid.translator.swagger; import java.io.File; import java.sql.Blob; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.teiid.core.types.DataTypeManager; import org.teiid.metadata.BaseColumn.NullType; import org.teiid.metadata.Column; import org.teiid.metadata.ExtensionMetadataProperty; import org.teiid.metadata.MetadataFactory; import org.teiid.metadata.Procedure; import org.teiid.metadata.ProcedureParameter; import org.teiid.metadata.ProcedureParameter.Type; import org.teiid.metadata.RestMetadataExtension; import org.teiid.translator.MetadataProcessor; import org.teiid.translator.TranslatorException; import org.teiid.translator.TranslatorProperty; import org.teiid.translator.TranslatorProperty.PropertyType; import org.teiid.translator.WSConnection; import org.teiid.translator.ws.BinaryWSProcedureExecution; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.models.HttpMethod; import io.swagger.models.Model; import io.swagger.models.ModelImpl; import io.swagger.models.Operation; import io.swagger.models.Path; import io.swagger.models.RefModel; import io.swagger.models.Response; import io.swagger.models.Scheme; import io.swagger.models.Swagger; import io.swagger.models.parameters.BodyParameter; import io.swagger.models.parameters.CookieParameter; import io.swagger.models.parameters.FormParameter; import io.swagger.models.parameters.HeaderParameter; import io.swagger.models.parameters.Parameter; import io.swagger.models.parameters.PathParameter; import io.swagger.models.parameters.QueryParameter; import io.swagger.models.properties.ArrayProperty; import io.swagger.models.properties.FileProperty; import io.swagger.models.properties.MapProperty; import io.swagger.models.properties.ObjectProperty; import io.swagger.models.properties.Property; import io.swagger.models.properties.RefProperty; import io.swagger.parser.SwaggerParser; public class SwaggerMetadataProcessor implements MetadataProcessor<WSConnection>{ public static final String KEY_NAME = "key_name"; public static final String KEY_VALUE = "key_value"; @ExtensionMetadataProperty(applicable=Procedure.class, datatype=String.class, display="URI", description="Used to define endpoint of the procedure", required=true) public final static String URI = RestMetadataExtension.URI; @ExtensionMetadataProperty(applicable=Procedure.class, datatype=String.class, display="Http Method", description="Http method used to execute the procedure", required=true, allowed="GET,POST,PUT,DELETE,OPTIONS,HEAD,PATCH") public final static String METHOD = RestMetadataExtension.METHOD; @ExtensionMetadataProperty(applicable=Procedure.class, datatype=String.class, display="Scheme", description="Scheme to use http, https etc.", allowed="HTTP,HTTPS") public final static String SCHEME = RestMetadataExtension.SCHEME; @ExtensionMetadataProperty(applicable=Procedure.class, datatype=String.class, display="Produces", description="Used to define content type produced by this procedure, default JSON assumed") public final static String PRODUCES = RestMetadataExtension.PRODUCES; @ExtensionMetadataProperty(applicable=Procedure.class, datatype=String.class, display="Consumes", description="Used to define content type consumed by this procedure with body type parameters. Default JSON assumed") public final static String CONSUMES = RestMetadataExtension.CONSUMES; @ExtensionMetadataProperty(applicable=Procedure.class, datatype=String.class, display="Charset", description="Encoding of the return data") public final static String CHARSET = RestMetadataExtension.CHARSET; @ExtensionMetadataProperty(applicable=ProcedureParameter.class, datatype=String.class, display="Parameter Type", description="Parameter type, as to how the parameter is being provided to the procedure", required=true, allowed="PATH,QUERY,FORM,FORMDATA,BODY,HEADER") public final static String PARAMETER_TYPE = RestMetadataExtension.PARAMETER_TYPE; @ExtensionMetadataProperty(applicable=ProcedureParameter.class, datatype=String.class, display="Collection Format", description="Determines the format of the array if type array is used, like CSV,TSV etc.", allowed="CSV,SSV,TSV,PIPES,MULTI") public final static String COLLECION_FORMAT = RestMetadataExtension.COLLECION_FORMAT; private String swaggerFilePath; private boolean useDefaultHost = true; private String preferredScheme; private String preferredProduces = "application/json"; private String preferredConsumes = "application/json"; private SwaggerExecutionFactory ef; public SwaggerMetadataProcessor(SwaggerExecutionFactory ef) { this.ef = ef; } @TranslatorProperty(display="Swagger metadata file path", category=PropertyType.IMPORT, description="Swagger metadata file path.") public String getSwaggerFilePath() { return swaggerFilePath; } public void setSwaggerFilePath(String swaggerFilePath) { this.swaggerFilePath = swaggerFilePath; } @TranslatorProperty(display="Use Host from Swagger File", category=PropertyType.IMPORT, description="Use default host specified in the Swagger file; Defaults to true") public boolean isUseDefaultHost() { return this.useDefaultHost; } public void setUseDefaultHost(boolean useDefault) { this.useDefaultHost = useDefault; } @TranslatorProperty(display="Preferred Scheme", category=PropertyType.IMPORT, description="Preferred Scheme to use when Swagger file supports multiple invocation schemes like http, https etc.") public String getPreferredScheme() { return this.preferredScheme; } public void setPreferredScheme(String scheme) { this.preferredScheme = scheme; } @TranslatorProperty(display="Preferred Accept Header", category=PropertyType.IMPORT, description="Preferred Accept MIME type header, this should be one of the Swagger " + "'produces' types; default is application/json") public String getPreferredProduces() { return this.preferredProduces; } public void setPreferredProduces(String accept) { this.preferredProduces = accept; } @TranslatorProperty(display="Preferred Content-type Header", category=PropertyType.IMPORT, description="Preferred Content-type header, this should be one of the Swagger 'consume' " + "types, default is application/json") public String getPreferredConsumes() { return this.preferredConsumes; } public void setPreferredConsumes(String type) { this.preferredConsumes = type; } @Override public void process(MetadataFactory mf, WSConnection connection) throws TranslatorException { Swagger swagger = getSchema(connection); String basePath = swagger.getBasePath(); String scheme = null; if(swagger.getSchemes().size() > 0) { if (this.preferredScheme == null) { scheme = swagger.getSchemes().get(0).toValue(); } else { for (Scheme s : swagger.getSchemes()) { if (s.toValue().equalsIgnoreCase(this.preferredScheme)) { scheme = s.toValue(); break; } } } } String httpHost = null; if(swagger.getHost() != null && !swagger.getHost().trim().isEmpty()) { //$NON-NLS-1$ httpHost = scheme + "://" + swagger.getHost();//$NON-NLS-1$ } if (this.useDefaultHost && httpHost != null) { httpHost = httpHost + basePath; } for(Entry<String, Path> entry : swagger.getPaths().entrySet()) { addProcedure(mf, swagger, ((httpHost != null) ? httpHost : basePath), entry.getKey(), entry.getValue()); } } private String buildURL(String basePath, String endpoint) { if (endpoint.startsWith("/")) { if (basePath.endsWith("/")) { return basePath+endpoint.substring(1); } else { return basePath+endpoint; } } if (basePath.endsWith("/")) { return basePath+endpoint; } return basePath+"/"+endpoint; } private String getSchemes(Operation op) { StringBuilder sb = new StringBuilder(); for (Scheme s:op.getSchemes()) { if (sb.length() > 0) { sb.append(','); } sb.append(s.name()); } return sb.toString(); } private void addProcedure(MetadataFactory mf, Swagger swagger, String basePath, String endpoint, Path operations) throws TranslatorException { for(Entry<HttpMethod, Operation> entry : operations.getOperationMap().entrySet()){ Operation operation = entry.getValue(); String produces = getTypes(operation.getProduces(), getPreferredProduces()); String consumes = getTypes(operation.getConsumes(), getPreferredConsumes()); Procedure procedure = mf.addProcedure(operation.getOperationId()); procedure.setVirtual(false); procedure.setProperty(METHOD, entry.getKey().name()); if (operation.getSchemes() != null && !operation.getSchemes().isEmpty()) { procedure.setProperty(SCHEME, getSchemes(operation)); } procedure.setProperty(URI, buildURL(basePath, endpoint)); procedure.setProperty(PRODUCES, produces); procedure.setProperty(CONSUMES, consumes); procedure.setAnnotation(getOperationSummary(operation)); for (Entry<String, Object> extension:operation.getVendorExtensions().entrySet()) { procedure.setProperty(extension.getKey(), extension.getValue().toString()); } addProcedureParameters(mf, swagger, procedure, operation); boolean returnAdded = false; Map<String, Response> respMap = operation.getResponses(); for (String code : respMap.keySet()) { if (code.equalsIgnoreCase("default")) { continue; } int httpCode = Integer.valueOf(code); // Success codes if (httpCode > 100 && httpCode < 300) { Response resp = respMap.get(code); returnAdded = buildResponse(mf, swagger, procedure, resp); break; } } if (!returnAdded && respMap.get("default") != null) { Response resp = respMap.get("default"); returnAdded = buildResponse(mf, swagger, procedure, resp); } } } private boolean buildResponse(final MetadataFactory mf, final Swagger swagger, final Procedure procedure, final Response resp) throws TranslatorException { PropertyAction pa = new PropertyAction() { @Override public void execute(String name, String nameInSource, Property property, boolean array) { String type = SwaggerTypeManager.teiidType(property.getType(), property.getFormat(), array); Column c = mf.addProcedureResultSetColumn(name, type, procedure); if (!name.equalsIgnoreCase(nameInSource)) { c.setNameInSource(nameInSource); } } }; Property schema = resp.getSchema(); if (schema != null) { if (isSimple(schema)) { boolean array = false; if (schema instanceof ArrayProperty) { schema = ((ArrayProperty)schema).getItems(); array = true; } if(resp.getHeaders() == null|| resp.getHeaders().isEmpty()) { String type = SwaggerTypeManager.teiidType(schema.getType(), schema.getFormat(), array); mf.addProcedureParameter("return", type, ProcedureParameter.Type.ReturnValue, procedure); } else { HashMap<String, Property> properties = new HashMap<String, Property>(); properties.put("return", schema); walkProperties(swagger, properties, null, null, pa); } } else { // since the return is always a collection unwrap the array without any issues. if (schema instanceof ArrayProperty) { schema = ((ArrayProperty)schema).getItems(); } if (schema instanceof ObjectProperty) { walkProperties(swagger, ((ObjectProperty)schema).getProperties(), null, null, pa); } else if (schema instanceof RefProperty) { String modelName = ((RefProperty)schema).getSimpleRef(); Model model = swagger.getDefinitions().get(modelName); walkProperties(swagger, model.getProperties(), null, null, pa); } else if (schema instanceof MapProperty){ Property property = ((MapProperty)schema).getAdditionalProperties(); String type = SwaggerTypeManager.teiidType(property.getType(), property.getFormat(), false); Column c = mf.addProcedureResultSetColumn(KEY_NAME, "string", procedure); c.setNameInSource(KEY_NAME); c = mf.addProcedureResultSetColumn(KEY_VALUE, type, procedure); c.setNameInSource(KEY_VALUE); } else { throw new TranslatorException("File properties are not supported"); } } } Map<String, Property> headers = resp.getHeaders(); if (headers != null && !headers.isEmpty()) { walkProperties(swagger, headers, null, null, pa); } return procedure.getResultSet() != null; } private boolean isSimple(Property property) { if (property instanceof ArrayProperty) { ArrayProperty ap = (ArrayProperty)property; return isSimple(ap.getItems()); } else if (property instanceof RefProperty) { return false; } else if (property instanceof ObjectProperty) { return false; } else if (property instanceof MapProperty) { return false; } else if (property instanceof FileProperty) { return false; } return true; } interface PropertyAction { void execute(String name, String nameInSource, Property property, boolean array); } private void walkProperties(final Swagger swagger, final Map<String,Property> properties, final String namePrefix, final String nisPrefix, final PropertyAction pa) { final PropertyVisitor visitor = new PropertyVisitor() { @Override public void visit(String name, Property property) { pa.execute(fqn(namePrefix,name), nis(nisPrefix, name, false), property, false); } @Override public void visit(String name, ArrayProperty property) { if (isSimple(property)) { // the array type defined in the type of the property pa.execute(fqn(namePrefix,name), nis(nisPrefix,name, false), property.getItems(), true); } else { // if Object or Ref, array does not matter as return is already a resultset. Property items = property.getItems(); if (items instanceof ObjectProperty) { String modelName = ((ObjectProperty)items).getName(); walkProperties(swagger, ((ObjectProperty) items).getProperties(), fqn(fqn(namePrefix, name), modelName), nis(nis(nisPrefix, name, true), modelName, false), pa); } else if (items instanceof RefProperty) { String modelName = ((RefProperty)items).getSimpleRef(); Model model = swagger.getDefinitions().get(modelName); walkProperties(swagger, model.getProperties(), fqn(fqn(namePrefix, name), modelName), nis(nis(nisPrefix, name, true), modelName, false), pa); } else { walkProperties(swagger, properties, fqn(namePrefix, name), nis(nisPrefix, name, true), pa); } } } @Override public void visit(String name, FileProperty property) { //TODO: } @Override public void visit(String name, MapProperty property) { //TODO: } @Override public void visit(String name, ObjectProperty property) { walkProperties(swagger, property.getProperties(), fqn(namePrefix, name), nis(nisPrefix, name, false), pa); } @Override public void visit(String name, RefProperty property) { Model model = swagger.getDefinitions().get(property.getSimpleRef()); walkProperties(swagger, model.getProperties(), fqn(namePrefix, name), nis(nisPrefix, name, false), pa); } }; for (Entry<String, Property> p:properties.entrySet()) { visitor.accept(p.getKey(), p.getValue()); } } private String fqn(String prefix, String name) { return prefix == null?name:prefix+"_"+name; } private String nis(String prefix, String name, boolean array) { String nis = prefix == null?name:prefix+"/"+name; if (array) { nis = nis+"[]"; } return nis; } private void addProcedureParameters(final MetadataFactory mf, final Swagger swagger, final Procedure procedure, final Operation operation) throws TranslatorException { for(final Parameter parameter : operation.getParameters()) { if (parameter instanceof BodyParameter) { PropertyAction pa = new PropertyAction() { @Override public void execute(String name, String nameInSource, Property property, boolean array) { String type = SwaggerTypeManager.teiidType(property.getType(), property.getFormat(), array); if (procedure.getParameterByName(nameInSource) == null) { ProcedureParameter param = mf.addProcedureParameter(name, type, Type.In, procedure); param.setProperty(PARAMETER_TYPE, parameter.getIn()); param.setNullType(property.getRequired()?NullType.No_Nulls:NullType.Nullable); param.setAnnotation(property.getDescription()); if (!name.equalsIgnoreCase(nameInSource)) { param.setNameInSource(nameInSource); } } } }; Model model = ((BodyParameter)parameter).getSchema(); if (model instanceof RefModel) { RefModel refModel = (RefModel)model; if (refModel.getProperties() != null) { walkProperties(swagger, refModel.getProperties(), null, null, pa); } else if (refModel.getReference() != null) { Model m = swagger.getDefinitions().get(refModel.getSimpleRef()); walkProperties(swagger, m.getProperties(), null, null, pa); } break; } else { if ((model instanceof ModelImpl) && model.getProperties() != null) { walkProperties(swagger, model.getProperties(), null, null, pa); } else { ProcedureParameter p = mf.addProcedureParameter( parameter.getName(), DataTypeManager.DefaultDataTypes.CLOB, Type.In, procedure); p.setProperty(PARAMETER_TYPE, parameter.getIn()); p.setNullType(NullType.No_Nulls); p.setAnnotation(parameter.getDescription()); } } } else { String name = parameter.getName(); ProcedureParameter pp = null; String type = null; String defaultValue = null; String collectionFormat = null; if(parameter instanceof PathParameter) { PathParameter p = (PathParameter) parameter; type = p.getType(); if (p.getType().equalsIgnoreCase("array")){ Property ap = p.getItems(); type = ap.getType(); } type = SwaggerTypeManager.teiidType(type, p.getFormat(), p.getItems() != null); defaultValue = p.getDefaultValue(); collectionFormat = p.getCollectionFormat(); } else if(parameter instanceof QueryParameter) { QueryParameter p = (QueryParameter) parameter; type = p.getType(); if (p.getType().equalsIgnoreCase("array")){ Property ap = p.getItems(); type = ap.getType(); } type = SwaggerTypeManager.teiidType(type, p.getFormat(), p.getItems() != null); defaultValue = p.getDefaultValue(); collectionFormat = p.getCollectionFormat(); } else if (parameter instanceof FormParameter) { FormParameter p = (FormParameter) parameter; type = p.getType(); if (p.getType().equalsIgnoreCase("array")){ Property ap = p.getItems(); type = ap.getType(); } type = SwaggerTypeManager.teiidType(type, p.getFormat(), p.getItems() != null); defaultValue = p.getDefaultValue(); collectionFormat = p.getCollectionFormat(); } else if (parameter instanceof HeaderParameter) { HeaderParameter p = (HeaderParameter)parameter; type = p.getType(); if (p.getType().equalsIgnoreCase("array")){ Property ap = p.getItems(); type = ap.getType(); } type = SwaggerTypeManager.teiidType(type, p.getFormat(), p.getItems() != null); defaultValue = p.getDefaultValue(); collectionFormat = p.getCollectionFormat(); } else if (parameter instanceof CookieParameter) { CookieParameter p = (CookieParameter) parameter; type = p.getType(); if (p.getType().equalsIgnoreCase("array")){ Property ap = p.getItems(); type = ap.getType(); } type = SwaggerTypeManager.teiidType(type, p.getFormat(), p.getItems() != null); defaultValue = p.getDefaultValue(); collectionFormat = p.getCollectionFormat(); } pp = mf.addProcedureParameter(name, type, Type.In, procedure); pp.setProperty(PARAMETER_TYPE, parameter.getIn()); boolean required = parameter.getRequired(); pp.setNullType(required ? NullType.No_Nulls : NullType.Nullable); pp.setAnnotation(parameter.getDescription()); if (defaultValue != null) { pp.setDefaultValue(defaultValue); } if (collectionFormat != null) { pp.setProperty(COLLECION_FORMAT, collectionFormat); } // extended properties for (Entry<String, Object> extension:parameter.getVendorExtensions().entrySet()) { pp.setProperty(extension.getKey(), extension.getValue().toString()); } } } } private String getOperationSummary(Operation operation) { String description = operation.getDescription(); if(description == null || description.equals("")) { //$NON-NLS-1$ description = operation.getSummary(); } return description; } private String getTypes(List<String> types, String preferred) { String selected = null; if(types != null && !types.isEmpty()) { for(String type : types){ if (preferred != null) { if (preferred.equalsIgnoreCase(type)) { selected = preferred; } } } if (selected == null) { selected = types.get(0); } } return selected; } protected Swagger getSchema(WSConnection conn) throws TranslatorException { Swagger swagger = null; try { String swaggerFile = getSwaggerFilePath(); if( swaggerFile != null && !swaggerFile.isEmpty()) { File f = new File(swaggerFile); if(f == null || !f.exists() || !f.isFile()) { throw new TranslatorException(SwaggerPlugin.Event.TEIID28019, SwaggerPlugin.Util.gs(SwaggerPlugin.Event.TEIID28019, swaggerFile)); } SwaggerParser parser = new SwaggerParser(); swagger = parser.read(f.getAbsolutePath()); } else { BaseQueryExecution execution = new BaseQueryExecution(this.ef, null, null, conn); Map<String, List<String>> headers = new HashMap<String, List<String>>(); BinaryWSProcedureExecution call = execution.buildInvokeHTTP("GET", "swagger.json", null, headers); //$NON-NLS-1$ //$NON-NLS-2$ call.execute(); if (call.getResponseCode() != 200) { throw new TranslatorException(SwaggerPlugin.Event.TEIID28015, SwaggerPlugin.Util.gs(SwaggerPlugin.Event.TEIID28015,call.getResponseCode())); } Blob out = (Blob)call.getOutputParameterValues().get(0); ObjectMapper objectMapper = new ObjectMapper(); JsonNode rootNode = objectMapper.readTree(out.getBinaryStream()); swagger = new SwaggerParser().read(rootNode); } } catch (Exception e) { throw new TranslatorException(SwaggerPlugin.Event.TEIID28016, e, SwaggerPlugin.Util.gs(SwaggerPlugin.Event.TEIID28016, e)); } return swagger; } }