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

PAYARA-2889 Defer OpenAPI Scanning Until Endpoint Visit #2916

Merged
merged 6 commits into from Jul 5, 2018
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
@@ -0,0 +1,48 @@
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) [2018] Payara Foundation and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* https://github.com/payara/Payara/blob/master/LICENSE.txt
* See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at glassfish/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* The Payara Foundation designates this particular file as subject to the "Classpath"
* exception as provided by the Payara Foundation in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package fish.payara.microprofile.openapi.api;

public class OpenAPIBuildException extends Exception {
private static final long serialVersionUID = 1L;

public OpenAPIBuildException(Throwable t) {
super(t);
}
}
Expand Up @@ -45,7 +45,6 @@
import java.util.Collections;
import java.util.Deque;
import java.util.Enumeration;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.logging.Logger;
Expand All @@ -72,6 +71,7 @@
import org.jvnet.hk2.config.NotProcessed;
import org.jvnet.hk2.config.UnprocessedChangeEvents;

import fish.payara.microprofile.openapi.api.OpenAPIBuildException;
import fish.payara.microprofile.openapi.impl.admin.OpenApiServiceConfiguration;
import fish.payara.microprofile.openapi.impl.config.OpenApiConfiguration;
import fish.payara.microprofile.openapi.impl.model.OpenAPIImpl;
Expand All @@ -80,24 +80,28 @@
import fish.payara.microprofile.openapi.impl.processor.FileProcessor;
import fish.payara.microprofile.openapi.impl.processor.FilterProcessor;
import fish.payara.microprofile.openapi.impl.processor.ModelReaderProcessor;
import fish.payara.nucleus.executorservice.PayaraExecutorService;

@Service(name = "microprofile-openapi-service")
@RunLevel(StartupRunLevel.VAL)
public class OpenApiService implements PostConstruct, PreDestroy, EventListener, ConfigListener {

private static final Logger LOGGER = Logger.getLogger(OpenApiService.class.getName());

private Deque<Map<ApplicationInfo, OpenAPI>> models;
private Deque<OpenApiMapping> mappings;

@Inject
private Events events;

@Inject
private OpenApiServiceConfiguration config;

@Inject
private PayaraExecutorService executor;

@Override
public void postConstruct() {
models = new ConcurrentLinkedDeque<>();
mappings = new ConcurrentLinkedDeque<>();
events.register(this);
}

Expand All @@ -113,8 +117,8 @@ public boolean isEnabled() {
/**
* Listen for OpenAPI config changes.
*/
@Override
public UnprocessedChangeEvents changed(PropertyChangeEvent[] event) {
@Override
public UnprocessedChangeEvents changed(PropertyChangeEvent[] event) {
return ConfigSupport.sortAndDispatch(event, new Changed() {
@Override
public <T extends ConfigBeanProxy> NotProcessed changed(TYPE type, Class<T> tClass, T t) {
Expand All @@ -130,7 +134,7 @@ public <T extends ConfigBeanProxy> NotProcessed changed(TYPE type, Class<T> tCla
return null;
}
}, LOGGER);
}
}

/**
* Listen for application deployment events.
Expand All @@ -142,55 +146,35 @@ public void event(Event<?> event) {
ApplicationInfo appInfo = (ApplicationInfo) event.hook();

// Create all the relevant resources
if (isEnabled() && isValidApp(appInfo)) {
// Create the OpenAPI config
OpenApiConfiguration appConfig = new OpenApiConfiguration(appInfo.getAppClassLoader());

// Map the application info to a new openapi document, and store it in the list
Map<ApplicationInfo, OpenAPI> map = Collections.singletonMap(appInfo,
createOpenApiDocument(appInfo, appConfig));
models.add(map);

LOGGER.info("OpenAPI document created.");
if (isValidApp(appInfo)) {
// Store the application mapping in the list
mappings.add(new OpenApiMapping(appInfo));
}
} else if (event.is(Deployment.APPLICATION_UNLOADED)) {
ApplicationInfo appInfo = (ApplicationInfo) event.hook();
for (Map<ApplicationInfo, OpenAPI> map : models) {
if (map.keySet().toArray()[0].equals(appInfo)) {
models.remove(map);
for (OpenApiMapping mapping : mappings) {
if (mapping.getAppInfo().equals(appInfo)) {
mappings.remove(mapping);
break;
}
}
}
}

/**
* Gets the document for the most recently deployed application.
* @return the document for the most recently deployed application. Creates one
* if it hasn't already been created.
* @throws OpenAPIBuildException if creating the document failed.
*/
public OpenAPI getDocument() {
if (models.isEmpty()) {
public OpenAPI getDocument() throws OpenAPIBuildException {
if (mappings.isEmpty() || !isEnabled()) {
return null;
}
return (OpenAPI) models.getLast().values().toArray()[0];
}

private OpenAPI createOpenApiDocument(ApplicationInfo appInfo, OpenApiConfiguration config) {
OpenAPI document = new OpenAPIImpl();

String contextRoot = getContextRoot(appInfo);
ReadableArchive archive = appInfo.getSource();
Set<Class<?>> classes = getClassesFromArchive(archive, appInfo.getAppClassLoader());

document = new ModelReaderProcessor().process(document, config);
document = new FileProcessor(appInfo.getAppClassLoader()).process(document, config);
document = new ApplicationProcessor(classes).process(document, config);
document = new BaseProcessor(contextRoot).process(document, config);
document = new FilterProcessor().process(document, config);
return document;
return (OpenAPI) mappings.peekLast().getDocument();
}

/**
* Retrieves an instance of this service from HK2.
* @return an instance of this service from HK2.
*/
public static OpenApiService getInstance() {
return Globals.getStaticBaseServiceLocator().getService(OpenApiService.class);
Expand All @@ -213,7 +197,7 @@ private static String getContextRoot(ApplicationInfo appInfo) {
}

/**
* @param archive the archive to read from.
* @param archive the archive to read from.
* @param appClassLoader the classloader to use to load the classes.
* @return a list of all loadable classes in the archive.
*/
Expand All @@ -227,13 +211,13 @@ private static Set<Class<?>> getClassesFromArchive(ReadableArchive archive, Clas
.map(x -> {
Class<?> loadedClass = null;
// Attempt to load the class, ignoring any errors
try {
loadedClass = appClassLoader.loadClass(x);
} catch (Throwable t) {
try {
loadedClass = appClassLoader.loadClass(x);
} catch (Throwable t) {
}
try {
loadedClass = Class.forName(x);
} catch (Throwable t) {
try {
loadedClass = Class.forName(x);
} catch (Throwable t) {
}
// If the class can be loaded, check that everything in the class also can
if (loadedClass != null) {
Expand All @@ -247,8 +231,52 @@ private static Set<Class<?>> getClassesFromArchive(ReadableArchive archive, Clas
return loadedClass;
})
// Don't return null classes
.filter(x -> x != null)
.collect(toSet());
.filter(x -> x != null).collect(toSet());
}

private class OpenApiMapping {

private final ApplicationInfo appInfo;
private final OpenApiConfiguration appConfig;
private volatile OpenAPI document;

private OpenApiMapping(ApplicationInfo appInfo) {
this.appInfo = appInfo;
this.appConfig = new OpenApiConfiguration(appInfo.getAppClassLoader());
}

private ApplicationInfo getAppInfo() {
return appInfo;
}

private synchronized OpenAPI getDocument() throws OpenAPIBuildException {
if (document == null) {
document = buildDocument();
}
return document;
}

private OpenAPI buildDocument() throws OpenAPIBuildException {
OpenAPI openapi = new OpenAPIImpl();

try {
String contextRoot = getContextRoot(appInfo);
ReadableArchive archive = appInfo.getSource();
Set<Class<?>> classes = getClassesFromArchive(archive, appInfo.getAppClassLoader());

openapi = new ModelReaderProcessor().process(openapi, appConfig);
openapi = new FileProcessor(appInfo.getAppClassLoader()).process(openapi, appConfig);
openapi = new ApplicationProcessor(classes).process(openapi, appConfig);
openapi = new BaseProcessor(contextRoot).process(openapi, appConfig);
openapi = new FilterProcessor().process(openapi, appConfig);
} catch (Throwable t) {
throw new OpenAPIBuildException(t);
}

LOGGER.info("OpenAPI document created.");
return openapi;
}

}

}
Expand Up @@ -310,7 +310,7 @@ public void visitPATCH(PATCH patch, Method element, ApiContext context) {

@Override
public void visitProduces(Produces produces, AnnotatedElement element, ApiContext context) {
if (element instanceof Method) {
if (element instanceof Method && context.getWorkingOperation() != null) {
for (org.eclipse.microprofile.openapi.models.responses.APIResponse response : context.getWorkingOperation()
.getResponses().values()) {

Expand Down
Expand Up @@ -40,6 +40,7 @@
package fish.payara.microprofile.openapi.impl.rest.app.service;

import static fish.payara.microprofile.openapi.impl.rest.app.OpenApiApplication.APPLICATION_YAML;
import static java.util.logging.Level.WARNING;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.Response.Status.FORBIDDEN;

Expand All @@ -56,6 +57,7 @@

import org.eclipse.microprofile.openapi.models.OpenAPI;

import fish.payara.microprofile.openapi.api.OpenAPIBuildException;
import fish.payara.microprofile.openapi.impl.OpenApiService;
import fish.payara.microprofile.openapi.impl.model.OpenAPIImpl;

Expand All @@ -75,11 +77,16 @@ public Response getResponse(@Context HttpServletResponse response) throws IOExce
}

// Get the OpenAPI document
OpenAPI document = OpenApiService.getInstance().getDocument();
OpenAPI document = null;
try {
document = OpenApiService.getInstance().getDocument();
} catch (OpenAPIBuildException ex) {
LOGGER.log(WARNING, "OpenAPI document creation failed.", ex);
}

// If there are none, return an empty OpenAPI document
if (document == null) {
LOGGER.info("No document found.");
LOGGER.info("No OpenAPI document found.");
return Response.status(Status.NOT_FOUND).entity(new OpenAPIImpl()).build();
}

Expand Down