Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CAMEL-20520: camel-rest-openapi - Generate OpenApi scheme once on sta… #13402

Merged
merged 1 commit into from Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 2 additions & 3 deletions components/camel-openapi-java/src/main/docs/openapi-java.adoc
Expand Up @@ -174,9 +174,8 @@ is using OAuth security with permitted scopes of read and write pets.
== JSon or Yaml

The camel-openapi-java module supports both JSon and Yaml out of the
box. You can specify in the request url what you want returned by using
`/openapi.json` or `/openapi.yaml` for either one. If none is specified, then
the HTTP Accept header is used to detect if json or yaml can be
box. You can specify in the request url what you want by using `.json` or `.yaml` as suffix in the context-path
If none is specified, then the HTTP Accept header is used to detect if json or yaml can be
accepted. If either both are accepted or none was set as accepted, then
json is returned as the default format.

Expand Down
Expand Up @@ -17,29 +17,54 @@
package org.apache.camel.openapi;

import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;

import io.swagger.v3.oas.models.OpenAPI;
import org.apache.camel.Exchange;

public class ExchangeRestApiResponseAdapter implements RestApiResponseAdapter {
/**
* A {@link RestApiResponseAdapter} that caches the response.
*/
public class DefaultRestApiResponseAdapter implements RestApiResponseAdapter {

private final Map<String, String> headers = new LinkedHashMap<>();
private byte[] body;
private boolean noContent;
private OpenAPI openApi;

private final Exchange exchange;
public OpenAPI getOpenApi() {
return openApi;
}

public ExchangeRestApiResponseAdapter(Exchange exchange) {
this.exchange = exchange;
public void setOpenApi(OpenAPI openApi) {
this.openApi = openApi;
}

@Override
public void setHeader(String name, String value) {
exchange.getIn().setHeader(name, value);
headers.put(name, value);
}

@Override
public void writeBytes(byte[] bytes) throws IOException {
exchange.getIn().setBody(bytes);
this.body = bytes;
}

@Override
public void noContent() {
exchange.getIn().setHeader(Exchange.HTTP_RESPONSE_CODE, 204);
this.noContent = true;
}

public void copyResult(Exchange exchange) {
if (!headers.isEmpty()) {
exchange.getMessage().getHeaders().putAll(headers);
}
if (body != null) {
exchange.getMessage().setBody(body);
}
if (noContent) {
exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 204);
}
}
}
Expand Up @@ -78,6 +78,8 @@ public Processor createApiProcessor(
options.put("cors", "true");
}

return new RestOpenApiProcessor(options, configuration);
RestOpenApiProcessor answer = new RestOpenApiProcessor(options, configuration);
answer.setCamelContext(camelContext);
return answer;
}
}
Expand Up @@ -27,6 +27,7 @@
import io.swagger.v3.parser.OpenAPIV3Parser;
import io.swagger.v3.parser.core.models.SwaggerParseResult;
import org.apache.camel.CamelContext;
import org.apache.camel.CamelContextAware;
import org.apache.camel.Producer;
import org.apache.camel.spi.RestConfiguration;
import org.apache.camel.spi.RestProducerFactory;
Expand Down Expand Up @@ -90,7 +91,6 @@ OpenAPI loadOpenApiModel(String apiDoc) throws Exception {
final SwaggerParseResult openApi = openApiParser.readLocation(apiDoc, null, null);

if (openApi != null && openApi.getOpenAPI() != null) {
// checkV2specification(openApi.getOpenAPI(), uri);
return openApi.getOpenAPI();
}

Expand Down Expand Up @@ -161,22 +161,22 @@ private Producer createHttpProducer(
String basePath;
String uriTemplate;
if (host == null) {

//if no explicit host has been configured then use host and base path from the openApi api-doc
host = RestOpenApiSupport.getHostFromOasDocument(openApi);
basePath = RestOpenApiSupport.getBasePathFromOasDocument(openApi);
uriTemplate = path;

} else {
// path includes also uri template
basePath = path;
uriTemplate = null;
}

RestConfiguration config = CamelContextHelper.getRestConfiguration(camelContext, null, componentName);
return factory.createProducer(camelContext, host, verb, basePath, uriTemplate, queryParameters, consumes, produces,
Producer answer = factory.createProducer(camelContext, host, verb, basePath, uriTemplate, queryParameters, consumes,
produces,
config, parameters);

CamelContextAware.trySetCamelContext(answer, camelContext);
return answer;
} else {
throw new IllegalStateException("Cannot find RestProducerFactory in Registry or as a Component to use");
}
Expand Down
Expand Up @@ -18,11 +18,42 @@

import java.io.IOException;

import io.swagger.v3.oas.models.OpenAPI;
import org.apache.camel.Exchange;

/**
* Adapter for rendering API response
*/
public interface RestApiResponseAdapter {

/**
* Sets the generated OpenAPI model
*/
void setOpenApi(OpenAPI openApi);

/**
* Gets the generated OpenAPI model
*/
OpenAPI getOpenApi();

/**
* Adds a header
*/
void setHeader(String name, String value);

/**
* The content of the OpenAPI spec as byte array
*/
void writeBytes(byte[] bytes) throws IOException;

/**
* There is no Rest DSL and therefore no OpenAPI spec
*/
void noContent();

/**
* Copy content from this adapter into the given {@link Exchange}.
*/
void copyResult(Exchange exchange);

}
Expand Up @@ -19,16 +19,23 @@
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.RejectedExecutionException;

import org.apache.camel.CamelContext;
import org.apache.camel.CamelContextAware;
import org.apache.camel.Exchange;
import org.apache.camel.Processor;
import org.apache.camel.StartupStep;
import org.apache.camel.spi.RestConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.camel.spi.StartupStepRecorder;
import org.apache.camel.support.service.ServiceSupport;
import org.apache.camel.util.ObjectHelper;

public class RestOpenApiProcessor implements Processor {
public class RestOpenApiProcessor extends ServiceSupport implements Processor, CamelContextAware {

private static final Logger LOG = LoggerFactory.getLogger(RestOpenApiProcessor.class);
private final RestApiResponseAdapter jsonAdapter = new DefaultRestApiResponseAdapter();
private final RestApiResponseAdapter yamlAdapter = new DefaultRestApiResponseAdapter();
private CamelContext camelContext;
private final BeanConfig openApiConfig;
private final RestOpenApiSupport support;
private final RestConfiguration configuration;
Expand All @@ -45,20 +52,48 @@ public RestOpenApiProcessor(Map<String, Object> parameters,
support.initOpenApi(openApiConfig, parameters);
}

@Override
public CamelContext getCamelContext() {
return camelContext;
}

@Override
public void setCamelContext(CamelContext camelContext) {
this.camelContext = camelContext;
}

@Override
protected void doInit() throws Exception {
ObjectHelper.notNull(camelContext, "CamelContext", this);

StartupStepRecorder recorder = camelContext.getCamelContextExtension().getStartupStepRecorder();
StartupStep step = recorder.beginStep(RestOpenApiProcessor.class, "openapi", "Generating OpenAPI specification");
try {
support.renderResourceListing(camelContext, jsonAdapter, openApiConfig, true,
camelContext.getClassResolver(), configuration);
yamlAdapter.setOpenApi(jsonAdapter.getOpenApi()); // no need to compute OpenApi again
support.renderResourceListing(camelContext, yamlAdapter, openApiConfig, false,
camelContext.getClassResolver(), configuration);
} finally {
recorder.endStep(step);
}
}

@Override
public void process(Exchange exchange) throws Exception {
if (!isRunAllowed()) {
throw new RejectedExecutionException();
}

String route = exchange.getIn().getHeader(Exchange.HTTP_PATH, String.class);
String accept = exchange.getIn().getHeader("Accept", String.class);

RestApiResponseAdapter adapter = new ExchangeRestApiResponseAdapter(exchange);

// whether to use json or yaml
boolean json = false;
boolean yaml = false;
if (route != null && route.endsWith("/openapi.json")) {
if (route != null && route.endsWith(".json")) {
json = true;
} else if (route != null && route.endsWith("/openapi.yaml")) {
} else if (route != null && route.endsWith(".yaml")) {
yaml = true;
}
if (accept != null && !json && !yaml) {
Expand All @@ -70,12 +105,11 @@ public void process(Exchange exchange) throws Exception {
json = true;
}

try {
support.renderResourceListing(exchange.getContext(), adapter, openApiConfig, json,
exchange.getIn().getHeaders(), exchange.getContext().getClassResolver(), configuration);
} catch (Exception e) {
LOG.warn("Error rendering OpenApi API due {}", e.getMessage(), e);
RestApiResponseAdapter adapter = json ? jsonAdapter : yamlAdapter;
if (configuration.isUseXForwardHeaders()) {
support.setupXForwardHeaders(adapter, exchange);
}
adapter.copyResult(exchange);
}

}
Expand Up @@ -105,22 +105,17 @@ private static void setupCorsHeaders(RestApiResponseAdapter response, Map<String
}

static void setupXForwardedHeaders(OpenAPI openApi, Map<String, Object> headers) {

String basePath = getBasePathFromOasDocument(openApi);

String host = (String) headers.get(HEADER_HOST);

String forwardedPrefix = (String) headers.get(HEADER_X_FORWARDED_PREFIX);

if (ObjectHelper.isNotEmpty(forwardedPrefix)) {
basePath = URISupport.joinPaths(forwardedPrefix, basePath);
}

String forwardedHost = (String) headers.get(HEADER_X_FORWARDED_HOST);
if (ObjectHelper.isNotEmpty(forwardedHost)) {
host = forwardedHost;
}

String proto = (String) headers.get(HEADER_X_FORWARDED_PROTO);
if (openApi.getServers() != null) {
openApi.getServers().clear();
Expand Down Expand Up @@ -290,35 +285,39 @@ protected RestDefinitionsResolver createJmxRestDefinitionsResolver(CamelContext
() -> new IllegalArgumentException("Cannot find camel-openapi-java on classpath."));
}

public void setupXForwardHeaders(RestApiResponseAdapter response, Exchange exchange) {
setupXForwardedHeaders(response.getOpenApi(), exchange.getMessage().getHeaders());
}

public void renderResourceListing(
CamelContext camelContext, RestApiResponseAdapter response,
BeanConfig openApiConfig, boolean json,
Map<String, Object> headers, ClassResolver classResolver,
ClassResolver classResolver,
RestConfiguration configuration)
throws Exception {

LOG.trace("renderResourceListing");

if (cors) {
setupCorsHeaders(response, configuration.getCorsHeaders());
}

List<RestDefinition> rests = getRestDefinitions(camelContext);

if (rests != null) {
final Map<String, Object> apiProperties = configuration.getApiProperties() != null
? configuration.getApiProperties() : new HashMap<>();
String key = json ? "api.specification.contentType.json" : "api.specification.contentType.yaml";
String defaultValue = json ? "application/json" : "text/yaml";
response.setHeader(Exchange.CONTENT_TYPE, (String) apiProperties.getOrDefault(key, defaultValue));
// read the rest-dsl into openApi model
OpenAPI openApi = reader.read(
camelContext, rests, openApiConfig, camelContext.getName(), classResolver);
if (configuration.isUseXForwardHeaders()) {
setupXForwardedHeaders(openApi, headers);
}
if (!configuration.isApiVendorExtension()) {
clearVendorExtensions(openApi);
OpenAPI openApi = response.getOpenApi();
if (openApi == null) {
openApi = reader.read(camelContext, rests, openApiConfig, camelContext.getName(), classResolver);
if (!configuration.isApiVendorExtension()) {
clearVendorExtensions(openApi);
}
}
response.setOpenApi(openApi);
byte[] bytes = getFromOpenAPI(openApi, openApiConfig, byte[].class, json);
int len = bytes.length;
response.setHeader(Exchange.CONTENT_LENGTH, Integer.toString(len));
Expand Down
Expand Up @@ -70,6 +70,8 @@ public void configure() {
RestConfiguration restConfiguration = context.getRestConfiguration();
RestOpenApiProcessor processor
= new RestOpenApiProcessor(restConfiguration.getApiProperties(), restConfiguration);
processor.setCamelContext(context);
processor.start();
Exchange exchange = new DefaultExchange(context);
processor.process(exchange);

Expand Down Expand Up @@ -104,6 +106,8 @@ public void configure() {
RestConfiguration restConfiguration = context.getRestConfiguration();
RestOpenApiProcessor processor
= new RestOpenApiProcessor(restConfiguration.getApiProperties(), restConfiguration);
processor.setCamelContext(context);
processor.start();
Exchange exchange = new DefaultExchange(context);
processor.process(exchange);

Expand Down Expand Up @@ -141,6 +145,8 @@ public void configure() {
RestConfiguration restConfiguration = context.getRestConfiguration();
RestOpenApiProcessor processor
= new RestOpenApiProcessor(restConfiguration.getApiProperties(), restConfiguration);
processor.setCamelContext(context);
processor.start();
Exchange exchange = new DefaultExchange(context);
processor.process(exchange);

Expand Down Expand Up @@ -189,6 +195,8 @@ public void configure() {
RestConfiguration restConfiguration = context.getRestConfiguration();
RestOpenApiProcessor processor
= new RestOpenApiProcessor(restConfiguration.getApiProperties(), restConfiguration);
processor.setCamelContext(context);
processor.start();
Exchange exchange = new DefaultExchange(context);
processor.process(exchange);

Expand Down Expand Up @@ -237,6 +245,8 @@ public void configure() {
RestConfiguration restConfiguration = context.getRestConfiguration();
RestOpenApiProcessor processor
= new RestOpenApiProcessor(restConfiguration.getApiProperties(), restConfiguration);
processor.setCamelContext(context);
processor.start();
Exchange exchange = new DefaultExchange(context);
processor.process(exchange);

Expand Down
Expand Up @@ -54,6 +54,9 @@ public void configure() {
RestConfiguration restConfiguration = context.getRestConfiguration();
RestOpenApiProcessor processor
= new RestOpenApiProcessor(restConfiguration.getApiProperties(), restConfiguration);
processor.setCamelContext(context);
processor.start();

Exchange exchange = new DefaultExchange(context);
processor.process(exchange);

Expand Down