diff --git a/appserver/payara-appserver-modules/microprofile/openapi/src/main/java/fish/payara/microprofile/openapi/api/OpenAPIBuildException.java b/appserver/payara-appserver-modules/microprofile/openapi/src/main/java/fish/payara/microprofile/openapi/api/OpenAPIBuildException.java new file mode 100644 index 00000000000..752dde69b86 --- /dev/null +++ b/appserver/payara-appserver-modules/microprofile/openapi/src/main/java/fish/payara/microprofile/openapi/api/OpenAPIBuildException.java @@ -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); + } +} \ No newline at end of file diff --git a/appserver/payara-appserver-modules/microprofile/openapi/src/main/java/fish/payara/microprofile/openapi/impl/OpenApiService.java b/appserver/payara-appserver-modules/microprofile/openapi/src/main/java/fish/payara/microprofile/openapi/impl/OpenApiService.java index d16fb8bd951..2db36964b06 100644 --- a/appserver/payara-appserver-modules/microprofile/openapi/src/main/java/fish/payara/microprofile/openapi/impl/OpenApiService.java +++ b/appserver/payara-appserver-modules/microprofile/openapi/src/main/java/fish/payara/microprofile/openapi/impl/OpenApiService.java @@ -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; @@ -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; @@ -80,6 +80,7 @@ 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) @@ -87,7 +88,7 @@ public class OpenApiService implements PostConstruct, PreDestroy, EventListener, private static final Logger LOGGER = Logger.getLogger(OpenApiService.class.getName()); - private Deque<Map<ApplicationInfo, OpenAPI>> models; + private Deque<OpenApiMapping> mappings; @Inject private Events events; @@ -95,9 +96,12 @@ public class OpenApiService implements PostConstruct, PreDestroy, EventListener, @Inject private OpenApiServiceConfiguration config; + @Inject + private PayaraExecutorService executor; + @Override public void postConstruct() { - models = new ConcurrentLinkedDeque<>(); + mappings = new ConcurrentLinkedDeque<>(); events.register(this); } @@ -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) { @@ -130,7 +134,7 @@ public <T extends ConfigBeanProxy> NotProcessed changed(TYPE type, Class<T> tCla return null; } }, LOGGER); - } + } /** * Listen for application deployment events. @@ -142,22 +146,15 @@ 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; } } @@ -165,32 +162,19 @@ public void event(Event<?> event) { } /** - * 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); @@ -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. */ @@ -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) { @@ -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; + } + } } \ No newline at end of file diff --git a/appserver/payara-appserver-modules/microprofile/openapi/src/main/java/fish/payara/microprofile/openapi/impl/processor/ApplicationProcessor.java b/appserver/payara-appserver-modules/microprofile/openapi/src/main/java/fish/payara/microprofile/openapi/impl/processor/ApplicationProcessor.java index f294df625cc..594709c7497 100644 --- a/appserver/payara-appserver-modules/microprofile/openapi/src/main/java/fish/payara/microprofile/openapi/impl/processor/ApplicationProcessor.java +++ b/appserver/payara-appserver-modules/microprofile/openapi/src/main/java/fish/payara/microprofile/openapi/impl/processor/ApplicationProcessor.java @@ -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()) { diff --git a/appserver/payara-appserver-modules/microprofile/openapi/src/main/java/fish/payara/microprofile/openapi/impl/rest/app/service/OpenApiResource.java b/appserver/payara-appserver-modules/microprofile/openapi/src/main/java/fish/payara/microprofile/openapi/impl/rest/app/service/OpenApiResource.java index b9dff295261..12273b8ef4d 100644 --- a/appserver/payara-appserver-modules/microprofile/openapi/src/main/java/fish/payara/microprofile/openapi/impl/rest/app/service/OpenApiResource.java +++ b/appserver/payara-appserver-modules/microprofile/openapi/src/main/java/fish/payara/microprofile/openapi/impl/rest/app/service/OpenApiResource.java @@ -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; @@ -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; @@ -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(); }