diff --git a/dev/fattest.simplicity/src/com/ibm/websphere/simplicity/config/MpOpenAPIElement.java b/dev/fattest.simplicity/src/com/ibm/websphere/simplicity/config/MpOpenAPIElement.java index a370ce6ffff6..d85bce6c3ef6 100644 --- a/dev/fattest.simplicity/src/com/ibm/websphere/simplicity/config/MpOpenAPIElement.java +++ b/dev/fattest.simplicity/src/com/ibm/websphere/simplicity/config/MpOpenAPIElement.java @@ -9,12 +9,32 @@ *******************************************************************************/ package com.ibm.websphere.simplicity.config; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.logging.Logger; + import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; + +import componenttest.topology.impl.LibertyServer; public class MpOpenAPIElement extends ConfigElement { - private String docPath; - private String uiPath; + protected String docPath; + protected String uiPath; + + @XmlElement(name = "includeApplication") + protected List includedApplications; + + @XmlElement(name = "excludeApplication") + protected List excludedApplications; + + @XmlElement(name = "includeModule") + protected List includedModules; + + @XmlElement(name = "excludeModule") + protected List excludedModules; /** * @return the docPath @@ -46,9 +66,153 @@ public void setUiPath(String uiPath) { this.uiPath = uiPath; } + /** + * @return a list of applications to be included. + */ + public List getIncludedApplications() { + return (includedApplications == null) ? (includedApplications = new ArrayList()) : includedApplications; + } + + /** + * @return a list of applications to be excluded. + */ + + public List getExcludedApplications() { + return (excludedApplications == null) ? (excludedApplications = new ArrayList()) : excludedApplications; + } + + /** + * @return a list of modules to be included + */ + public List getIncludedModules() { + return (includedModules == null) ? (includedModules = new ArrayList()) : includedModules; + } + + /** + * @return a list of modules to be excluded + */ + public List getExcludedModules() { + return (excludedModules == null) ? (excludedModules = new ArrayList()) : excludedModules; + } + @Override public String toString() { - return "MpOpenAPIElement [docPath=" + docPath + ", uiPath=" + uiPath + "]"; + + StringBuilder sb = new StringBuilder(); + sb.append(", includeApplication=").append("[" + String.join(",", getIncludedApplications()) + "]"); + sb.append(", excludeApplication=").append("[" + String.join(",", getExcludedApplications()) + "]"); + sb.append(", includeModule=").append("[" + String.join(",", getIncludedModules()) + "]"); + sb.append(", excludeModule=").append("[" + String.join(",", getExcludedModules()) + "]"); + + return "MpOpenAPIElement [docPath=" + docPath + ", uiPath=" + uiPath + sb.toString() + "]"; + } + + public static class MpOpenAPIElementBuilder { + + private final static Logger LOG = Logger.getLogger("MpOpenAPIElementBuilder"); + + public static MpOpenAPIElementBuilder cloneBuilderFromServer(LibertyServer server) throws CloneNotSupportedException, Exception { + return new MpOpenAPIElementBuilder(server); + } + + public static MpOpenAPIElementBuilder cloneBuilderFromServerResetAppsAndModules(LibertyServer server) throws CloneNotSupportedException, Exception { + MpOpenAPIElementBuilder builder = new MpOpenAPIElementBuilder(server); + nullSafeClear(builder.element.excludedApplications); + nullSafeClear(builder.element.includedApplications); + nullSafeClear(builder.element.excludedModules); + nullSafeClear(builder.element.excludedModules); + return builder; + } + + private final MpOpenAPIElement element; + private final LibertyServer server; + private final ServerConfiguration serverConfig; + + private MpOpenAPIElementBuilder(LibertyServer server) throws CloneNotSupportedException, Exception { + this.server = server; + this.serverConfig = server.getServerConfiguration().clone(); + this.element = serverConfig.getMpOpenAPIElement(); + } + + public void buildAndPushToServer() throws Exception { + LOG.info("Pushing new server configuration: " + serverConfig.toString()); + server.setMarkToEndOfLog(); + server.updateServerConfiguration(serverConfig); + if (server.isStarted()) { + server.waitForStringInLogUsingMark("CWWKG0017I"); //The server configuration was successfully updated + //Setting a low timeout because this only appears if OpenAPI trace is enabled (and enabling trace breaks debuggers) + server.waitForStringInLogUsingMark("Finished creating OpenAPI provider", 500); + } + } + + public MpOpenAPIElement build() throws Exception { + return element; + } + + public void buildAndOverwrite(MpOpenAPIElement other) { + other.docPath = element.docPath; + other.uiPath = element.uiPath; + other.includedApplications = element.getIncludedApplications(); + other.excludedApplications = element.getExcludedApplications(); + other.includedModules = element.getIncludedModules(); + other.excludedModules = element.getExcludedModules(); + } + + public MpOpenAPIElementBuilder setDocPath(String docPath) { + element.docPath = docPath; + return this; + } + + public MpOpenAPIElementBuilder setUiPath(String uiPath) { + element.uiPath = uiPath; + return this; + } + + public MpOpenAPIElementBuilder addIncludedApplicaiton(String application) { + element.getIncludedApplications().add(application); + return this; + } + + public MpOpenAPIElementBuilder addIncludedApplicaiton(List applications) { + element.getIncludedApplications().addAll(applications); + return this; + } + + public MpOpenAPIElementBuilder addIncludedModule(String module) { + element.getIncludedModules().add(module); + return this; + } + + public MpOpenAPIElementBuilder addIncludedModule(List module) { + element.getIncludedModules().addAll(module); + return this; + } + + public MpOpenAPIElementBuilder addExcludedApplicaiton(String application) { + element.getExcludedApplications().add(application); + return this; + } + + public MpOpenAPIElementBuilder addExcludedApplicaiton(List applications) { + element.getExcludedApplications().addAll(applications); + return this; + } + + public MpOpenAPIElementBuilder addExcludedModule(String module) { + element.getExcludedModules().add(module); + return this; + } + + public MpOpenAPIElementBuilder addExcludedModule(List module) { + element.getExcludedModules().addAll(module); + return this; + } + + private static void nullSafeClear(Collection c) { + if (c != null) { + c.clear(); + } + } } } diff --git a/dev/fattest.simplicity/src/com/ibm/websphere/simplicity/config/OpenAPIElement.java b/dev/fattest.simplicity/src/com/ibm/websphere/simplicity/config/OpenAPIElement.java index 2eb55b9fb2d3..8f1275ebbf15 100644 --- a/dev/fattest.simplicity/src/com/ibm/websphere/simplicity/config/OpenAPIElement.java +++ b/dev/fattest.simplicity/src/com/ibm/websphere/simplicity/config/OpenAPIElement.java @@ -4,7 +4,7 @@ * are made available under the terms of the Eclipse Public License 2.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-2.0/ - * + * * SPDX-License-Identifier: EPL-2.0 * * Contributors: diff --git a/dev/io.openliberty.microprofile.openapi.2.0.internal/bnd.bnd b/dev/io.openliberty.microprofile.openapi.2.0.internal/bnd.bnd index cae45f9ece7d..4f9cdd6ba3d6 100644 --- a/dev/io.openliberty.microprofile.openapi.2.0.internal/bnd.bnd +++ b/dev/io.openliberty.microprofile.openapi.2.0.internal/bnd.bnd @@ -55,7 +55,8 @@ IBM-Default-Config: OSGI-INF/wlp/defaultInstances.xml io.openliberty.microprofile.openapi20.internal.css.CustomCSSProcessor,\ io.openliberty.microprofile.openapi20.internal.DefaultHostListenerImpl,\ io.openliberty.microprofile.openapi20.internal.cache.ConfigSerializer,\ - io.openliberty.microprofile.openapi20.internal.validation.OASValidator30Impl + io.openliberty.microprofile.openapi20.internal.validation.OASValidator30Impl,\ + io.openliberty.microprofile.openapi20.internal.ModuleSelectionConfigImpl WS-TraceGroup: MPOPENAPI diff --git a/dev/io.openliberty.microprofile.openapi.2.0.internal/src/io/openliberty/microprofile/openapi20/internal/ApplicationProcessor.java b/dev/io.openliberty.microprofile.openapi.2.0.internal/src/io/openliberty/microprofile/openapi20/internal/ApplicationProcessor.java index b56d14adc480..949e996be0e0 100644 --- a/dev/io.openliberty.microprofile.openapi.2.0.internal/src/io/openliberty/microprofile/openapi20/internal/ApplicationProcessor.java +++ b/dev/io.openliberty.microprofile.openapi.2.0.internal/src/io/openliberty/microprofile/openapi20/internal/ApplicationProcessor.java @@ -48,6 +48,7 @@ import io.openliberty.microprofile.openapi20.internal.cache.ConfigSerializer; import io.openliberty.microprofile.openapi20.internal.services.ConfigFieldProvider; import io.openliberty.microprofile.openapi20.internal.services.ModelGenerator; +import io.openliberty.microprofile.openapi20.internal.services.ModuleSelectionConfig; import io.openliberty.microprofile.openapi20.internal.services.OpenAPIProvider; import io.openliberty.microprofile.openapi20.internal.utils.Constants; import io.openliberty.microprofile.openapi20.internal.utils.IndexUtils; diff --git a/dev/io.openliberty.microprofile.openapi.2.0.internal/src/io/openliberty/microprofile/openapi20/internal/ApplicationRegistryImpl.java b/dev/io.openliberty.microprofile.openapi.2.0.internal/src/io/openliberty/microprofile/openapi20/internal/ApplicationRegistryImpl.java index f933233dc1cd..0bc0cb96fe10 100644 --- a/dev/io.openliberty.microprofile.openapi.2.0.internal/src/io/openliberty/microprofile/openapi20/internal/ApplicationRegistryImpl.java +++ b/dev/io.openliberty.microprofile.openapi.2.0.internal/src/io/openliberty/microprofile/openapi20/internal/ApplicationRegistryImpl.java @@ -19,7 +19,6 @@ import java.util.Map.Entry; import java.util.Objects; -import org.eclipse.microprofile.config.ConfigProvider; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.ConfigurationPolicy; import org.osgi.service.component.annotations.Reference; @@ -36,10 +35,11 @@ import com.ibm.ws.kernel.feature.ServerStartedPhase2; import com.ibm.wsspi.kernel.service.utils.FrameworkState; +import io.openliberty.microprofile.openapi.internal.common.services.OpenAPIAppConfigProvider; import io.openliberty.microprofile.openapi20.internal.services.ApplicationRegistry; import io.openliberty.microprofile.openapi20.internal.services.MergeProcessor; +import io.openliberty.microprofile.openapi20.internal.services.ModuleSelectionConfig; import io.openliberty.microprofile.openapi20.internal.services.OpenAPIProvider; -import io.openliberty.microprofile.openapi20.internal.utils.Constants; import io.openliberty.microprofile.openapi20.internal.utils.LoggingUtils; import io.openliberty.microprofile.openapi20.internal.utils.MessageConstants; import io.openliberty.microprofile.openapi20.internal.utils.ModuleUtils; @@ -52,8 +52,8 @@ * OpenAPI documentation is generated for each web module and then merged together if merging is enabled. If merging is not enabled, * then documentation is only generated for the first web module found. */ -@Component(configurationPolicy = ConfigurationPolicy.IGNORE, service = ApplicationRegistry.class) -public class ApplicationRegistryImpl implements ApplicationRegistry { +@Component(configurationPolicy = ConfigurationPolicy.IGNORE) +public class ApplicationRegistryImpl implements ApplicationRegistry, OpenAPIAppConfigProvider.OpenAPIAppConfigListener { private static final TraceComponent tc = Tr.register(ApplicationRegistryImpl.class); @@ -66,13 +66,22 @@ public class ApplicationRegistryImpl implements ApplicationRegistry { @Reference private MergeProcessor mergeProcessor; + @Reference(cardinality = ReferenceCardinality.MANDATORY, policy = ReferencePolicy.STATIC, unbind = "unbindAppConfigListener") + public void bindAppConfigListener(OpenAPIAppConfigProvider openAPIAppConfigProvider) { + openAPIAppConfigProvider.registerAppConfigListener(this); + } + + public void unbindAppConfigListener(OpenAPIAppConfigProvider openAPIAppConfigProvider) { + openAPIAppConfigProvider.unregisterAppConfigListener(this); + } + // Thread safety: access to these fields must be synchronized on this - private final Map applications = new LinkedHashMap<>(); // Linked map retains order in which applications were added + private Map applications = new LinkedHashMap<>(); // Linked map retains order in which applications were added private OpenAPIProvider cachedProvider = null; - // Lazily initialized, use getModuleSelectionConfig() instead - private ModuleSelectionConfig moduleSelectionConfig = null; + @Reference + private ModuleSelectionConfig moduleSelectionConfig; /** * The addApplication method is invoked by the {@link ApplicationListener} when it is notified that an application @@ -95,13 +104,13 @@ public void addApplication(ApplicationInfo newAppInfo) { OpenAPIProvider firstProvider = getFirstProvider(); - if (getModuleSelectionConfig().useFirstModuleOnly() && firstProvider != null) { + if (moduleSelectionConfig.useFirstModuleOnly() && firstProvider != null) { if (LoggingUtils.isEventEnabled(tc)) { Tr.event(this, tc, "Application Processor: useFirstModuleOnly is configured and we already have a module. Not processing. appInfo=" + newAppInfo); } mergeDisabledAlerter.setUsingMultiModulesWithoutConfig(firstProvider); } else { - Collection openApiProviders = applicationProcessor.processApplication(newAppInfo, getModuleSelectionConfig()); + Collection openApiProviders = applicationProcessor.processApplication(newAppInfo, moduleSelectionConfig); if (!openApiProviders.isEmpty()) { // If the new application has any providers, invalidate the model cache @@ -139,7 +148,7 @@ public void removeApplication(ApplicationInfo removedAppInfo) { // If the removed application had any providers, invalidate the provider cache cachedProvider = null; - if (getModuleSelectionConfig().useFirstModuleOnly() && !FrameworkState.isStopping()) { + if (moduleSelectionConfig.useFirstModuleOnly() && !FrameworkState.isStopping()) { if (LoggingUtils.isEventEnabled(tc)) { Tr.event(this, tc, "Application Processor: Current OpenAPI application removed, looking for another application to document."); } @@ -147,7 +156,7 @@ public void removeApplication(ApplicationInfo removedAppInfo) { // We just removed the module used for the OpenAPI document and the server is not shutting down. // We need to find a new module to use if there is one for (ApplicationRecord app : applications.values()) { - Collection providers = applicationProcessor.processApplication(app.info, getModuleSelectionConfig()); + Collection providers = applicationProcessor.processApplication(app.info, moduleSelectionConfig); if (!providers.isEmpty()) { app.providers.addAll(providers); break; @@ -192,9 +201,7 @@ protected void setServerStartPhase2(ServerStartedPhase2 event) { // Couldn't read this application for some reason, but that means we can't have been able to include modules from it anyway. } } - for (String unmatchedInclude : getModuleSelectionConfig().findIncludesNotMatchingAnything(modules)) { - Tr.warning(tc, MessageConstants.OPENAPI_MERGE_UNUSED_INCLUDE_CWWKO1667W, Constants.MERGE_INCLUDE_CONFIG, unmatchedInclude); - } + moduleSelectionConfig.sendWarningsForAppsAndModulesNotMatchingAnything(modules); } } @@ -242,6 +249,9 @@ public OpenAPIProvider getOpenAPIProvider() { } cachedProvider = result; + if (LoggingUtils.isEventEnabled(tc)) { + Tr.event(this, tc, "Finished creating OpenAPI provider"); + } return result; } } @@ -254,19 +264,6 @@ private List getProvidersToMerge() { } } - /** - * Thread safety: Caller must hold lock on {@code this} - * - * @return the module selection config - */ - private ModuleSelectionConfig getModuleSelectionConfig() { - if (moduleSelectionConfig == null) { - // Lazy initialization to avoid calling getConfig() before Config is ready - moduleSelectionConfig = ModuleSelectionConfig.fromConfig(ConfigProvider.getConfig(ApplicationRegistryImpl.class.getClassLoader())); - } - return moduleSelectionConfig; - } - /** * Thread safety: Caller must hold lock on {@code this} * @@ -282,6 +279,20 @@ private OpenAPIProvider getFirstProvider() { return null; } + @Override + public void processConfigUpdate() { + synchronized (this) { + Map oldApps = applications; + applications = new LinkedHashMap<>(); + for (ApplicationRecord record : oldApps.values()) { + //Add application uses config to decide if it creates and registers any providers in ApplicationInfo + //Rather than map from the old state to the new state when the config changes, KISS and start again. + addApplication(record.info); + } + cachedProvider = null; + } + } + private static class ApplicationRecord { public ApplicationRecord(ApplicationInfo info) { this.info = info; @@ -291,4 +302,10 @@ public ApplicationRecord(ApplicationInfo info) { private final List providers = new ArrayList<>(); } + //This is to ensure we're called after ModuleSelectionConfigImpl + @Override + public int getConfigListenerPriority() { + return 2; + } + } diff --git a/dev/io.openliberty.microprofile.openapi.2.0.internal/src/io/openliberty/microprofile/openapi20/internal/ModuleSelectionConfig.java b/dev/io.openliberty.microprofile.openapi.2.0.internal/src/io/openliberty/microprofile/openapi20/internal/ModuleSelectionConfig.java deleted file mode 100644 index 4ada90d461f4..000000000000 --- a/dev/io.openliberty.microprofile.openapi.2.0.internal/src/io/openliberty/microprofile/openapi20/internal/ModuleSelectionConfig.java +++ /dev/null @@ -1,258 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2021 IBM Corporation and others. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 2.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * IBM Corporation - initial API and implementation - *******************************************************************************/ -package io.openliberty.microprofile.openapi20.internal; - -import static java.util.stream.Collectors.toList; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.eclipse.microprofile.config.Config; - -import com.ibm.websphere.ras.Tr; -import com.ibm.websphere.ras.TraceComponent; -import com.ibm.ws.container.service.app.deploy.ModuleInfo; - -import io.openliberty.microprofile.openapi20.internal.utils.Constants; -import io.openliberty.microprofile.openapi20.internal.utils.MessageConstants; - -/** - * Handles reading the merge include/exclude configuration properties and indicating whether a particular module should be included or excluded. - */ -public class ModuleSelectionConfig { - - private static final TraceComponent tc = Tr.register(ModuleSelectionConfig.class); - - private boolean isAll = false; - private boolean isFirst = false; - private List included; - private List excluded; - - /** - * Builds a {@code ModuleSelectionConfig} based on the given {@code Config} - *

- * This will read and parse the {@value Constants#MERGE_INCLUDE_CONFIG} and {@value Constants#MERGE_EXCLUDE_CONFIG} config properties. - *

- * If the config is invalid, this method will output warning messages but still return a usable result object. - * - * @param config the config to read - * @return the module selection config - */ - public static ModuleSelectionConfig fromConfig(Config config) { - ModuleSelectionConfig result = new ModuleSelectionConfig(); - - String inclusion = config.getOptionalValue(Constants.MERGE_INCLUDE_CONFIG, String.class).orElse("first").trim(); - - if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { - Tr.debug(null, tc, "Names in config: " + config.getPropertyNames()); - Tr.debug(null, tc, "Inclusion read from config: " + inclusion); - } - - if (inclusion.equals("none")) { - result.included = Collections.emptyList(); - } else if (inclusion.equals("all")) { - result.isAll = true; - } else if (inclusion.equals("first")) { - result.isFirst = true; - } else { - result.included = parseModuleNames(inclusion, Constants.MERGE_INCLUDE_CONFIG); - } - - String exclusion = config.getOptionalValue(Constants.MERGE_EXCLUDE_CONFIG, String.class).orElse("none").trim(); - if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { - Tr.debug(null, tc, "Exclusion read from config: " + exclusion); - } - - if (exclusion.equals("none")) { - result.excluded = Collections.emptyList(); - } else { - result.excluded = parseModuleNames(exclusion, Constants.MERGE_EXCLUDE_CONFIG); - } - - return result; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder("Module Selection Config["); - if (isFirst) { - sb.append("useFirstModuleOnly"); - } else { - if (isAll) { - sb.append("include = all"); - } else { - sb.append("include = ").append(included); - } - sb.append(", "); - sb.append("exclude = ").append(excluded); - } - sb.append("]"); - return sb.toString(); - } - - /** - * Whether the legacy "first module only" mode should be used. - *

- * As this requires special handling, if this method returns {@code true}, the other methods on this object should not be called. - * - * @return {@code true} if only the first module found should be processed for OpenAPI annotations, {@code false} otherwise. - */ - public boolean useFirstModuleOnly() { - return isFirst; - } - - /** - * Whether the given module should be used to create the OpenAPI document, based on the config - * - * @param module the module to check - * @return {@code true} if the module should be used, {@code false} otherwise - */ - public boolean isIncluded(ModuleInfo module) { - if (isFirst) { - return true; - } - - boolean result = false; - if (isAll) { - result = true; - } else { - for (ModuleName name : included) { - if (matches(name, module)) { - result = true; - break; - } - } - } - - if (result) { - for (ModuleName name : excluded) { - if (matches(name, module)) { - result = false; - break; - } - } - } - - return result; - } - - /** - * Given a complete list of all application modules deployed, return a list of entries from the include configuration which didn't match any of the deployed modules. - * - * @param moduleInfos the deployed module infos - * @return the list of include configuration entries which was unused - */ - public List findIncludesNotMatchingAnything(Collection moduleInfos) { - if (isAll || isFirst) { - return Collections.emptyList(); - } - - List includedNotYetSeen = new ArrayList<>(included); - for (Iterator iterator = includedNotYetSeen.iterator(); iterator.hasNext();) { - ModuleName moduleName = iterator.next(); - for (ModuleInfo moduleInfo : moduleInfos) { - if (matches(moduleName, moduleInfo)) { - iterator.remove(); - break; - } - } - } - - return includedNotYetSeen.stream().map(ModuleName::toString).collect(toList()); - } - - private boolean matches(ModuleName name, ModuleInfo module) { - if (name.moduleName != null && !name.moduleName.equals(module.getName())) { - return false; - } - - if (!name.appName.equals(module.getApplicationInfo().getName())) { - return false; - } - - return true; - } - - private static class ModuleName { - /** - * The application name - */ - private final String appName; - - /** - * The module name, may be {@code null} if the configuration just indicates an application name - */ - private final String moduleName; - - /** - * @param appName - * @param moduleName - */ - public ModuleName(String appName, String moduleName) { - this.appName = appName; - this.moduleName = moduleName; - } - - @Override - public String toString() { - if (moduleName == null) { - return appName; - } else { - return appName + "/" + moduleName; - } - } - } - - private static final Pattern CONFIG_VALUE_NAME_REFERENCE = Pattern.compile("(.+?)(/(.+))?"); - - /** - * Parses a comma separated list of app and module names into a list of {@code ModuleName} - *

- * Names must be in one of these formats: - *

    - *
  • appName
  • - *
  • appName/moduleName
  • - *
- * - * @param nameList the comma separated list - * @param configKey the name of the config property holding the list (used for reporting errors) - * @param the list of parsed names - */ - private static List parseModuleNames(String nameList, String configKey) { - List result = new ArrayList<>(); - - for (String configValuePart : nameList.split(",")) { - Matcher m = CONFIG_VALUE_NAME_REFERENCE.matcher(configValuePart); - - if (!m.matches()) { - Tr.warning(tc, MessageConstants.OPENAPI_MERGE_INVALID_NAME_CWWKO1666W, configKey, configValuePart); - continue; - } - - String appName = m.group(1).trim(); - String moduleName = m.group(3); - if (moduleName != null) { - moduleName = moduleName.trim(); - } - result.add(new ModuleName(appName, moduleName)); - } - - return result; - } - -} diff --git a/dev/io.openliberty.microprofile.openapi.2.0.internal/src/io/openliberty/microprofile/openapi20/internal/ModuleSelectionConfigImpl.java b/dev/io.openliberty.microprofile.openapi.2.0.internal/src/io/openliberty/microprofile/openapi20/internal/ModuleSelectionConfigImpl.java new file mode 100644 index 000000000000..085aa698b62a --- /dev/null +++ b/dev/io.openliberty.microprofile.openapi.2.0.internal/src/io/openliberty/microprofile/openapi20/internal/ModuleSelectionConfigImpl.java @@ -0,0 +1,475 @@ +/******************************************************************************* + * Copyright (c) 2021, 2024 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package io.openliberty.microprofile.openapi20.internal; + +import static java.util.stream.Collectors.toList; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; + +import com.ibm.websphere.ras.Tr; +import com.ibm.websphere.ras.TraceComponent; +import com.ibm.ws.container.service.app.deploy.ModuleInfo; +import com.ibm.ws.kernel.productinfo.ProductInfo; + +import io.openliberty.microprofile.openapi.internal.common.services.OpenAPIAppConfigProvider; +import io.openliberty.microprofile.openapi.internal.common.services.OpenAPIServerXMLConfig; +import io.openliberty.microprofile.openapi.internal.common.services.OpenAPIServerXMLConfig.ConfigMode; +import io.openliberty.microprofile.openapi20.internal.services.ModuleSelectionConfig; +import io.openliberty.microprofile.openapi20.internal.utils.Constants; +import io.openliberty.microprofile.openapi20.internal.utils.MessageConstants; + +/** + * Handles reading the merge include/exclude configuration properties and indicating whether a particular module should be included or excluded. + */ +@Component(configurationPolicy = ConfigurationPolicy.IGNORE) +public class ModuleSelectionConfigImpl implements ModuleSelectionConfig, OpenAPIAppConfigProvider.OpenAPIAppConfigListener { + + private static final TraceComponent tc = Tr.register(ModuleSelectionConfigImpl.class); + + private volatile ConfigValues configValues = null; + + private enum MatchingMode { + SERVER_XML_NAME, + DEPLOYMENT_DESCRIPTOR_NAME + } + + @Reference + private OpenAPIAppConfigProvider configFromServerXMLProvider; + + @Activate + public void activate() { + configFromServerXMLProvider.registerAppConfigListener(this); + } + + @Deactivate + public void deactivate() { + configFromServerXMLProvider.unregisterAppConfigListener(this); + } + + @Override + public String toString() { + ConfigValues config = configValues; + if (config == null) { + return ("Unconfigured Module Selection Config"); + } + + StringBuilder sb = new StringBuilder("Module Selection Config["); + if (config.isFirst) { + sb.append("useFirstModuleOnly"); + } else { + if (config.isAll) { + sb.append("include = all"); + } else { + sb.append("include = ").append(config.included); + } + sb.append(", "); + sb.append("exclude = ").append(config.excluded); + } + sb.append("]"); + return sb.toString(); + } + + /** + * Whether the legacy "first module only" mode should be used. + *

+ * As this requires special handling, if this method returns {@code true}, the other methods on this object should not be called. + * + * @return {@code true} if only the first module found should be processed for OpenAPI annotations, {@code false} otherwise. + */ + @Override + public boolean useFirstModuleOnly() { + return getConfigValues().isFirst; + } + + /** + * Whether the given module should be used to create the OpenAPI document, based on the config + * + * @param module the module to check + * @return {@code true} if the module should be used, {@code false} otherwise + */ + @Override + public boolean isIncluded(ModuleInfo module) { + ConfigValues config = getConfigValues(); + if (config.isFirst) { + return true; + } + + boolean result = false; + if (config.isAll) { + result = true; + } else { + for (ModuleName name : config.included) { + if (matches(name, module)) { + result = true; + break; + } + } + } + + if (result) { + for (ModuleName name : config.excluded) { + if (matches(name, module)) { + result = false; + break; + } + } + } + + return result; + } + + /** + * Given a complete list of all application modules deployed, emit a warning for all applications or modules included in the OpenAPI + * document that do not match a deployed module. The warning will specify if the include statement matches the deployment descriptor + * when we're looking for the name that's specified in server.xml. + * + * Also throw a warning for an application or module excluded from the openAPI document if it matches a deployment descriptor when + * we're looking for the name that's specified in server.xml, but not if it doesn't match either. + * + * @param moduleInfos the deployed module infos + */ + @Override + public void sendWarningsForAppsAndModulesNotMatchingAnything(Collection moduleInfos) { + ConfigValues config = getConfigValues(); + if (config.isAll || config.isFirst) { + return; + } + + List includedNotYetSeen = new ArrayList<>(config.included); + for (Iterator iterator = includedNotYetSeen.iterator(); iterator.hasNext();) { + ModuleName moduleName = iterator.next(); + for (ModuleInfo moduleInfo : moduleInfos) { + if (matches(moduleName, moduleInfo)) { + iterator.remove(); + break; + } + } + } + + Map includedNotYetSeenButSeenUnderOldNaming = new HashMap<>(); + if (config.serverxmlmode && ProductInfo.getBetaEdition()) { + for (Iterator iterator = includedNotYetSeen.iterator(); iterator.hasNext();) { + ModuleName moduleName = iterator.next(); + for (ModuleInfo moduleInfo : moduleInfos) { + if (matches(moduleName, moduleInfo, MatchingMode.DEPLOYMENT_DESCRIPTOR_NAME)) { + includedNotYetSeenButSeenUnderOldNaming.put(moduleName, + moduleInfo.getApplicationInfo().getDeploymentName()); + iterator.remove(); + break; + } + } + } + + for (ModuleName unmatchedInclude : includedNotYetSeenButSeenUnderOldNaming.keySet()) { + String appOrModule = unmatchedInclude.moduleName != null ? "includeModule" : "includeApplication"; + + Tr.warning(tc, MessageConstants.OPENAPI_USING_WRONG_NAME_SOURCE_CWWKO1680W, appOrModule, unmatchedInclude.appName, + includedNotYetSeenButSeenUnderOldNaming.get(unmatchedInclude)); + } + } + + for (String unmatchedInclude : includedNotYetSeen.stream().map(ModuleName::toString).collect(toList())) { + Tr.warning(tc, MessageConstants.OPENAPI_MERGE_UNUSED_INCLUDE_CWWKO1667W, Constants.MERGE_INCLUDE_CONFIG, unmatchedInclude); + } + + //Now we repeat the process for excluded applications and modules, however this time we only throw a warning message if we get a match on the deployment descriptor's name + + List excludedNotYetSeen = new ArrayList<>(config.excluded); + for (Iterator iterator = excludedNotYetSeen.iterator(); iterator.hasNext();) { + ModuleName moduleName = iterator.next(); + for (ModuleInfo moduleInfo : moduleInfos) { + if (matches(moduleName, moduleInfo)) { + iterator.remove(); + break; + } + } + } + + Map excludedNotYetSeenButSeenUnderOldNaming = new HashMap<>(); + if (config.serverxmlmode && ProductInfo.getBetaEdition()) { + for (Iterator iterator = excludedNotYetSeen.iterator(); iterator.hasNext();) { + ModuleName moduleName = iterator.next(); + for (ModuleInfo moduleInfo : moduleInfos) { + if (matches(moduleName, moduleInfo, MatchingMode.DEPLOYMENT_DESCRIPTOR_NAME)) { + excludedNotYetSeenButSeenUnderOldNaming.put(moduleName, + moduleInfo.getApplicationInfo().getDeploymentName()); + iterator.remove(); + break; + } + } + } + + for (ModuleName unmatchedExclude : excludedNotYetSeenButSeenUnderOldNaming.keySet()) { + + String appOrModule = unmatchedExclude.moduleName != null ? "excludeModule" : "excludeApplication"; + + Tr.warning(tc, MessageConstants.OPENAPI_USING_WRONG_NAME_SOURCE_CWWKO1680W, appOrModule, unmatchedExclude.appName, + excludedNotYetSeenButSeenUnderOldNaming.get(unmatchedExclude)); + } + } + + } + + /** + * + * @param name a name we're configured to include in the openAPI documentation + * @param module an actual module deployed on the server + */ + private boolean matches(ModuleName name, ModuleInfo module) { + return matches(name, module, getConfigValues().serverxmlmode ? MatchingMode.SERVER_XML_NAME : MatchingMode.DEPLOYMENT_DESCRIPTOR_NAME); + } + + private static boolean matches(ModuleName name, ModuleInfo module, MatchingMode matchingMode) { + if (name.moduleName != null && !name.moduleName.equals(module.getName())) { + return false; + } + + if (matchingMode == MatchingMode.SERVER_XML_NAME) { + if (!name.appName.equals(module.getApplicationInfo().getDeploymentName())) { + //Deployment name comes from the server.xml and this is the intended name to use when enabling an app + //getName comes from a metadata file in the war/ear, and is included under the grandfather clause. + return false; + } + } else { + if (!name.appName.equals(module.getApplicationInfo().getName())) { + return false; + } + } + + return true; + } + + private static class ModuleName { + /** + * The application name + */ + private final String appName; + + /** + * The module name, may be {@code null} if the configuration just indicates an application name + */ + private final String moduleName; + + /** + * @param appName + * @param moduleName + */ + public ModuleName(String appName, String moduleName) { + this.appName = appName; + this.moduleName = moduleName; + } + + @Override + public String toString() { + if (moduleName == null) { + return appName; + } else { + return appName + "/" + moduleName; + } + } + } + + @Override + public void processConfigUpdate() { + //Just wipe the configuration so its recreated next time its read + synchronized (this) { + configValues = null; + } + } + + private static final Pattern CONFIG_VALUE_NAME_REFERENCE = Pattern.compile("(.+?)(/(.+))?"); + + /** + * Parses a comma separated list of app and module names into a list of {@code ModuleName} + *

+ * Names must be in one of these formats: + *

    + *
  • appName
  • + *
  • appName/moduleName
  • + *
+ * + * @param nameList the comma separated list + * @param configKey the name of the config property holding the list (used for reporting errors) + * @return the list of parsed names + */ + private static List parseModuleNames(String nameList, String configKey) { + List result = new ArrayList<>(); + + for (String configValuePart : nameList.split(",")) { + Optional processedName = parseModuleName(configValuePart, configKey); + processedName.ifPresent(result::add); + } + + return result; + } + + /** + * Parses an app or module name into a {@code ModuleName} + *

+ * The name must be in one of these formats: + *

    + *
  • appName
  • + *
  • appName/moduleName
  • + *
+ * + * @param name the name to parse + * @param configKey the name of the config property holding the list (used for reporting errors) + * @return the parsed name or an empty {@code Optional} if it did not fit the format. + */ + private static Optional parseModuleName(String name, String configKey) { + Matcher m = CONFIG_VALUE_NAME_REFERENCE.matcher(name); + + if (!m.matches()) { + Tr.warning(tc, MessageConstants.OPENAPI_MERGE_INVALID_NAME_CWWKO1666W, configKey, name); + return Optional.empty(); + } + + String appName = m.group(1).trim(); + String moduleName = m.group(3); + if (moduleName != null) { + moduleName = moduleName.trim(); + } + return Optional.of(new ModuleName(appName, moduleName)); + } + + @Override + public int getConfigListenerPriority() { + return 1; + } + + private ConfigValues getConfigValues() { + synchronized (this) { + if (configValues == null) { + configValues = new ConfigValues(); + } + return configValues; + } + } + + private class ConfigValues { + protected final boolean isAll; + protected final boolean isFirst; + protected final boolean serverxmlmode; + protected final List included; + protected final List excluded; + + /** + * A consistent set of configuration values used for module selection + *

+ * Callers should get an instance by calling {@code getConfigValues()}. + */ + protected ConfigValues() { + //defaults + boolean isAll = false; + boolean isFirst = false; + boolean serverxmlmode = false; + List included = Collections.emptyList(); + List excluded = Collections.emptyList(); + + Config configFromMPConfig = ConfigProvider.getConfig(ApplicationRegistryImpl.class.getClassLoader()); + + OpenAPIServerXMLConfig configFromServerXML = null; + if (!ProductInfo.getBetaEdition()) { + serverxmlmode = false; + } else { + configFromServerXML = configFromServerXMLProvider.getConfiguration(); + serverxmlmode = configFromServerXML.wasAnyConfigFound(); + } + + if (serverxmlmode) { + Tr.debug(this, tc, "Acquired includes statement from server.xml"); + + if (configFromServerXML.getConfigMode().isPresent()) { + ConfigMode configMode = configFromServerXML.getConfigMode().get(); + if (configMode == OpenAPIServerXMLConfig.ConfigMode.All) { + isAll = true; + } else if (configMode == OpenAPIServerXMLConfig.ConfigMode.First) { + isFirst = true; + } else if (configMode == OpenAPIServerXMLConfig.ConfigMode.None) { + included = Collections.emptyList(); + } + } else if (configFromServerXML.getIncludedAppsAndModules().isPresent()) { + List rawNames = configFromServerXML.getIncludedAppsAndModules().get(); + included = rawNames.stream().map((String rawName) -> parseModuleName(rawName, Constants.MERGE_INCLUDE_CONFIG)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + } + } else { + String inclusion = configFromMPConfig.getOptionalValue(Constants.MERGE_INCLUDE_CONFIG, String.class).orElse("first"); + Tr.debug(this, tc, "Names in config: " + configFromMPConfig.getPropertyNames()); + Tr.debug(this, tc, "Inclusion read from config: " + inclusion); + + if (inclusion.equals("none")) { + included = Collections.emptyList(); + } else if (inclusion.equals("all")) { + isAll = true; + } else if (inclusion.equals("first")) { + isFirst = true; + } else { + included = parseModuleNames(inclusion, Constants.MERGE_INCLUDE_CONFIG); + } + } + + if (serverxmlmode) { + if (configFromServerXML.getExcludedAppsAndModules().isPresent()) { + List rawNames = configFromServerXML.getExcludedAppsAndModules().get(); + excluded = rawNames.stream().map((String rawName) -> parseModuleName(rawName, Constants.MERGE_INCLUDE_CONFIG)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + } else { + excluded = Collections.emptyList(); + } + } else { + String exclusion = configFromMPConfig.getOptionalValue(Constants.MERGE_EXCLUDE_CONFIG, String.class).orElse("none") + .trim(); + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { + Tr.debug(this, tc, "Exclusion read from config: " + exclusion); + } + + if (exclusion.equals("none")) { + excluded = Collections.emptyList(); + } else { + excluded = parseModuleNames(exclusion, Constants.MERGE_EXCLUDE_CONFIG); + } + } + + this.isAll = isAll; + this.isFirst = isFirst; + this.serverxmlmode = serverxmlmode; + this.included = included; + this.excluded = excluded; + + } + } + +} diff --git a/dev/io.openliberty.microprofile.openapi.2.0.internal/src/io/openliberty/microprofile/openapi20/internal/merge/ModelEquality.java b/dev/io.openliberty.microprofile.openapi.2.0.internal/src/io/openliberty/microprofile/openapi20/internal/merge/ModelEquality.java index d9b51be1d612..29522a11fe57 100644 --- a/dev/io.openliberty.microprofile.openapi.2.0.internal/src/io/openliberty/microprofile/openapi20/internal/merge/ModelEquality.java +++ b/dev/io.openliberty.microprofile.openapi.2.0.internal/src/io/openliberty/microprofile/openapi20/internal/merge/ModelEquality.java @@ -4,7 +4,7 @@ * are made available under the terms of the Eclipse Public License 2.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-2.0/ - * + * * SPDX-License-Identifier: EPL-2.0 * * Contributors: @@ -49,14 +49,24 @@ private static boolean equalsImpl(Object a, Object b) { return true; } + /* + * Take https://github.com/smallrye/smallrye-open-api/blob/3.13.0/core/src/main/java/io/smallrye/openapi/api/models/ExtensibleImpl.java + * extensions is null. If you call addExtension() it will become an map with an item. + * if you then call remove, it will remove the item from the list and not null it. + * In short new ExtensibleImpl().addExtension(x).removeExtension(x) is not equals() new ExtensibleImpl(). + * + * So to work around that, we treat null and empty maps as equivalent. + * + * This may need to be extended to cover lists + */ if (a == null) { - if (b == null) { + if (nullOrEmptyMap(b)) { return true; } else { return false; } } else if (b == null) { - return false; + return nullOrEmptyMap(a); } Optional modelObject = ModelType.getModelObject(a.getClass()); @@ -79,6 +89,7 @@ private static boolean equalsImpl(Object a, Object b) { @Trivial private static boolean equalsMap(Map a, Map b) { + if (!Objects.equals(a.keySet(), b.keySet())) { return false; } @@ -124,4 +135,16 @@ private static boolean equalsModelObject(ModelType modelType, Object a, Object b return true; } + @Trivial + private static boolean nullOrEmptyMap(Object a) { + if (a == null) { + return true; + } + if (a instanceof Map) { + Map m = (Map) a; + return m.isEmpty(); + } + return false; + } + } diff --git a/dev/io.openliberty.microprofile.openapi.2.0.internal/src/io/openliberty/microprofile/openapi20/internal/services/ModuleSelectionConfig.java b/dev/io.openliberty.microprofile.openapi.2.0.internal/src/io/openliberty/microprofile/openapi20/internal/services/ModuleSelectionConfig.java new file mode 100644 index 000000000000..bd385df3ef9b --- /dev/null +++ b/dev/io.openliberty.microprofile.openapi.2.0.internal/src/io/openliberty/microprofile/openapi20/internal/services/ModuleSelectionConfig.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) 2024 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + *******************************************************************************/ +package io.openliberty.microprofile.openapi20.internal.services; + +import java.util.Collection; + +import com.ibm.ws.container.service.app.deploy.ModuleInfo; + +/** + * Handles reading the merge include/exclude configuration properties and indicating whether a particular module should be included or excluded. + */ +public interface ModuleSelectionConfig { + + /** + * Provide a human friendly summary of the current configuration. + */ + @Override + public String toString(); + + /** + * Whether the legacy "first module only" mode should be used. + *

+ * As this requires special handling, if this method returns {@code true}, the other methods on this object should not be called. + * + * @return {@code true} if only the first module found should be processed for OpenAPI annotations, {@code false} otherwise. + */ + public boolean useFirstModuleOnly(); + + /** + * Whether the given module should be used to create the OpenAPI document, based on the config + * + * @param module the module to check + * @return {@code true} if the module should be used, {@code false} otherwise + */ + public boolean isIncluded(ModuleInfo module); + + /** + * Given a complete list of all application modules deployed, throw a warning for each entry from the include configuration which didn't match any of the deployed modules. + * + * @param moduleInfos the deployed module infos + */ + public void sendWarningsForAppsAndModulesNotMatchingAnything(Collection moduleInfos); + +} diff --git a/dev/io.openliberty.microprofile.openapi.2.0.internal/src/io/openliberty/microprofile/openapi20/internal/utils/MessageConstants.java b/dev/io.openliberty.microprofile.openapi.2.0.internal/src/io/openliberty/microprofile/openapi20/internal/utils/MessageConstants.java index c770e0984488..6952e0379026 100644 --- a/dev/io.openliberty.microprofile.openapi.2.0.internal/src/io/openliberty/microprofile/openapi20/internal/utils/MessageConstants.java +++ b/dev/io.openliberty.microprofile.openapi.2.0.internal/src/io/openliberty/microprofile/openapi20/internal/utils/MessageConstants.java @@ -1,10 +1,10 @@ /******************************************************************************* - * Copyright (c) 2020, 2021 IBM Corporation and others. + * Copyright (c) 2020, 2024 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-2.0/ - * + * * SPDX-License-Identifier: EPL-2.0 * * Contributors: @@ -34,6 +34,7 @@ public class MessageConstants { public static final String OPENAPI_MERGE_INFO_PARSE_ERROR_CWWKO1665W = "OPENAPI_MERGE_INFO_PARSE_ERROR_CWWKO1665W"; //$NON-NLS-1$ public static final String OPENAPI_MERGE_INVALID_NAME_CWWKO1666W = "OPENAPI_MERGE_INVALID_NAME_CWWKO1666W"; //$NON-NLS-1$ public static final String OPENAPI_MERGE_UNUSED_INCLUDE_CWWKO1667W = "OPENAPI_MERGE_UNUSED_INCLUDE_CWWKO1667W"; //$NON-NLS-1$ + public static final String OPENAPI_USING_WRONG_NAME_SOURCE_CWWKO1680W = "OPENAPI_USING_WRONG_NAME_SOURCE_CWWKO1680W"; private MessageConstants() { // This class is not meant to be instantiated. diff --git a/dev/io.openliberty.microprofile.openapi.2.0.internal/src/io/openliberty/microprofile/openapi20/internal/utils/OpenAPIUtils.java b/dev/io.openliberty.microprofile.openapi.2.0.internal/src/io/openliberty/microprofile/openapi20/internal/utils/OpenAPIUtils.java index 0bbdcb0e384f..ce1e02d2bfca 100644 --- a/dev/io.openliberty.microprofile.openapi.2.0.internal/src/io/openliberty/microprofile/openapi20/internal/utils/OpenAPIUtils.java +++ b/dev/io.openliberty.microprofile.openapi.2.0.internal/src/io/openliberty/microprofile/openapi20/internal/utils/OpenAPIUtils.java @@ -182,6 +182,7 @@ public static Info getConfiguredInfo(Config config) { * @return {@code true} if all elements of {@code collection} are equal, {@code false} otherwise */ public static boolean allEqual(Collection collection, BiPredicate comparator) { + Iterator i = collection.iterator(); if (!i.hasNext()) { return true; @@ -189,7 +190,9 @@ public static boolean allEqual(Collection collection, BiPredica T first = i.next(); while (i.hasNext()) { - if (!equals(first, i.next(), comparator)) { + T next = i.next(); + + if (!equals(first, next, comparator)) { return false; } } diff --git a/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/fat/src/io/openliberty/microprofile/openapi20/fat/FATSuite.java b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/fat/src/io/openliberty/microprofile/openapi20/fat/FATSuite.java index 3ed9e4b47cf2..c65adc800e0a 100644 --- a/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/fat/src/io/openliberty/microprofile/openapi20/fat/FATSuite.java +++ b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/fat/src/io/openliberty/microprofile/openapi20/fat/FATSuite.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017, 2021 IBM Corporation and others. + * Copyright (c) 2017, 2024 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 * which accompanies this distribution, and is available at @@ -21,8 +21,11 @@ import io.openliberty.microprofile.openapi20.fat.cache.CacheTest; import io.openliberty.microprofile.openapi20.fat.deployments.DeploymentTest; import io.openliberty.microprofile.openapi20.fat.deployments.MergeConfigTest; +import io.openliberty.microprofile.openapi20.fat.deployments.MergeServerXMLTest; +import io.openliberty.microprofile.openapi20.fat.deployments.MergeServerXMLTestWithUpdate; import io.openliberty.microprofile.openapi20.fat.deployments.MergeTest; import io.openliberty.microprofile.openapi20.fat.deployments.MergeWithServletTest; +import io.openliberty.microprofile.openapi20.fat.deployments.StartupWarningMessagesTest; import io.openliberty.microprofile.openapi20.fat.shutdown.ShutdownTest; @RunWith(Suite.class) @@ -31,9 +34,12 @@ CacheTest.class, DeploymentTest.class, MergeConfigTest.class, + MergeServerXMLTest.class, + MergeServerXMLTestWithUpdate.class, MergeTest.class, MergeWithServletTest.class, - ShutdownTest.class + ShutdownTest.class, + StartupWarningMessagesTest.class }) public class FATSuite { public static RepeatTests repeatDefault(String serverName) { @@ -44,4 +50,4 @@ public static RepeatTests repeatDefault(String serverName) { MicroProfileActions.MP50, // mpOpenAPI-3.0, FULL MicroProfileActions.MP41);// mpOpenAPI-2.0, FULL } -} \ No newline at end of file +} diff --git a/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/fat/src/io/openliberty/microprofile/openapi20/fat/deployments/MergeServerXMLTest.java b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/fat/src/io/openliberty/microprofile/openapi20/fat/deployments/MergeServerXMLTest.java new file mode 100644 index 000000000000..ff5abc7b43a6 --- /dev/null +++ b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/fat/src/io/openliberty/microprofile/openapi20/fat/deployments/MergeServerXMLTest.java @@ -0,0 +1,253 @@ +/******************************************************************************* + * Copyright (c) 2024 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package io.openliberty.microprofile.openapi20.fat.deployments; + +import static com.ibm.websphere.simplicity.ShrinkHelper.DeployOptions.DISABLE_VALIDATION; +import static com.ibm.websphere.simplicity.ShrinkHelper.DeployOptions.SERVER_ONLY; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.EnterpriseArchive; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import com.fasterxml.jackson.databind.JsonNode; +import com.ibm.websphere.simplicity.ShrinkHelper; +import com.ibm.websphere.simplicity.config.MpOpenAPIElement; +import com.ibm.websphere.simplicity.config.MpOpenAPIElement.MpOpenAPIElementBuilder; +import com.ibm.websphere.simplicity.config.ServerConfiguration; + +import componenttest.annotation.Server; +import componenttest.custom.junit.runner.FATRunner; +import componenttest.rules.repeater.RepeatTests; +import componenttest.topology.impl.LibertyFileManager; +import componenttest.topology.impl.LibertyServer; +import componenttest.topology.utils.HttpRequest; +import io.openliberty.microprofile.openapi20.fat.FATSuite; +import io.openliberty.microprofile.openapi20.fat.deployments.test1.DeploymentTestApp; +import io.openliberty.microprofile.openapi20.fat.deployments.test1.DeploymentTestResource; +import io.openliberty.microprofile.openapi20.fat.utils.OpenAPIConnection; +import io.openliberty.microprofile.openapi20.fat.utils.OpenAPITestUtil; +import io.openliberty.microprofile.openapi20.fat.utils.WebXmlAsset; + +/** + * Test that applications are merged correctly + *

+ * These tests supplement the MergeProcessor unit tests + */ +@RunWith(FATRunner.class) +public class MergeServerXMLTest { + + private static final String SERVER_NAME = "OpenAPIMergeWithServerXMLTestServer"; + + @Server(SERVER_NAME) + public static LibertyServer server; + + @ClassRule + public static RepeatTests r = FATSuite.repeatDefault(SERVER_NAME); + + @BeforeClass + public static void setupServer() throws Exception { + //This will be ignored because we have openapi includes/excludes in server.xml + server.setAdditionalSystemProperties( + Collections.singletonMap("mp_openapi_extensions_liberty_merged_include", "none")); + server.saveServerConfiguration(); + server.startServer(); + } + + @AfterClass + public static void shutdownServer() throws Exception { + server.stopServer("CWWKO1662W", "CWWKO1678W", "CWWKO1679W"); // Problems occurred while merging. Warning message for invalid config we're testing. + } + + @After + public void cleanup() throws Exception { + server.setMarkToEndOfLog(); + + server.deleteAllDropinApplications(); // Will stop all dropin apps + server.restoreServerConfiguration(); // Will stop all apps deployed via server.xml + server.removeAllInstalledAppsForValidation(); // Validates that all apps stop + + // Delete everything from the apps directory + server.deleteDirectoryFromLibertyServerRoot("apps"); + LibertyFileManager.createRemoteFile(server.getMachine(), server.getServerRoot() + "/apps").mkdir(); + } + + @Test + public void testInvalidServerXML() throws Exception { + server.setMarkToEndOfLog(); + + MpOpenAPIElement.MpOpenAPIElementBuilder.cloneBuilderFromServerResetAppsAndModules(server) + .addIncludedApplicaiton("testEar/invalid") + .addExcludedModule("testEar") + .buildAndPushToServer(); + + List list = new ArrayList<>(Arrays.asList("CWWKO1678W", "CWWKO1679W")); + server.waitForStringsInLogUsingMark(list); + + } + + @Test + public void testMultiModuleEarWithServerXMLAppNameAndWebXmlModuleName() throws Exception { + + String appName = "serverXMLName"; + + MpOpenAPIElementBuilder.cloneBuilderFromServerResetAppsAndModules(server) + .addIncludedApplicaiton("serverXMLName") + .addExcludedModule("serverXMLName/nameFromWar") + .buildAndPushToServer(); + + WebArchive war1 = ShrinkWrap.create(WebArchive.class, "test1.war") + .addClasses(DeploymentTestApp.class, DeploymentTestResource.class); + + WebArchive war2 = ShrinkWrap.create(WebArchive.class, "test2.war") + .addClasses(DeploymentTestApp.class, DeploymentTestResource.class); + + WebArchive war3 = ShrinkWrap.create(WebArchive.class, "test3.war") + .addClasses(DeploymentTestApp.class, DeploymentTestResource.class) + .setWebXML(new WebXmlAsset("nameFromWar")); + + EnterpriseArchive ear = ShrinkWrap.create(EnterpriseArchive.class, "testEar.ear") + .addAsModules(war1, war2, war3); + + deployAppToApps(ear, appName, "ear"); + + String doc = OpenAPIConnection.openAPIDocsConnection(server, false).download(); + JsonNode openapiNode = OpenAPITestUtil.readYamlTree(doc); + OpenAPITestUtil.checkPaths(openapiNode, 2, "/test1/test", "/test2/test"); + OpenAPITestUtil.checkInfo(openapiNode, "Generated API", "1.0"); + assertServerContextRoot(openapiNode, null); + } + + @Test + public void testMultiModuleEarComplexServerXML() throws Exception { + + MpOpenAPIElement.MpOpenAPIElementBuilder.cloneBuilderFromServerResetAppsAndModules(server) + .addIncludedApplicaiton("testEar") + .addExcludedModule("testEar/test3") + .addExcludedModule("testEar/test4") + .buildAndPushToServer(); + + WebArchive war1 = ShrinkWrap.create(WebArchive.class, "test1.war") + .addClasses(DeploymentTestApp.class, DeploymentTestResource.class); + + WebArchive war2 = ShrinkWrap.create(WebArchive.class, "test2.war") + .addClasses(DeploymentTestApp.class, DeploymentTestResource.class); + + WebArchive war3 = ShrinkWrap.create(WebArchive.class, "test3.war") + .addClasses(DeploymentTestApp.class, DeploymentTestResource.class); + + WebArchive war4 = ShrinkWrap.create(WebArchive.class, "test4.war") + .addClasses(DeploymentTestApp.class, DeploymentTestResource.class); + + EnterpriseArchive ear = ShrinkWrap.create(EnterpriseArchive.class, "testEar.ear") + .addAsModules(war1, war2, war3, war4); + + deployApp(ear); + + logOnServer("/test1/log", "testMultiModuleEarComplexServerXML"); + + String doc = OpenAPIConnection.openAPIDocsConnection(server, false).download(); + JsonNode openapiNode = OpenAPITestUtil.readYamlTree(doc); + OpenAPITestUtil.checkPaths(openapiNode, 2, "/test1/test", "/test2/test"); + OpenAPITestUtil.checkInfo(openapiNode, "Generated API", "1.0"); + assertServerContextRoot(openapiNode, null); + } + + @Test + public void testMPConfigIgnoredInServerXMLMode() throws Exception { + + MpOpenAPIElement.MpOpenAPIElementBuilder.cloneBuilderFromServerResetAppsAndModules(server) + .addIncludedApplicaiton("all") + .buildAndPushToServer(); + + //The combo of all and an exclude should be all except testEar/test2, but since this is set via + //mpConfig this should be ignored. + setMergeConfig(null, "testEar/test2", null); + + WebArchive war1 = ShrinkWrap.create(WebArchive.class, "test1.war") + .addClasses(DeploymentTestApp.class, DeploymentTestResource.class); + + WebArchive war2 = ShrinkWrap.create(WebArchive.class, "test2.war") + .addClasses(DeploymentTestApp.class, DeploymentTestResource.class); + + EnterpriseArchive ear = ShrinkWrap.create(EnterpriseArchive.class, "testEar.ear") + .addAsModules(war1, war2); + + deployApp(ear); + + String doc = OpenAPIConnection.openAPIDocsConnection(server, false).download(); + JsonNode openapiNode = OpenAPITestUtil.readYamlTree(doc); + OpenAPITestUtil.checkPaths(openapiNode, 2, "/test1/test", "/test2/test"); + OpenAPITestUtil.checkInfo(openapiNode, "Generated API", "1.0"); + assertServerContextRoot(openapiNode, null); + } + + private void assertServerContextRoot(JsonNode model, + String contextRoot) { + OpenAPITestUtil.checkServer(model, + OpenAPITestUtil.getServerURLs(server, server.getHttpDefaultPort(), -1, contextRoot)); + } + + private void deployApp(Archive archive) throws Exception { + ShrinkHelper.exportDropinAppToServer(server, archive, SERVER_ONLY); + } + + private void deployAppToApps(Archive archive, String appName, String appType) throws Exception { + // Deploy the app archive + ShrinkHelper.exportAppToServer(server, archive, SERVER_ONLY, DISABLE_VALIDATION); + // Add app to server configuration + ServerConfiguration sc = server.getServerConfiguration(); + sc.addApplication(appName, archive.getName(), appType); + server.updateServerConfiguration(sc); + // Wait for app to start + server.addInstalledAppForValidation(appName); + } + + private void logOnServer(String pathUpToEndpoint, String message) throws Exception { + String path = pathUpToEndpoint + "?message=" + message; + new HttpRequest(server, path).run(String.class); + } + + private void setMergeConfig(String included, + String excluded, + String info) { + Map configProps = new HashMap<>(); + if (included != null) { + configProps.put("mp_openapi_extensions_liberty_merged_include", included); + } + + if (excluded != null) { + configProps.put("mp_openapi_extensions_liberty_merged_exclude", excluded); + } + + if (info != null) { + configProps.put("mp_openapi_extensions_liberty_merged_info", info); + } + + server.setAdditionalSystemProperties(configProps); + } + +} diff --git a/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/fat/src/io/openliberty/microprofile/openapi20/fat/deployments/MergeServerXMLTestWithUpdate.java b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/fat/src/io/openliberty/microprofile/openapi20/fat/deployments/MergeServerXMLTestWithUpdate.java new file mode 100644 index 000000000000..c4764bf8f90b --- /dev/null +++ b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/fat/src/io/openliberty/microprofile/openapi20/fat/deployments/MergeServerXMLTestWithUpdate.java @@ -0,0 +1,264 @@ +/******************************************************************************* + * Copyright (c) 2024 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package io.openliberty.microprofile.openapi20.fat.deployments; + +import static com.ibm.websphere.simplicity.ShrinkHelper.DeployOptions.DISABLE_VALIDATION; +import static com.ibm.websphere.simplicity.ShrinkHelper.DeployOptions.SERVER_ONLY; +import static org.hamcrest.Matchers.hasSize; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.EnterpriseArchive; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import com.fasterxml.jackson.databind.JsonNode; +import com.ibm.websphere.simplicity.ShrinkHelper; +import com.ibm.websphere.simplicity.config.MpOpenAPIElement; + +import componenttest.annotation.Server; +import componenttest.custom.junit.runner.FATRunner; +import componenttest.rules.repeater.RepeatTests; +import componenttest.topology.impl.LibertyServer; +import componenttest.topology.utils.HttpRequest; +import io.openliberty.microprofile.openapi20.fat.FATSuite; +import io.openliberty.microprofile.openapi20.fat.deployments.test1.DeploymentTestApp; +import io.openliberty.microprofile.openapi20.fat.deployments.test1.DeploymentTestResource; +import io.openliberty.microprofile.openapi20.fat.utils.OpenAPIConnection; +import io.openliberty.microprofile.openapi20.fat.utils.OpenAPITestUtil; + +/** + * Test that applications are merged correctly + *

+ * These tests supplement the MergeProcessor unit tests + */ +@RunWith(FATRunner.class) +public class MergeServerXMLTestWithUpdate { + + private static final String SERVER_NAME = "OpenAPIMergeWithServerXMLTestServer"; + private static final String SERVER_XML_NAME = "updatedserver.xml"; + private static final String OLD_XML_NAME = "originalserver.xml"; + private static final String APP1_XML_NAME = "App1Only.xml"; + + @Server(SERVER_NAME) + public static LibertyServer server; + + @ClassRule + public static RepeatTests r = FATSuite.repeatDefault(SERVER_NAME); + + private final List deployedApps = new ArrayList<>(); + + @BeforeClass + public static void setupServer() throws Exception { + server.setAdditionalSystemProperties( + Collections.singletonMap("mp_openapi_extensions_liberty_merged_include", "none")); + server.startServer(); + server.waitForStringInLogUsingMark("CWWKF0011I"); //ready to run a smarter planet + + } + + @AfterClass + public static void shutdownServer() throws Exception { + server.stopServer("CWWKO1662W"); // Problems occurred while merging + } + + @After + public void cleanup() throws Exception { + server.setMarkToEndOfLog(); + server.deleteAllDropinApplications(); + + List failedToStop = new ArrayList<>(); + for (String app : deployedApps) { + if (server.waitForStringInLogUsingMark("CWWKZ0009I:.*" + app) == null) { + failedToStop.add(app); + } + } + + if (!failedToStop.isEmpty()) { + throw new AssertionError("The following apps failed to stop: " + failedToStop); + } + + } + + @Test + public void testTwoWarsWithUpdate() throws Exception { + WebArchive war1 = ShrinkWrap.create(WebArchive.class, "test1.war") + .addClasses(DeploymentTestApp.class, DeploymentTestResource.class); + + WebArchive war2 = ShrinkWrap.create(WebArchive.class, "test2.war") + .addClasses(DeploymentTestApp.class, DeploymentTestResource.class); + + WebArchive war3 = ShrinkWrap.create(WebArchive.class, "test3.war") + .addClasses(DeploymentTestApp.class, DeploymentTestResource.class); + + deployApp(war1); + + //Swap the server.xml from saying everything is excluded to saying its included + logOnServer("/test1/log", "testTwoWarsUpdate1"); + + MpOpenAPIElement.MpOpenAPIElementBuilder.cloneBuilderFromServerResetAppsAndModules(server) + .addIncludedApplicaiton("test1") + .buildAndPushToServer(); + + assertRest("/test1/test"); + + // With one app deployed, we should only have the paths from war1 listed + String doc = OpenAPIConnection.openAPIDocsConnection(server, false).download(); + JsonNode openapiNode = OpenAPITestUtil.readYamlTree(doc); + OpenAPITestUtil.checkPaths(openapiNode, 1, "/test"); + assertEquals("/test1/test", OpenAPITestUtil.expandPath(openapiNode, "/test")); + + // check that merge is not traced + assertThat(server.findStringsInLogsAndTraceUsingMark("Merged document:"), hasSize(0)); + assertThat(server.findStringsInLogsAndTraceUsingMark("OpenAPIProvider retrieved from cache"), hasSize(0)); + + // check merged model was cached and cache is used on subsequent requests + OpenAPIConnection.openAPIDocsConnection(server, false).download(); + assertThat(server.findStringsInLogsAndTraceUsingMark("Merged document:"), hasSize(0)); + assertThat(server.findStringsInLogsAndTraceUsingMark("OpenAPIProvider retrieved from cache"), hasSize(1)); + + deployApp(war2); + deployApp(war3); + assertRest("/test1/test"); + assertRest("/test2/test"); + assertRest("/test3/test"); + //Swap the server.xml from saying everything is excluded to saying its included + logOnServer("/test1/log", "testTwoWarsUpdate2"); + MpOpenAPIElement.MpOpenAPIElementBuilder.cloneBuilderFromServerResetAppsAndModules(server) + .addIncludedApplicaiton("all") + .buildAndPushToServer(); + server.waitForStringInLogUsingMark("CWWKG0017I"); //The server configuration was successfully updated + + // With three apps deployed, we should have a merged doc with paths from all + // apps listed + // and a new info section + doc = OpenAPIConnection.openAPIDocsConnection(server, false).download(); + openapiNode = OpenAPITestUtil.readYamlTree(doc); + OpenAPITestUtil.checkPaths(openapiNode, 3, "/test1/test", "/test2/test", "/test3/test"); + OpenAPITestUtil.checkInfo(openapiNode, "Generated API", "1.0"); + assertServerContextRoot(openapiNode, null); + assertEquals("/test1/test", OpenAPITestUtil.expandPath(openapiNode, "/test1/test")); + + // check that merge is traced + assertThat(server.findStringsInLogsAndTraceUsingMark("Merged document:"), hasSize(1)); + assertThat(server.findStringsInLogsAndTraceUsingMark("OpenAPIProvider retrieved from cache"), hasSize(0)); + + // check merged model was cached and cache is used on subsequent requests + OpenAPIConnection.openAPIDocsConnection(server, false).download(); + assertThat(server.findStringsInLogsAndTraceUsingMark("Merged document:"), hasSize(1)); + assertThat(server.findStringsInLogsAndTraceUsingMark("OpenAPIProvider retrieved from cache"), hasSize(1)); + + // Remove war1 + undeployApp(war1); + + // Now we should just have war2 and war3 + doc = OpenAPIConnection.openAPIDocsConnection(server, false).download(); + openapiNode = OpenAPITestUtil.readYamlTree(doc); + OpenAPITestUtil.checkPaths(openapiNode, 2, "/test2/test", "/test3/test"); + OpenAPITestUtil.checkInfo(openapiNode, "Generated API", "1.0"); + assertServerContextRoot(openapiNode, null); + + // check that merge is traced + assertThat(server.findStringsInLogsAndTraceUsingMark("Merged document:"), hasSize(1)); + assertThat(server.findStringsInLogsAndTraceUsingMark("OpenAPIProvider retrieved from cache"), hasSize(0)); + + // check merged model was cached and cache is used on subsequent requests + OpenAPIConnection.openAPIDocsConnection(server, false).download(); + assertThat(server.findStringsInLogsAndTraceUsingMark("Merged document:"), hasSize(1)); + assertThat(server.findStringsInLogsAndTraceUsingMark("OpenAPIProvider retrieved from cache"), hasSize(1)); + + } + + @Test + public void testMultiModuleEarWithUpdate() throws Exception { + WebArchive war1 = ShrinkWrap.create(WebArchive.class, "test1.war") + .addClasses(DeploymentTestApp.class, DeploymentTestResource.class); + + WebArchive war2 = ShrinkWrap.create(WebArchive.class, "test2.war") + .addClasses(DeploymentTestApp.class, DeploymentTestResource.class); + + EnterpriseArchive ear = ShrinkWrap.create(EnterpriseArchive.class, "test.ear") + .addAsModules(war1, war2); + + deployApp(ear); + + //Swap the server.xml from saying everything is excluded to saying its included + MpOpenAPIElement.MpOpenAPIElementBuilder.cloneBuilderFromServerResetAppsAndModules(server) + .addIncludedApplicaiton("all") + .buildAndPushToServer(); + server.waitForStringInLogUsingMark("CWWKG0017I"); //The server configuration was successfully updated + + String doc = OpenAPIConnection.openAPIDocsConnection(server, false).download(); + JsonNode openapiNode = OpenAPITestUtil.readYamlTree(doc); + OpenAPITestUtil.checkPaths(openapiNode, 2, "/test1/test", "/test2/test"); + OpenAPITestUtil.checkInfo(openapiNode, "Generated API", "1.0"); + assertServerContextRoot(openapiNode, null); + } + + private void assertRest(String path) throws Exception { + String response = new HttpRequest(server, path).run(String.class); + assertEquals("Failed to call test resource at " + path, "OK", response); + } + + private void logOnServer(String pathUpToEndpoint, String message) throws Exception { + String path = pathUpToEndpoint + "?message=" + message; + new HttpRequest(server, path).run(String.class); + } + + private void assertServerContextRoot(JsonNode model, + String contextRoot) { + OpenAPITestUtil.checkServer(model, + OpenAPITestUtil.getServerURLs(server, server.getHttpDefaultPort(), -1, contextRoot)); + } + + private void deployApp(Archive archive) throws Exception { + server.setMarkToEndOfLog(); + server.setTraceMarkToEndOfDefaultTrace(); + ShrinkHelper.exportDropinAppToServer(server, archive, SERVER_ONLY, DISABLE_VALIDATION); + assertNotNull(server.waitForStringInLogUsingMark("CWWKZ0001I:.*" + getName(archive))); + deployedApps.add(getName(archive)); + } + + private void undeployApp(Archive archive) throws Exception { + server.setMarkToEndOfLog(); + server.setTraceMarkToEndOfDefaultTrace(); + server.deleteFileFromLibertyServerRoot("dropins/" + archive.getName()); + deployedApps.remove(getName(archive)); + assertNotNull(server.waitForStringInLogUsingMark("CWWKZ0009I:.*" + getName(archive))); + } + + private String getName(Archive archive) { + return getName(archive.getName()); + } + + private String getName(String fileName) { + int lastDot = fileName.lastIndexOf('.'); + if (lastDot != -1) { + fileName = fileName.substring(0, lastDot); + } + return fileName; + } + +} diff --git a/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/fat/src/io/openliberty/microprofile/openapi20/fat/deployments/StartupWarningMessagesTest.java b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/fat/src/io/openliberty/microprofile/openapi20/fat/deployments/StartupWarningMessagesTest.java new file mode 100644 index 000000000000..162392e14d13 --- /dev/null +++ b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/fat/src/io/openliberty/microprofile/openapi20/fat/deployments/StartupWarningMessagesTest.java @@ -0,0 +1,135 @@ +package io.openliberty.microprofile.openapi20.fat.deployments; + +import static com.ibm.websphere.simplicity.ShrinkHelper.DeployOptions.DISABLE_VALIDATION; +import static com.ibm.websphere.simplicity.ShrinkHelper.DeployOptions.SERVER_ONLY; + +import java.util.ArrayList; +import java.util.List; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.EnterpriseArchive; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.After; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import com.ibm.websphere.simplicity.ShrinkHelper; +import com.ibm.websphere.simplicity.config.MpOpenAPIElement; +import com.ibm.websphere.simplicity.config.ServerConfiguration; + +import componenttest.annotation.Server; +import componenttest.custom.junit.runner.FATRunner; +import componenttest.custom.junit.runner.Mode; +import componenttest.custom.junit.runner.Mode.TestMode; +import componenttest.rules.repeater.RepeatTests; +import componenttest.topology.impl.LibertyServer; +import io.openliberty.microprofile.openapi20.fat.FATSuite; +import io.openliberty.microprofile.openapi20.fat.deployments.test1.DeploymentTestApp; +import io.openliberty.microprofile.openapi20.fat.deployments.test1.DeploymentTestResource; + +@RunWith(FATRunner.class) +@Mode(TestMode.FULL) +public class StartupWarningMessagesTest { + private static final String SERVER_NAME = "OpenAPIWarningMessageTestServer"; + + @Server(SERVER_NAME) + public static LibertyServer server; + + @ClassRule + public static RepeatTests r = FATSuite.repeatDefault(SERVER_NAME); + + @BeforeClass + public static void setUp() throws Exception { + server.saveServerConfiguration(); + } + + @After + public void cleanup() throws Exception { + try { + if (server.isStarted()) { + server.stopServer("CWWKO1680W"); + } + } finally { + server.deleteAllDropinApplications(); + server.removeAllInstalledAppsForValidation(); + server.clearAdditionalSystemProperties(); + server.restoreServerConfiguration(); + } + } + + @Test + public void testAppNameMatchesOtherAppDeploymentName() throws Exception { + + WebArchive war1 = ShrinkWrap.create(WebArchive.class, "nameClash.war") + .addClasses(DeploymentTestApp.class, DeploymentTestResource.class); + + WebArchive war2 = ShrinkWrap.create(WebArchive.class, "test1.war") + .addClasses(DeploymentTestApp.class, DeploymentTestResource.class); + + ServerConfiguration serverConfig = server.getServerConfiguration().clone(); + serverConfig.addApplication("nameClash", "test1.war", "war"); + + MpOpenAPIElement.MpOpenAPIElementBuilder.cloneBuilderFromServerResetAppsAndModules(server) + //these match the application's deployment descriptor but not its name as set in server.xml. + //a warning should be emitted informing the user of their likely mistake and how o fix it. + .addIncludedApplicaiton("nameClash") + .buildAndOverwrite(serverConfig.getMpOpenAPIElement()); + + server.updateServerConfiguration(serverConfig); + + ShrinkHelper.exportAppToServer(server, war1, SERVER_ONLY); + ShrinkHelper.exportAppToServer(server, war2, SERVER_ONLY, DISABLE_VALIDATION); + server.startServer(); + + } + + @Test + public void testWarningWhenMatchesDeploymentName() throws Exception { + + ServerConfiguration serverConfig = server.getServerConfiguration().clone(); + serverConfig.addApplication("serverXMLNameIncluded", "testEarIncluded.ear", "ear"); + serverConfig.addApplication("serverXMLNameExcluded", "testEarExcluded.ear", "ear"); + + MpOpenAPIElement.MpOpenAPIElementBuilder.cloneBuilderFromServerResetAppsAndModules(server) + //these match the application's deployment descriptor but not its name as set in server.xml. + //a warning should be emitted informing the user of their likely mistake and how o fix it. + .addIncludedApplicaiton("testEarIncluded") + .addExcludedApplicaiton("testEarExcluded") + .addIncludedModule("testEarIncluded/test1") + .addExcludedModule("testEarExcluded/test2") + .buildAndOverwrite(serverConfig.getMpOpenAPIElement()); + + server.updateServerConfiguration(serverConfig); + + WebArchive war1 = ShrinkWrap.create(WebArchive.class, "test1.war") + .addClasses(DeploymentTestApp.class, DeploymentTestResource.class); + + EnterpriseArchive ear = ShrinkWrap.create(EnterpriseArchive.class, "testEarIncluded.ear") + .addAsModules(war1); + + WebArchive war2 = ShrinkWrap.create(WebArchive.class, "test2.war") + .addClasses(DeploymentTestApp.class, DeploymentTestResource.class); + + EnterpriseArchive ear2 = ShrinkWrap.create(EnterpriseArchive.class, "testEarExcluded.ear") + .addAsModules(war2); + + ShrinkHelper.exportAppToServer(server, ear, SERVER_ONLY, DISABLE_VALIDATION);//set up validation manually because the app name doesn't match the archive name + server.addInstalledAppForValidation("serverXMLNameIncluded"); + ShrinkHelper.exportAppToServer(server, ear2, SERVER_ONLY, DISABLE_VALIDATION); + server.addInstalledAppForValidation("serverXMLNameExcluded"); + + server.startServer(); + + //Example message: CWWKO1680W: The testEar application name in the includeApplication or includeModule configuration element does not match the name of any deployed application but it does match the name from the deployment descriptor of the serverXMLName application. The application name used here must be the application name specified in server.xml, or the archive file name with the extension removed if no name is specified in server.xml. + List messages = new ArrayList<>(); + messages.add("CWWKO1680W: The testEarIncluded application name in the includeModule configuration.*serverXMLNameIncluded application. The application name"); + messages.add("CWWKO1680W: The testEarIncluded application name in the includeApplication configuration.*serverXMLNameIncluded application. The application name"); + messages.add("CWWKO1680W: The testEarExcluded application name in the excludeModule configuration.*serverXMLNameExcluded application. The application name"); + messages.add("CWWKO1680W: The testEarExcluded application name in the excludeApplication configuration.*serverXMLNameExcluded application. The application name"); + server.waitForStringsInLogUsingMark(messages); //This method asserts the messages exist so no need to check its output. + + } + +} diff --git a/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/fat/src/io/openliberty/microprofile/openapi20/fat/deployments/test1/DeploymentTestResource.java b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/fat/src/io/openliberty/microprofile/openapi20/fat/deployments/test1/DeploymentTestResource.java index 905ab094e0a5..8d8ea18738dd 100644 --- a/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/fat/src/io/openliberty/microprofile/openapi20/fat/deployments/test1/DeploymentTestResource.java +++ b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/fat/src/io/openliberty/microprofile/openapi20/fat/deployments/test1/DeploymentTestResource.java @@ -1,8 +1,19 @@ +/******************************************************************************* + * Copyright (c) 2020, 2024 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + *******************************************************************************/ package io.openliberty.microprofile.openapi20.fat.deployments.test1; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import org.eclipse.microprofile.openapi.annotations.Operation; @@ -20,4 +31,14 @@ public String test() { return "OK"; } + @GET + @Path("/log") + @Operation(summary = "log method", hidden = true) + @APIResponse(responseCode = "200", description = "logs the queryparam message") + @Produces(value = MediaType.TEXT_PLAIN) + public String log(@QueryParam("message") String message) { + System.out.println(message); + return "OK"; + } + } diff --git a/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/fat/src/io/openliberty/microprofile/openapi20/fat/utils/OpenAPITestUtil.java b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/fat/src/io/openliberty/microprofile/openapi20/fat/utils/OpenAPITestUtil.java index 42e20c7b3e64..bfae736edf06 100644 --- a/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/fat/src/io/openliberty/microprofile/openapi20/fat/utils/OpenAPITestUtil.java +++ b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/fat/src/io/openliberty/microprofile/openapi20/fat/utils/OpenAPITestUtil.java @@ -27,11 +27,13 @@ import java.util.Iterator; import java.util.List; import java.util.Set; +import java.util.logging.Logger; import org.junit.Assert; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.constructor.SafeConstructor; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -49,6 +51,8 @@ public class OpenAPITestUtil { private final static int TIMEOUT = 30000; + private final static Logger LOG = Logger.getLogger("OpenAPITestUtil"); + /** * Change Liberty features (Mark is set first on log. Then wait for feature updated message using mark) * @@ -229,7 +233,9 @@ public static Application addApplication(LibertyServer server, public static JsonNode readYamlTree(String contents) { org.yaml.snakeyaml.Yaml yaml = new org.yaml.snakeyaml.Yaml(new SafeConstructor(new LoaderOptions())); - return new ObjectMapper().convertValue(yaml.load(contents), JsonNode.class); + JsonNode node = new ObjectMapper().convertValue(yaml.load(contents), JsonNode.class); + LOG.info(node.toPrettyString()); + return node; } /** @@ -305,18 +311,19 @@ private static URI findServerUrl(JsonNode serversNode) { public static void checkInfo(JsonNode root, String defaultTitle, - String defaultVersion) { + String defaultVersion) + throws JsonProcessingException { JsonNode infoNode = root.get("info"); assertNotNull(infoNode); - assertNotNull("Title is not specified to the default value", infoNode.get("title")); - assertNotNull("Version is not specified to the default value", infoNode.get("version")); + assertNotNull("Title is not specified to the default value; " + new ObjectMapper().writeValueAsString(root), infoNode.get("title")); + assertNotNull("Version is not specified to the default value" + new ObjectMapper().writeValueAsString(root), infoNode.get("version")); String title = infoNode.get("title").textValue(); String version = infoNode.get("version").textValue(); - assertEquals("Incorrect default value for title", defaultTitle, title); - assertEquals("Incorrect default value for version", defaultVersion, version); + assertEquals("Incorrect default value for title" + new ObjectMapper().writeValueAsString(root), defaultTitle, title); + assertEquals("Incorrect default value for version" + new ObjectMapper().writeValueAsString(root), defaultVersion, version); } public static void changeServerPorts(LibertyServer server, diff --git a/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/ApplicationProcessorServer/server.xml b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/ApplicationProcessorServer/server.xml index 05f9ca6ec0e0..2b86fbfbe0cf 100644 --- a/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/ApplicationProcessorServer/server.xml +++ b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/ApplicationProcessorServer/server.xml @@ -19,6 +19,7 @@ componenttest-1.0 ssl-1.0 mpOpenAPI-2.0 + mpConfig-2.0 jaxrs-2.1 diff --git a/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPIMergeTestServer/jvm.options b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPIMergeTestServer/jvm.options new file mode 100644 index 000000000000..75ff822e3efb --- /dev/null +++ b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPIMergeTestServer/jvm.options @@ -0,0 +1 @@ +-Dcom.ibm.ws.beta.edition=false \ No newline at end of file diff --git a/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPIMergeTestServer/server.xml b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPIMergeTestServer/server.xml index 48db8c440840..60982c7a77d7 100644 --- a/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPIMergeTestServer/server.xml +++ b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPIMergeTestServer/server.xml @@ -15,9 +15,15 @@ + osgiConsole-1.0 componenttest-1.0 mpOpenAPI-2.0 + mpConfig-2.0 jaxrs-2.1 + + ​ + none + diff --git a/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPIMergeWithServerXMLTestServer/bootstrap.properties b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPIMergeWithServerXMLTestServer/bootstrap.properties new file mode 100644 index 000000000000..15c880393111 --- /dev/null +++ b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPIMergeWithServerXMLTestServer/bootstrap.properties @@ -0,0 +1,14 @@ +############################################################################### +# Copyright (c) 2021 IBM Corporation and others. +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License 2.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +# +# Contributors: +# IBM Corporation - initial API and implementation +############################################################################### +bootstrap.include=../testports.properties +com.ibm.ws.logging.trace.specification=*=info=enabled:mpOpenAPI=all=enabled diff --git a/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPIMergeWithServerXMLTestServer/jvm.options b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPIMergeWithServerXMLTestServer/jvm.options new file mode 100644 index 000000000000..9ec5b94cf71a --- /dev/null +++ b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPIMergeWithServerXMLTestServer/jvm.options @@ -0,0 +1 @@ +-Dcom.ibm.ws.beta.edition=true \ No newline at end of file diff --git a/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPIMergeWithServerXMLTestServer/server.xml b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPIMergeWithServerXMLTestServer/server.xml new file mode 100644 index 000000000000..9ca2d8425a7a --- /dev/null +++ b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPIMergeWithServerXMLTestServer/server.xml @@ -0,0 +1,24 @@ + + + + + + + componenttest-1.0 + mpOpenAPI-2.0 + mpConfig-2.0 + servlet-4.0 + ​ + + diff --git a/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPIMergeWithServletTestServer/server.xml b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPIMergeWithServletTestServer/server.xml index fc24dfa16b80..c72299420545 100644 --- a/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPIMergeWithServletTestServer/server.xml +++ b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPIMergeWithServletTestServer/server.xml @@ -17,6 +17,7 @@ componenttest-1.0 mpOpenAPI-2.0 + mpConfig-2.0 servlet-4.0 diff --git a/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPITestServer/server.xml b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPITestServer/server.xml index 07d39f28c659..d81f66eed43d 100644 --- a/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPITestServer/server.xml +++ b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPITestServer/server.xml @@ -18,6 +18,7 @@ componenttest-1.0 ssl-1.0 mpOpenAPI-2.0 + mpConfig-2.0 jaxrs-2.1 diff --git a/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPIWarningMessageTestServer/bootstrap.properties b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPIWarningMessageTestServer/bootstrap.properties new file mode 100644 index 000000000000..15c880393111 --- /dev/null +++ b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPIWarningMessageTestServer/bootstrap.properties @@ -0,0 +1,14 @@ +############################################################################### +# Copyright (c) 2021 IBM Corporation and others. +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License 2.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +# +# Contributors: +# IBM Corporation - initial API and implementation +############################################################################### +bootstrap.include=../testports.properties +com.ibm.ws.logging.trace.specification=*=info=enabled:mpOpenAPI=all=enabled diff --git a/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPIWarningMessageTestServer/jvm.options b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPIWarningMessageTestServer/jvm.options new file mode 100644 index 000000000000..9ec5b94cf71a --- /dev/null +++ b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPIWarningMessageTestServer/jvm.options @@ -0,0 +1 @@ +-Dcom.ibm.ws.beta.edition=true \ No newline at end of file diff --git a/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPIWarningMessageTestServer/server.xml b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPIWarningMessageTestServer/server.xml new file mode 100644 index 000000000000..9b0432c3e16e --- /dev/null +++ b/dev/io.openliberty.microprofile.openapi.2.0.internal_fat/publish/servers/OpenAPIWarningMessageTestServer/server.xml @@ -0,0 +1,25 @@ + + + + + + + componenttest-1.0 + mpOpenAPI-2.0 + mpConfig-2.0 + servlet-4.0 + + ​ + + diff --git a/dev/io.openliberty.microprofile.openapi.internal.common/bnd.bnd b/dev/io.openliberty.microprofile.openapi.internal.common/bnd.bnd index f572a7ddd4fb..98bc4b9e0257 100644 --- a/dev/io.openliberty.microprofile.openapi.internal.common/bnd.bnd +++ b/dev/io.openliberty.microprofile.openapi.internal.common/bnd.bnd @@ -24,7 +24,8 @@ Import-Package: \ -dsannotations-inherit: true -dsannotations: \ - io.openliberty.microprofile.openapi.internal.common.OpenAPIEndpointManager + io.openliberty.microprofile.openapi.internal.common.OpenAPIEndpointManager,\ + io.openliberty.microprofile.openapi.internal.common.OpenAPIAppConfigProviderImpl Export-Package: \ io.openliberty.microprofile.openapi.internal.resources; version="1.0",\ @@ -38,16 +39,17 @@ Include-Resource: \ WS-TraceGroup: MPOPENAPI -buildpath: \ - com.ibm.ws.logging;version=latest,\ - com.ibm.ws.app.manager.wab;version=latest,\ - com.ibm.ws.logging;version=latest,\ - com.ibm.ws.kernel.boot.core;version=latest,\ - com.ibm.ws.kernel.service;version=latest,\ - com.ibm.ws.org.osgi.annotation.versioning;version=latest,\ - com.ibm.websphere.org.osgi.core;version=latest,\ - com.ibm.websphere.org.osgi.service.component;version=latest,\ - com.ibm.wsspi.org.osgi.service.component.annotations;version=latest,\ - com.ibm.wsspi.org.osgi.service.event;version=latest + com.ibm.ws.logging;version=latest,\ + com.ibm.ws.app.manager.wab;version=latest,\ + com.ibm.ws.logging;version=latest,\ + com.ibm.ws.kernel.boot.core;version=latest,\ + com.ibm.ws.kernel.service;version=latest,\ + com.ibm.ws.org.osgi.annotation.versioning;version=latest,\ + com.ibm.websphere.org.osgi.core;version=latest,\ + com.ibm.websphere.org.osgi.service.component;version=latest,\ + com.ibm.wsspi.org.osgi.service.component.annotations;version=latest,\ + com.ibm.wsspi.org.osgi.service.event;version=latest,\ + org.apache.commons.lang3 -testpath: \ ../build.sharedResources/lib/junit/old/junit.jar;version=file \ No newline at end of file diff --git a/dev/io.openliberty.microprofile.openapi.internal.common/resources/OSGI-INF/l10n/metatype.properties b/dev/io.openliberty.microprofile.openapi.internal.common/resources/OSGI-INF/l10n/metatype.properties index fbf3c494ed15..cee46949a5c7 100644 --- a/dev/io.openliberty.microprofile.openapi.internal.common/resources/OSGI-INF/l10n/metatype.properties +++ b/dev/io.openliberty.microprofile.openapi.internal.common/resources/OSGI-INF/l10n/metatype.properties @@ -23,4 +23,16 @@ docPath=MicroProfile OpenAPI document endpoint path docPath.desc=Specifies the URL path for obtaining OpenAPI documents. The URL path must be constructed with Unicode alphanumeric characters A-Za-z0-9, underscore (_), dash (-), forward slash (/), or period (.). uiPath=MicroProfile OpenAPI UI endpoint path -uiPath.desc=Specifies the URL path for accessing the MicroProfile OpenAPI UI. The URL path must be constructed with Unicode alphanumeric characters A-Za-z0-9, underscore (_), dash (-), forward slash (/), or period (.) If no value is specified, the path is set to $docPath/ui. \ No newline at end of file +uiPath.desc=Specifies the URL path for accessing the MicroProfile OpenAPI UI. The URL path must be constructed with Unicode alphanumeric characters A-Za-z0-9, underscore (_), dash (-), forward slash (/), or period (.) If no value is specified, the path is set to $docPath/ui. + +includeApplication=MicroProfile OpenAPI included applications +includeApplication.desc=Application names, one per element, that are to be included in the OpenAPI document. The name of an application can be defined in the server.xml, otherwise it defaults to the application archive filename excluding any extension. The "all" special value includes all available applications. + +excludeApplication=MicroProfile OpenAPI excluded applications +excludeApplication.desc=Application names, one per element, that are to be excluded from the OpenAPI document. + +includeModule=MicroProfile OpenAPI included modules +includeModule.desc=Module names, one per element, that are to be included in the OpenAPI document. Module names must be provided in the format {ApplicationName}/{ModuleName}. The name of a module can be defined in its deployment descriptor, otherwise it defaults to the module archive filename excluding any extension. It is also logged in the SRVE0169I message. + +excludeModule=MicroProfile OpenAPI excluded modules +excludeModule.desc=Module names, one per element, that are to be excluded from the OpenAPI document. diff --git a/dev/io.openliberty.microprofile.openapi.internal.common/resources/OSGI-INF/metatype/metatype.xml b/dev/io.openliberty.microprofile.openapi.internal.common/resources/OSGI-INF/metatype/metatype.xml index 802517f73d3f..22a2f4ef8c2f 100644 --- a/dev/io.openliberty.microprofile.openapi.internal.common/resources/OSGI-INF/metatype/metatype.xml +++ b/dev/io.openliberty.microprofile.openapi.internal.common/resources/OSGI-INF/metatype/metatype.xml @@ -17,6 +17,10 @@ + + + + diff --git a/dev/io.openliberty.microprofile.openapi.internal.common/resources/io/openliberty/microprofile/openapi/internal/resources/OpenAPI.nlsprops b/dev/io.openliberty.microprofile.openapi.internal.common/resources/io/openliberty/microprofile/openapi/internal/resources/OpenAPI.nlsprops index bedf96946c14..54da7c05ca4e 100644 --- a/dev/io.openliberty.microprofile.openapi.internal.common/resources/io/openliberty/microprofile/openapi/internal/resources/OpenAPI.nlsprops +++ b/dev/io.openliberty.microprofile.openapi.internal.common/resources/io/openliberty/microprofile/openapi/internal/resources/OpenAPI.nlsprops @@ -130,3 +130,17 @@ OPEN_API_DOC_PATH_INVALID_CWWKO1676E.useraction=Ensure the OpenAPI Path contains OPEN_API_PATH_SEGMENT_INVALID_CWWKO1677E=CWWKO1677E: Path segment "/." or "/.." is invalid for OpenAPI endpoints. OPEN_API_PATH_SEGMENT_INVALID_CWWKO1677E.explanation=OpenAPI path segment "/." or "/.." is invalid. OPEN_API_PATH_SEGMENT_INVALID_CWWKO1677E.useraction=Remove any path "/." or "/.." segment from the path. + +# {1} - the server.xml element name, either includeApplication or excludeApplication +OPEN_API_SLASH_IN_APPLICATION_CWWKO1678W=CWWKO1678W: The configured {0} application name in the {1} element is not valid because it contains the "/" character. +OPEN_API_SLASH_IN_APPLICATION_CWWKO1678W.explanation=An application cannot name cannot include a "/" as the "/" is used to separate the application from its component modules. +OPEN_API_SLASH_IN_APPLICATION_CWWKO1678W.useraction=Remove the "/" from the includeApplication or excludeApplication element in your server.xml file, or convert that element to includeModule or excludeModule to reference a single module. + +# {1} - the server.xml element name, either includeModule or excludeModule +OPEN_API_SLASH_IN_MODULE_CWWKO1679W=CWWKO1679W: The configured {0} module name in the {1} element is not valid as it is not in the format "/". +OPEN_API_SLASH_IN_MODULE_CWWKO1679W.explanation=A module must be in the format "/"; therefore, a module name without a "/" is not valid. +OPEN_API_SLASH_IN_MODULE_CWWKO1679W.useraction=Correct the includeModule or excludeModule element in your server.xml file, or convert that element to includeApplication or excludeApplication to reference an entire application. + +OPENAPI_USING_WRONG_NAME_SOURCE_CWWKO1680W=CWWKO1680W: The {1} application name in the {0} configuration element does not match the name of any deployed application, but it matches either the name from the deployment descriptor or the archive name of the {2} application. The application name that is used here must be the one specified in the server.xml file, or if no name is specified, the archive file name without the extension. +OPENAPI_USING_WRONG_NAME_SOURCE_CWWKO1680W.explanation=The application name reference in the mpOpenAPI configuration element matches the name from an application deployment descriptor, but does not match any application name that is defined in the server.xml file. +OPENAPI_USING_WRONG_NAME_SOURCE_CWWKO1680W.useraction=Check that all applications started correctly and that the names in the configuration element are specified correctly. \ No newline at end of file diff --git a/dev/io.openliberty.microprofile.openapi.internal.common/src/io/openliberty/microprofile/openapi/internal/common/OpenAPIAppConfigProviderImpl.java b/dev/io.openliberty.microprofile.openapi.internal.common/src/io/openliberty/microprofile/openapi/internal/common/OpenAPIAppConfigProviderImpl.java new file mode 100644 index 000000000000..add0ae328537 --- /dev/null +++ b/dev/io.openliberty.microprofile.openapi.internal.common/src/io/openliberty/microprofile/openapi/internal/common/OpenAPIAppConfigProviderImpl.java @@ -0,0 +1,256 @@ +/******************************************************************************* + * Copyright (c) 2023, 2024 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ******************************************************************************/ + +package io.openliberty.microprofile.openapi.internal.common; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.osgi.framework.BundleContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Modified; + +import com.ibm.websphere.ras.Tr; +import com.ibm.websphere.ras.TraceComponent; +import com.ibm.ws.kernel.productinfo.ProductInfo; + +import io.openliberty.microprofile.openapi.internal.common.services.OpenAPIAppConfigProvider; +import io.openliberty.microprofile.openapi.internal.common.services.OpenAPIServerXMLConfig; + +@Component(configurationPolicy = ConfigurationPolicy.OPTIONAL, configurationPid = "io.openliberty.microprofile.openapi") +public class OpenAPIAppConfigProviderImpl implements OpenAPIAppConfigProvider { + + private static final TraceComponent tc = Tr.register(OpenAPIAppConfigProviderImpl.class); + + private static final String INVALID_APP_WARNING = "OPEN_API_SLASH_IN_APPLICATION_CWWKO1678W"; + private static final String INVALID_MODULE_WARNING = "OPEN_API_SLASH_IN_MODULE_CWWKO1679W"; + + private static final String INCLUDE_APP_PROPERTY_NAME = "includeApplication"; + private static final String EXCLUDE_APP_PROPERTY_NAME = "excludeApplication"; + private static final String INCLUDE_MODULE_PROPERTY_NAME = "includeModule"; + private static final String EXCLUDE_MODULE_PROPERTY_NAME = "excludeModule"; + + private final List openAPIAppConfigListeners = new CopyOnWriteArrayList(); + + private volatile MpConfigServerConfigObject config = null; + + private boolean issuedBetaMessage; + + @Activate + protected void activate(BundleContext context, Map properties) { + if (ProductInfo.getBetaEdition()) { + if (TraceComponent.isAnyTracingEnabled() && tc.isEventEnabled()) { + Tr.event(this, tc, "Initial processing of server.xml"); + } + + MpConfigServerConfigObject newConfig = new MpConfigServerConfigObject(properties); + config = newConfig; + } + } + + @Modified + protected void modified(Map properties) { + if (ProductInfo.getBetaEdition()) { + if (TraceComponent.isAnyTracingEnabled() && tc.isEventEnabled()) { + Tr.event(this, tc, "Processing update to server.xml"); + } + + MpConfigServerConfigObject newConfig = new MpConfigServerConfigObject(properties); + + if (config.equals(newConfig)) { + Tr.event(this, tc, "After update there is no need to rebuild the openAPI document"); + } else { + Tr.event(this, tc, "Clearing out the outdated openAPI document"); + + config = newConfig; + openAPIAppConfigListeners.sort((OpenAPIAppConfigListener o1, OpenAPIAppConfigListener o2) -> Integer.compare(o1.getConfigListenerPriority(), + o2.getConfigListenerPriority())); + + for (OpenAPIAppConfigListener listener : openAPIAppConfigListeners) { + listener.processConfigUpdate(); + } + } + } + } + + @Deactivate + protected void deactivate() { + if (ProductInfo.getBetaEdition()) { + if (TraceComponent.isAnyTracingEnabled() && tc.isEventEnabled()) { + Tr.event(this, tc, "Deactivating OpenAPIAppConfigProviderImpl"); + } + config = null; + } + } + + private void betaFenceCheck() throws UnsupportedOperationException { + // Not running beta edition, throw exception + if (!ProductInfo.getBetaEdition()) { + throw new UnsupportedOperationException("This method is beta and is not available."); + } else { + // Running beta exception, issue message if we haven't already issued one for this class + if (!issuedBetaMessage) { + Tr.info(tc, "BETA: A beta method has been invoked for the class " + this.getClass().getName() + " for the first time."); + issuedBetaMessage = !issuedBetaMessage; + } + } + } + + /** {@inheritDoc} */ + @Override + @Deprecated + public MpConfigServerConfigObject getConfiguration() { + betaFenceCheck(); + return (config); + } + + @Override + public void registerAppConfigListener(OpenAPIAppConfigListener listener) { + openAPIAppConfigListeners.add(listener); + } + + @Override + public void unregisterAppConfigListener(OpenAPIAppConfigListener listener) { + openAPIAppConfigListeners.remove(listener); + } + + private static boolean isNotValidModuleName(String elementName) { + if (elementName.indexOf('/') <= 0) { + Tr.warning(tc, INVALID_MODULE_WARNING, elementName, elementName); + return true; + } + return false; + } + + private static boolean isNotValidAppName(String elementName) { + if (elementName.indexOf('/') > 0) { + Tr.warning(tc, INVALID_APP_WARNING, elementName, elementName); + return true; + } + return false; + } + + private static class MpConfigServerConfigObject implements OpenAPIServerXMLConfig { + + private final List includedApps = new ArrayList(); + private final List excludedApps = new ArrayList(); + + private final List includedModules = new ArrayList(); + private final List excludedModules = new ArrayList(); + + private Optional configMode = Optional.empty(); + + private MpConfigServerConfigObject(Map properties) { + //Read the server.xml + Optional includedAppArray = Optional.ofNullable((String[]) properties.get(INCLUDE_APP_PROPERTY_NAME)); + Optional excludedAppArray = Optional.ofNullable((String[]) properties.get(EXCLUDE_APP_PROPERTY_NAME)); + Optional includedModulesArray = Optional.ofNullable((String[]) properties.get(INCLUDE_MODULE_PROPERTY_NAME)); + Optional excludedModulesArray = Optional.ofNullable((String[]) properties.get(EXCLUDE_MODULE_PROPERTY_NAME)); + + Optional> includedAppList = includedAppArray.map((String[] array) -> new ArrayList<>(Arrays.asList(array))); + Optional> excludedAppList = excludedAppArray.map((String[] array) -> new ArrayList<>(Arrays.asList(array))); + Optional> includedModulesList = includedModulesArray.map((String[] array) -> new ArrayList<>(Arrays.asList(array))); + Optional> excludedModulesList = excludedModulesArray.map((String[] array) -> new ArrayList<>(Arrays.asList(array))); + + //Send a warning message and filter if the contents are malformed + includedAppList.ifPresent((List list) -> list.removeIf(OpenAPIAppConfigProviderImpl::isNotValidAppName)); + excludedAppList.ifPresent((List list) -> list.removeIf(OpenAPIAppConfigProviderImpl::isNotValidAppName)); + includedModulesList.ifPresent((List list) -> list.removeIf(OpenAPIAppConfigProviderImpl::isNotValidModuleName)); + excludedModulesList.ifPresent((List list) -> list.removeIf(OpenAPIAppConfigProviderImpl::isNotValidModuleName)); + + if (includedAppList.isPresent() && includedAppList.get().size() == 1) { + String includedString = includedAppList.get().get(0).toLowerCase().trim(); + if (includedString.equals("all")) { + configMode = Optional.of(ConfigMode.All); + includedAppList = Optional.empty(); + } else if (includedString.equals("first")) { + configMode = Optional.of(ConfigMode.First); + includedAppList = Optional.empty(); + } else if (includedString.equals("none")) { + configMode = Optional.of(ConfigMode.None); + includedAppList = Optional.empty(); + } + } + + //store outside of an optional because merging two optional lists is convoluted + includedApps.addAll(includedAppList.orElse(Collections.emptyList())); + excludedApps.addAll(excludedAppList.orElse(Collections.emptyList())); + includedModules.addAll(includedModulesList.orElse(Collections.emptyList())); + excludedModules.addAll(excludedModulesList.orElse(Collections.emptyList())); + + if (TraceComponent.isAnyTracingEnabled() && tc.isEventEnabled()) { + Tr.event(tc, "OpenAPI finished processing modules from server.xml." + + " found the following configuration for included apps {0}," + + " found the following configuration for excluded apps {1}," + + " found the following configuration for included modules {2}," + + " found the following configuration for excluded modules {3}", + includedApps, excludedApps, includedModules, excludedModules); + } + } + + @Override + public boolean equals(Object other) { + if (other == null) { + return false; + } + if (other == this) { + return true; + } + if (!(other instanceof MpConfigServerConfigObject)) { + return false; + } + + MpConfigServerConfigObject otherConfig = (MpConfigServerConfigObject) other; + + return (includedApps.equals(otherConfig.includedApps) + && excludedApps.equals(otherConfig.excludedModules) + && includedModules.equals(otherConfig.includedModules) + && excludedModules.equals(otherConfig.excludedModules) + && configMode.equals(otherConfig.configMode)); + } + + @Override + public Optional> getIncludedAppsAndModules() { + List combinedList = new ArrayList(includedApps); + combinedList.addAll(includedModules); + return combinedList.isEmpty() ? Optional.> empty() : Optional.of(Collections.unmodifiableList(combinedList)); + } + + @Override + public Optional> getExcludedAppsAndModules() { + List combinedList = new ArrayList(excludedApps); + combinedList.addAll(excludedModules); + return combinedList.isEmpty() ? Optional.> empty() : Optional.of(Collections.unmodifiableList(combinedList)); + } + + @Override + public Optional getConfigMode() { + return configMode; + } + + @Override + public boolean wasAnyConfigFound() { + return (includedApps.size() > 0 + || excludedApps.size() > 0 + || includedModules.size() > 0 + || excludedModules.size() > 0 + || configMode.isPresent()); + } + } + +} diff --git a/dev/io.openliberty.microprofile.openapi.internal.common/src/io/openliberty/microprofile/openapi/internal/common/services/OpenAPIAppConfigProvider.java b/dev/io.openliberty.microprofile.openapi.internal.common/src/io/openliberty/microprofile/openapi/internal/common/services/OpenAPIAppConfigProvider.java new file mode 100644 index 000000000000..5a7284f6c304 --- /dev/null +++ b/dev/io.openliberty.microprofile.openapi.internal.common/src/io/openliberty/microprofile/openapi/internal/common/services/OpenAPIAppConfigProvider.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * Copyright (c) 2023 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ******************************************************************************/ + +package io.openliberty.microprofile.openapi.internal.common.services; + +public interface OpenAPIAppConfigProvider { + + /** + * Returns all configuration from the server.xml's mpOpenAPI element + * + * @return An object containing mpOpenAPI configuration + */ + OpenAPIServerXMLConfig getConfiguration(); + + public static interface OpenAPIAppConfigListener { + /** + * This method will be called whenever the server.xml is updated and that update changes the mpOpenAPI include or exclude statements + */ + public void processConfigUpdate(); + + /** + * Get the priority of this listener, higher priorities must be run first + */ + public int getConfigListenerPriority(); + } + + /** + * @param listener + */ + void registerAppConfigListener(OpenAPIAppConfigListener listener); + + /** + * @param listener + */ + void unregisterAppConfigListener(OpenAPIAppConfigListener listener); +} diff --git a/dev/io.openliberty.microprofile.openapi.internal.common/src/io/openliberty/microprofile/openapi/internal/common/services/OpenAPIServerXMLConfig.java b/dev/io.openliberty.microprofile.openapi.internal.common/src/io/openliberty/microprofile/openapi/internal/common/services/OpenAPIServerXMLConfig.java new file mode 100644 index 000000000000..b8e39afb39e3 --- /dev/null +++ b/dev/io.openliberty.microprofile.openapi.internal.common/src/io/openliberty/microprofile/openapi/internal/common/services/OpenAPIServerXMLConfig.java @@ -0,0 +1,50 @@ +package io.openliberty.microprofile.openapi.internal.common.services; + +import java.util.List; +import java.util.Optional; + +/** + * An Object that contains the mpOpenAPI config from server.xml + */ +public interface OpenAPIServerXMLConfig { + + /** + * Retrieve a list of string representing the included modules. + * Valid entries are in the format applicationName and/or + * applicationName/moduleName. + * + * @return A list of included modules or an empty optional if there are none. + */ + Optional> getIncludedAppsAndModules(); + + /** + * Retrieve a list of string representing the excluded modules. + * Valid entries are in the format applicationName and/or + * applicationName/moduleName. + * + * @return A list of excluded modules or an empty optional if there are none. + */ + Optional> getExcludedAppsAndModules(); + + /** + * If the only includes statement in server.xml is "all", "first", or "none" this is a special keyword that changes the mode + * of OpenApi. This method will retrieve the config mode if it was set, otherwise it returns an empty optional. + * + * @return And Optional containing the config mode if one was explicitly set, otherwise an empty optional + */ + Optional getConfigMode(); + + /** + * Was any config for mpOpenAPI found in the server.xml, this is equivalent to calling isPresent on all preceding methods + * and returning true if any are true. + * + * @return true if we found server.xml config, otherwise false; + */ + boolean wasAnyConfigFound(); + + public enum ConfigMode { + First, + All, + None + } +}