blob: bb2997c230806a31a1225e9bf79e4ad1cd6b7e02 [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.geronimo.microprofile.openapi.cdi;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toSet;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.Instance;
import javax.enterprise.inject.spi.Annotated;
import javax.enterprise.inject.spi.AnnotatedMethod;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.BeforeBeanDiscovery;
import javax.enterprise.inject.spi.CDI;
import javax.enterprise.inject.spi.Extension;
import javax.enterprise.inject.spi.ProcessAnnotatedType;
import javax.enterprise.inject.spi.ProcessBean;
import javax.servlet.ServletContext;
import javax.ws.rs.Path;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.MediaType;
import org.apache.geronimo.microprofile.openapi.config.GeronimoOpenAPIConfig;
import org.apache.geronimo.microprofile.openapi.impl.filter.FilterImpl;
import org.apache.geronimo.microprofile.openapi.impl.loader.DefaultLoader;
import org.apache.geronimo.microprofile.openapi.impl.loader.yaml.Yaml;
import org.apache.geronimo.microprofile.openapi.impl.model.PathsImpl;
import org.apache.geronimo.microprofile.openapi.impl.processor.AnnotatedMethodElement;
import org.apache.geronimo.microprofile.openapi.impl.processor.AnnotatedTypeElement;
import org.apache.geronimo.microprofile.openapi.impl.processor.AnnotationProcessor;
import org.apache.geronimo.microprofile.openapi.impl.processor.spi.NamingStrategy;
import org.apache.geronimo.microprofile.openapi.jaxrs.JacksonOpenAPIYamlBodyWriter;
import org.eclipse.microprofile.openapi.OASConfig;
import org.eclipse.microprofile.openapi.OASFilter;
import org.eclipse.microprofile.openapi.OASModelReader;
import org.eclipse.microprofile.openapi.models.OpenAPI;
public class GeronimoOpenAPIExtension implements Extension {
private final Collection<Bean<?>> endpoints = new ArrayList<>();
private final Map<Application, OpenAPI> openapis = new HashMap<>();
private GeronimoOpenAPIConfig config;
private AnnotationProcessor processor;
private boolean skipScan;
private Collection<String> classes;
private Collection<String> packages;
private Collection<String> excludePackages;
private Collection<String> excludeClasses;
private boolean jacksonIsPresent;
void init(@Observes final BeforeBeanDiscovery beforeBeanDiscovery) {
config = GeronimoOpenAPIConfig.create();
processor = new AnnotationProcessor(config, loadNamingStrategy(config), null);
skipScan = Boolean.parseBoolean(config.read(OASConfig.SCAN_DISABLE, "false"));
classes = getConfigCollection(OASConfig.SCAN_CLASSES);
packages = getConfigCollection(OASConfig.SCAN_PACKAGES);
excludePackages = getConfigCollection(OASConfig.SCAN_EXCLUDE_PACKAGES);
excludeClasses = getConfigCollection(OASConfig.SCAN_EXCLUDE_CLASSES);
try {
Yaml.getObjectMapper();
jacksonIsPresent = true;
} catch (final Error | RuntimeException e) {
// no-op
}
}
public MediaType getDefaultMediaType() {
return jacksonIsPresent ? new MediaType("text", "vnd.yaml") : APPLICATION_JSON_TYPE;
}
private NamingStrategy loadNamingStrategy(final GeronimoOpenAPIConfig config) {
return ofNullable(config.read("model.operation.naming.strategy", null))
.map(String::trim)
.filter(it -> !it.isEmpty())
.map(it -> {
try {
return Thread.currentThread().getContextClassLoader().loadClass(it).getConstructor().newInstance();
} catch (final InstantiationException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException e) {
throw new IllegalArgumentException(e);
} catch (final InvocationTargetException ite) {
throw new IllegalArgumentException(ite.getTargetException());
}
})
.map(NamingStrategy.class::cast)
.orElseGet(NamingStrategy.Default::new);
}
void vetoJacksonIfNotHere(@Observes final ProcessAnnotatedType<JacksonOpenAPIYamlBodyWriter> event) {
if (!jacksonIsPresent) {
event.veto();
}
}
protected <T> void findEndpointsAndApplication(@Observes final ProcessBean<T> event) {
final String typeName = event.getAnnotated().getBaseType().getTypeName();
if (classes == null && !skipScan && event.getAnnotated().isAnnotationPresent(Path.class) &&
!typeName.startsWith("org.apache.geronimo.microprofile.openapi.") &&
(packages == null || packages.stream().anyMatch(typeName::startsWith))) {
endpoints.add(event.getBean());
}
}
public OpenAPI getOrCreateOpenAPI(final Application application) {
if (classes != null) {
final ClassLoader loader = Thread.currentThread().getContextClassLoader();
return openapis.computeIfAbsent(application,
app -> createOpenApi(application.getClass(), classes.stream().map(c -> {
try {
return loader.loadClass(c);
} catch (final ClassNotFoundException e) {
throw new IllegalArgumentException(e);
}
})));
}
if (packages == null && (!application.getSingletons().isEmpty() || !application.getClasses().isEmpty())) {
return openapis.computeIfAbsent(application,
app -> createOpenApi(application.getClass(), Stream.concat(endpoints.stream().map(Bean::getBeanClass),
Stream.concat(app.getClasses().stream(), app.getSingletons().stream().map(Object::getClass)))));
}
return openapis.computeIfAbsent(application,
app -> createOpenApi(application.getClass(), endpoints.stream().map(Bean::getBeanClass)));
}
private Collection<String> getConfigCollection(final String key) {
return ofNullable(config.read(key, null))
.map(vals -> Stream.of(vals.split(",")).map(String::trim).filter(v -> !v.isEmpty()).collect(toSet()))
.orElse(null);
}
private OpenAPI createOpenApi(final Class<?> application, final Stream<Class<?>> beans) {
final CDI<Object> current = CDI.current();
final OpenAPI api = ofNullable(config.read(OASConfig.MODEL_READER, null))
.map(value -> newInstance(current, value))
.map(it -> OASModelReader.class.cast(it).buildModel())
.orElseGet(() -> current.select(DefaultLoader.class).get().loadDefaultApi());
final BeanManager beanManager = current.getBeanManager();
processor.processApplication(api, new ElementImpl(beanManager.createAnnotatedType(application)));
if (skipScan) {
return api.paths(new PathsImpl());
}
// adds the context path to the base
final Instance<ServletContext> servletContextInstance = current.select(ServletContext.class);
final boolean appendContextPath = Boolean.valueOf(config.read("application.append-context-path", "true"));
String contextPath = "";
if (appendContextPath && !servletContextInstance.isAmbiguous() && !servletContextInstance.isUnsatisfied()) {
contextPath = servletContextInstance.get().getContextPath();
}
final String base = contextPath + processor.getApplicationBinding(application);
processor.beforeProcessing();
beans.filter(c -> (excludeClasses == null || !excludeClasses.contains(c.getName())))
.filter(c -> (excludePackages == null || excludePackages.stream().noneMatch(it -> c.getName().startsWith(it))))
.map(beanManager::createAnnotatedType)
.forEach(at -> processor.processClass(
base, api, new ElementImpl(at), at.getMethods().stream().map(MethodElementImpl::new)));
return ofNullable(config.read(OASConfig.FILTER, null))
.map(it -> newInstance(current, it))
.map(i -> new FilterImpl(OASFilter.class.cast(i)).filter(api))
.orElse(api);
}
private Object newInstance(final CDI<Object> current, final String value) {
try {
final Class<?> clazz = Thread.currentThread().getContextClassLoader().loadClass(value.trim());
try {
final Instance<?> instance = current.select(clazz);
if (!instance.isAmbiguous() && !instance.isUnsatisfied()) { // isResolvable is not always there
return instance.get();
}
} catch (final RuntimeException e) {
// let do " new"
}
return clazz.getConstructor().newInstance();
} catch (final Exception e) {
throw new IllegalArgumentException("Can't load " + value, e);
}
}
private static class MethodElementImpl extends ElementImpl implements AnnotatedMethodElement {
private final AnnotatedMethod<?> delegate;
private MethodElementImpl(final AnnotatedMethod<?> delegate) {
super(delegate);
this.delegate = delegate;
}
@Override
public String getName() {
return delegate.getJavaMember().getName();
}
@Override
public Type getReturnType() {
return delegate.getJavaMember().getGenericReturnType();
}
@Override
public Class<?> getDeclaringClass() {
return delegate.getDeclaringType().getJavaClass();
}
@Override
public AnnotatedTypeElement[] getParameters() {
return delegate.getParameters().stream().map(p -> new TypeElementImpl(p.getBaseType(), p)).toArray(TypeElementImpl[]::new);
}
}
private static class TypeElementImpl extends ElementImpl implements AnnotatedTypeElement {
private final Type type;
private TypeElementImpl(final Type type, final Annotated delegate) {
super(delegate);
this.type = type;
}
@Override
public Type getType() {
return type;
}
}
private static class ElementImpl implements AnnotatedElement {
private final Annotated delegate;
private ElementImpl(final Annotated annotated) {
this.delegate = annotated;
}
@Override
public <T extends Annotation> T getAnnotation(final Class<T> annotationClass) {
return delegate.getAnnotation(annotationClass);
}
@Override
public Annotation[] getAnnotations() {
return delegate.getAnnotations().toArray(new Annotation[0]);
}
@Override
public <T extends Annotation> T[] getAnnotationsByType(final Class<T> annotationClass) {
return delegate.getAnnotations(annotationClass).toArray((T[]) Array.newInstance(annotationClass, 0));
}
@Override
public Annotation[] getDeclaredAnnotations() {
return getAnnotations();
}
}
}