blob: fe32ed2667aa6f87e70e555f2ad42a5026d37bdb [file] [log] [blame]
/*
* 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.camel.swagger;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.management.ManagementFactory;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.management.AttributeNotFoundException;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import org.w3c.dom.Document;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import io.swagger.jaxrs.config.BeanConfig;
import io.swagger.models.Contact;
import io.swagger.models.Info;
import io.swagger.models.License;
import io.swagger.models.Scheme;
import io.swagger.models.Swagger;
import io.swagger.util.Json;
import io.swagger.util.Yaml;
import org.apache.camel.CamelContext;
import org.apache.camel.Exchange;
import org.apache.camel.model.Model;
import org.apache.camel.model.ModelHelper;
import org.apache.camel.model.rest.RestDefinition;
import org.apache.camel.model.rest.RestsDefinition;
import org.apache.camel.spi.ClassResolver;
import org.apache.camel.spi.RestConfiguration;
import org.apache.camel.support.PatternHelper;
import org.apache.camel.util.CamelVersionHelper;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.URISupport;
import org.apache.camel.util.xml.XmlLineNumberParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.camel.swagger.SwaggerHelper.clearVendorExtensions;
/**
* A support class for that allows SPI to plugin
* and offer Swagger API service listings as part of the Camel component. This allows rest-dsl components
* such as servlet/jetty/netty-http to offer Swagger API listings with minimal effort.
*/
public class RestSwaggerSupport {
static final String HEADER_X_FORWARDED_PREFIX = "X-Forwarded-Prefix";
static final String HEADER_X_FORWARDED_HOST = "X-Forwarded-Host";
static final String HEADER_X_FORWARDED_PROTO = "X-Forwarded-Proto";
static final String HEADER_HOST = "Host";
private static final Logger LOG = LoggerFactory.getLogger(RestSwaggerSupport.class);
private RestSwaggerReader reader = new RestSwaggerReader();
private boolean cors;
public void initSwagger(BeanConfig swaggerConfig, Map<String, Object> config) {
// configure swagger options
String s = (String) config.get("swagger.version");
if (s != null) {
swaggerConfig.setVersion(s);
}
s = (String) config.get("base.path");
if (s != null) {
swaggerConfig.setBasePath(s);
}
s = (String) config.get("host");
if (s != null) {
swaggerConfig.setHost(s);
}
s = (String) config.get("cors");
if (s != null) {
cors = "true".equalsIgnoreCase(s);
}
s = (String) config.get("schemes");
if (s == null) {
// deprecated due typo
s = (String) config.get("schemas");
}
if (s != null) {
String[] schemes = s.split(",");
swaggerConfig.setSchemes(schemes);
} else {
// assume http by default
swaggerConfig.setSchemes(new String[]{"http"});
}
String version = (String) config.get("api.version");
String title = (String) config.get("api.title");
String description = (String) config.get("api.description");
String termsOfService = (String) config.get("api.termsOfService");
String licenseName = (String) config.get("api.license.name");
String licenseUrl = (String) config.get("api.license.url");
String contactName = (String) config.get("api.contact.name");
String contactUrl = (String) config.get("api.contact.url");
String contactEmail = (String) config.get("api.contact.email");
Info info = new Info();
info.setVersion(version);
info.setTitle(title);
info.setDescription(description);
info.setTermsOfService(termsOfService);
if (licenseName != null || licenseUrl != null) {
License license = new License();
license.setName(licenseName);
license.setUrl(licenseUrl);
info.setLicense(license);
}
if (contactName != null || contactUrl != null || contactEmail != null) {
Contact contact = new Contact();
contact.setName(contactName);
contact.setUrl(contactUrl);
contact.setEmail(contactEmail);
info.setContact(contact);
}
swaggerConfig.setInfo(info);
}
public List<RestDefinition> getRestDefinitions(CamelContext camelContext) throws Exception {
Model model = camelContext.getExtension(Model.class);
List<RestDefinition> rests = model.getRestDefinitions();
if (rests.isEmpty()) {
return null;
}
// use a routes definition to dump the rests
RestsDefinition def = new RestsDefinition();
def.setRests(rests);
String xml = ModelHelper.dumpModelAsXml(camelContext, def);
// if resolving placeholders we parse the xml, and resolve the property placeholders during parsing
final AtomicBoolean changed = new AtomicBoolean();
InputStream is = new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8));
Document dom = XmlLineNumberParser.parseXml(is, new XmlLineNumberParser.XmlTextTransformer() {
@Override
public String transform(String text) {
try {
String after = camelContext.resolvePropertyPlaceholders(text);
if (!changed.get()) {
changed.set(!text.equals(after));
}
return after;
} catch (Exception e) {
// ignore
return text;
}
}
});
// okay there were some property placeholder replaced so re-create the model
if (changed.get()) {
xml = camelContext.getTypeConverter().mandatoryConvertTo(String.class, dom);
def = ModelHelper.createModelFromXml(camelContext, xml, RestsDefinition.class);
if (def != null) {
return def.getRests();
}
}
return rests;
}
public List<RestDefinition> getRestDefinitions(String camelId) throws Exception {
ObjectName found = null;
boolean supportResolvePlaceholder = false;
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
Set<ObjectName> names = server.queryNames(new ObjectName("org.apache.camel:type=context,*"), null);
for (ObjectName on : names) {
String id = on.getKeyProperty("name");
if (id.startsWith("\"") && id.endsWith("\"")) {
id = id.substring(1, id.length() - 1);
}
if (camelId == null || camelId.equals(id)) {
// filter out older Camel versions as this requires Camel 2.15 or better (rest-dsl)
String version = (String) server.getAttribute(on, "CamelVersion");
if (CamelVersionHelper.isGE("2.15.0", version)) {
found = on;
}
if (CamelVersionHelper.isGE("2.15.3", version)) {
supportResolvePlaceholder = true;
}
}
}
if (found != null) {
String xml;
if (supportResolvePlaceholder) {
xml = (String) server.invoke(found, "dumpRestsAsXml", new Object[]{true}, new String[]{"boolean"});
} else {
xml = (String) server.invoke(found, "dumpRestsAsXml", null, null);
}
if (xml != null) {
LOG.debug("DumpRestAsXml:\n{}", xml);
RestsDefinition rests = ModelHelper.createModelFromXml(null, xml, RestsDefinition.class);
if (rests != null) {
return rests.getRests();
}
}
}
return null;
}
public List<String> findCamelContexts() throws Exception {
List<String> answer = new ArrayList<>();
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
Set<ObjectName> names = server.queryNames(new ObjectName("*:type=context,*"), null);
for (ObjectName on : names) {
String id = on.getKeyProperty("name");
if (id.startsWith("\"") && id.endsWith("\"")) {
id = id.substring(1, id.length() - 1);
}
// filter out older Camel versions as this requires Camel 2.15 or better (rest-dsl)
try {
String version = (String) server.getAttribute(on, "CamelVersion");
if (CamelVersionHelper.isGE("2.15.0", version)) {
answer.add(id);
}
} catch (AttributeNotFoundException ex) {
// ignore
}
}
return answer;
}
public void renderResourceListing(CamelContext camelContext, RestApiResponseAdapter response, BeanConfig swaggerConfig, String contextId, String route, boolean json, boolean yaml,
Map<String, Object> headers, ClassResolver classResolver, RestConfiguration configuration) throws Exception {
LOG.trace("renderResourceListing");
ObjectMapper mapper = Json.mapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
if (cors) {
setupCorsHeaders(response, configuration.getCorsHeaders());
}
List<RestDefinition> rests = null;
if (camelContext != null && camelContext.getName().equals(contextId)) {
rests = getRestDefinitions(camelContext);
} else {
rests = getRestDefinitions(contextId);
}
if (rests != null) {
final Map<String, Object> apiProperties = configuration.getApiProperties() != null ? configuration.getApiProperties() : new HashMap<>();
if (json) {
response.setHeader(Exchange.CONTENT_TYPE, (String) apiProperties.getOrDefault("api.specification.contentType.json", "application/json"));
// read the rest-dsl into swagger model
Swagger swagger = reader.read(rests, route, swaggerConfig, contextId, classResolver);
if (configuration.isUseXForwardHeaders()) {
setupXForwardedHeaders(swagger, headers);
}
if (!configuration.isApiVendorExtension()) {
clearVendorExtensions(swagger);
}
byte[] bytes = mapper.writeValueAsBytes(swagger);
int len = bytes.length;
response.setHeader(Exchange.CONTENT_LENGTH, "" + len);
response.writeBytes(bytes);
} else {
response.setHeader(Exchange.CONTENT_TYPE, (String) apiProperties.getOrDefault("api.specification.contentType.yaml", "text/yaml"));
// read the rest-dsl into swagger model
Swagger swagger = reader.read(rests, route, swaggerConfig, contextId, classResolver);
if (configuration.isUseXForwardHeaders()) {
setupXForwardedHeaders(swagger, headers);
}
if (!configuration.isApiVendorExtension()) {
clearVendorExtensions(swagger);
}
byte[] jsonData = mapper.writeValueAsBytes(swagger);
// json to yaml
JsonNode node = mapper.readTree(jsonData);
byte[] bytes = Yaml.mapper().writerWithDefaultPrettyPrinter().writeValueAsBytes(node);
int len = bytes.length;
response.setHeader(Exchange.CONTENT_LENGTH, "" + len);
response.writeBytes(bytes);
}
} else {
response.noContent();
}
}
/**
* Renders a list of available CamelContexts in the JVM
*/
public void renderCamelContexts(RestApiResponseAdapter response, String contextId, String contextIdPattern, boolean json, boolean yaml,
RestConfiguration configuration) throws Exception {
LOG.trace("renderCamelContexts");
if (cors) {
setupCorsHeaders(response, configuration.getCorsHeaders());
}
List<String> contexts = findCamelContexts();
// filter non matched CamelContext's
if (contextIdPattern != null) {
Iterator<String> it = contexts.iterator();
while (it.hasNext()) {
String name = it.next();
boolean match;
if ("#name#".equals(contextIdPattern)) {
match = name.equals(contextId);
} else {
match = PatternHelper.matchPattern(name, contextIdPattern);
}
if (!match) {
it.remove();
}
}
}
StringBuffer sb = new StringBuffer();
if (json) {
response.setHeader(Exchange.CONTENT_TYPE, "application/json");
sb.append("[\n");
for (int i = 0; i < contexts.size(); i++) {
String name = contexts.get(i);
sb.append("{\"name\": \"").append(name).append("\"}");
if (i < contexts.size() - 1) {
sb.append(",\n");
}
}
sb.append("\n]");
} else {
response.setHeader(Exchange.CONTENT_TYPE, "text/yaml");
for (int i = 0; i < contexts.size(); i++) {
String name = contexts.get(i);
sb.append("- \"").append(name).append("\"\n");
}
}
int len = sb.length();
response.setHeader(Exchange.CONTENT_LENGTH, "" + len);
response.writeBytes(sb.toString().getBytes());
}
private static void setupCorsHeaders(RestApiResponseAdapter response, Map<String, String> corsHeaders) {
// use default value if none has been configured
String allowOrigin = corsHeaders != null ? corsHeaders.get("Access-Control-Allow-Origin") : null;
if (allowOrigin == null) {
allowOrigin = RestConfiguration.CORS_ACCESS_CONTROL_ALLOW_ORIGIN;
}
String allowMethods = corsHeaders != null ? corsHeaders.get("Access-Control-Allow-Methods") : null;
if (allowMethods == null) {
allowMethods = RestConfiguration.CORS_ACCESS_CONTROL_ALLOW_METHODS;
}
String allowHeaders = corsHeaders != null ? corsHeaders.get("Access-Control-Allow-Headers") : null;
if (allowHeaders == null) {
allowHeaders = RestConfiguration.CORS_ACCESS_CONTROL_ALLOW_HEADERS;
}
String maxAge = corsHeaders != null ? corsHeaders.get("Access-Control-Max-Age") : null;
if (maxAge == null) {
maxAge = RestConfiguration.CORS_ACCESS_CONTROL_MAX_AGE;
}
if (LOG.isTraceEnabled()) {
LOG.trace("Using CORS headers[");
LOG.trace(" Access-Control-Allow-Origin={}", allowOrigin);
LOG.trace(" Access-Control-Allow-Methods={}", allowMethods);
LOG.trace(" Access-Control-Allow-Headers={}", allowHeaders);
LOG.trace(" Access-Control-Max-Age={}", maxAge);
LOG.trace("]");
}
response.setHeader("Access-Control-Allow-Origin", allowOrigin);
response.setHeader("Access-Control-Allow-Methods", allowMethods);
response.setHeader("Access-Control-Allow-Headers", allowHeaders);
response.setHeader("Access-Control-Max-Age", maxAge);
}
static void setupXForwardedHeaders(Swagger swagger, Map<String, Object> headers) {
String host = (String) headers.get(HEADER_HOST);
if (ObjectHelper.isNotEmpty(host)) {
swagger.setHost(host);
}
String forwardedPrefix = (String) headers.get(HEADER_X_FORWARDED_PREFIX);
if (ObjectHelper.isNotEmpty(forwardedPrefix)) {
swagger.setBasePath(URISupport.joinPaths(forwardedPrefix, swagger.getBasePath()));
}
String forwardedHost = (String) headers.get(HEADER_X_FORWARDED_HOST);
if (ObjectHelper.isNotEmpty(forwardedHost)) {
swagger.setHost(forwardedHost);
}
String proto = (String) headers.get(HEADER_X_FORWARDED_PROTO);
if (ObjectHelper.isNotEmpty(proto)) {
String[] schemes = proto.split(",");
for (String scheme : schemes) {
String trimmedScheme = scheme.trim();
if (ObjectHelper.isNotEmpty(trimmedScheme)) {
swagger.addScheme(Scheme.forValue(trimmedScheme));
}
}
}
}
}