This is an automated email from the ASF dual-hosted git repository. kwin pushed a commit to branch feature/SLING-11864-move-webconsole-plugin-and-consider-merged-configs in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-installer-factory-configuration.git
commit 66d0e6d00c5c48690a4a9b7dc9ed927c48f55749 Author: Konrad Windszus <[email protected]> AuthorDate: Wed May 10 11:53:40 2023 +0200 SLING-11864 Move web console plugin Optionally remove DS component properties and merged properties --- pom.xml | 15 + .../factories/configuration/impl/ConfigUtil.java | 6 + .../ConfigurationSerializerWebConsolePlugin.java | 356 +++++++++++++++++++++ 3 files changed, 377 insertions(+) diff --git a/pom.xml b/pom.xml index 0ebfccd..8dac8bd 100644 --- a/pom.xml +++ b/pom.xml @@ -72,6 +72,11 @@ <artifactId>org.osgi.annotation.bundle</artifactId> <scope>provided</scope> </dependency> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.service.component.annotations</artifactId> + <scope>provided</scope> + </dependency> <!-- OSGi framework 1.8, Core R6 (https://osgi.org/javadoc/r6/core/org/osgi/framework/package-summary.html) --> <dependency> <groupId>org.slf4j</groupId> @@ -83,6 +88,11 @@ <artifactId>osgi.core</artifactId> <scope>provided</scope> </dependency> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.service.component</artifactId> + <scope>provided</scope> + </dependency> <dependency> <groupId>org.osgi</groupId> <artifactId>org.osgi.service.cm</artifactId> @@ -118,6 +128,11 @@ <version>1.0.0</version> <scope>provided</scope> </dependency> + <dependency> + <groupId>javax.servlet</groupId> + <artifactId>javax.servlet-api</artifactId> + <scope>provided</scope> + </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> diff --git a/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigUtil.java b/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigUtil.java index 30f3f50..f08d71f 100644 --- a/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigUtil.java +++ b/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigUtil.java @@ -112,6 +112,12 @@ abstract class ConfigUtil { } public static boolean isSameValue(final Object valA, final Object valB) { + if (valA == null && valB == null) { + return false; + } + if (valA == null || valB == null) { + return false; + } if ( valA.getClass().isArray() && valB.getClass().isArray()) { final Object[] arrA = convertToObjectArray(valA); final Object[] arrB = convertToObjectArray(valB); diff --git a/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigurationSerializerWebConsolePlugin.java b/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigurationSerializerWebConsolePlugin.java new file mode 100644 index 0000000..e5e8f51 --- /dev/null +++ b/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigurationSerializerWebConsolePlugin.java @@ -0,0 +1,356 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.installer.factories.configuration.impl; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.servlet.GenericServlet; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.apache.sling.installer.api.info.InfoProvider; +import org.apache.sling.installer.api.serializer.ConfigurationSerializerFactory; +import org.apache.sling.installer.api.serializer.ConfigurationSerializerFactory.Format; +import org.osgi.framework.Constants; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.runtime.ServiceComponentRuntime; +import org.osgi.service.component.runtime.dto.ComponentDescriptionDTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Component(service=javax.servlet.Servlet.class, + property = { + Constants.SERVICE_VENDOR + "=The Apache Software Foundation", + Constants.SERVICE_DESCRIPTION + "=Apache Sling OSGi Installer Configuration Serializer Web Console Plugin", + "felix.webconsole.label=" + ConfigurationSerializerWebConsolePlugin.LABEL, + "felix.webconsole.title=OSGi Installer Configuration Printer", + "felix.webconsole.category=OSGi" + }) +@SuppressWarnings("serial") +public class ConfigurationSerializerWebConsolePlugin extends GenericServlet { + + public static final String LABEL = "osgi-installer-config-printer"; + private static final String RES_LOC = LABEL + "/res/ui/"; + private static final String PARAMETER_PID = "pid"; + private static final String PARAMETER_FORMAT = "format"; + private static final String PARAMETER_REMOVE_MERGED_DEFAULT_PROPERTIES = "removeMergedDefaultProps"; + private static final String PARAMETER_REMOVE_COMPONENT_DEFAULT_PROPERTIES = "removeComponentDefaultProps"; + + // copied from org.apache.sling.installer.factories.configuration.impl.ConfigUtil + /** + * This property has been used in older versions to keep track where the + * configuration has been installed from. + */ + private static final String CONFIG_PATH_KEY = "org.apache.sling.installer.osgi.path"; + + /** + * This property has been used in older versions to keep track of factory + * configurations. + */ + private static final String ALIAS_KEY = "org.apache.sling.installer.osgi.factoryaliaspid"; + + /** Configuration properties to ignore when printing */ + private static final Set<String> IGNORED_PROPERTIES = new HashSet<>(); + static { + IGNORED_PROPERTIES.add(Constants.SERVICE_PID); + IGNORED_PROPERTIES.add(CONFIG_PATH_KEY); + IGNORED_PROPERTIES.add(ALIAS_KEY); + IGNORED_PROPERTIES.add(ConfigurationAdmin.SERVICE_FACTORYPID); + } + + /** The logger */ + private final Logger LOGGER = LoggerFactory.getLogger(ConfigurationSerializerWebConsolePlugin.class); + + @Reference + ConfigurationAdmin configurationAdmin; + + @Reference + private InfoProvider infoProvider; + + @Reference + private ServiceComponentRuntime scr; + + @Override + public void service(final ServletRequest request, final ServletResponse response) + throws IOException { + + final String pid = request.getParameter(PARAMETER_PID); + final String format = request.getParameter(PARAMETER_FORMAT); + // initial loading + final boolean removeComponentDefaultProperties; + final boolean removeMergedDefaultProperties; + // initial loading of tab? + if (format == null) { + removeComponentDefaultProperties = true; + removeMergedDefaultProperties = true; + } else { + removeComponentDefaultProperties = Boolean.parseBoolean(request.getParameter(PARAMETER_REMOVE_COMPONENT_DEFAULT_PROPERTIES)); + removeMergedDefaultProperties = Boolean.parseBoolean(request.getParameter(PARAMETER_REMOVE_MERGED_DEFAULT_PROPERTIES)); + } + Collection<ComponentDescriptionDTO> allComponentDescriptions; + if (removeComponentDefaultProperties) { + allComponentDescriptions = scr.getComponentDescriptionDTOs(); + } else { + allComponentDescriptions = Collections.emptyList(); + } + ConfigurationSerializerFactory.Format serializationFormat = Format.JSON; + if (format != null && !format.trim().isEmpty()) { + try { + serializationFormat = ConfigurationSerializerFactory.Format.valueOf(format); + } catch (IllegalArgumentException e) { + LOGGER.warn("Illegal parameter 'format' given, falling back to default '{}'", serializationFormat, e); + } + } + final PrintWriter pw = response.getWriter(); + + pw.println("<script type=\"text/javascript\" src=\"" + RES_LOC + "clipboard.js\"></script>"); + pw.print("<form method='get'>"); + pw.println("<table class='content' cellpadding='0' cellspacing='0' width='100%'>"); + + titleHtml( + pw, + "OSGi Installer Configuration Printer", + "To emit the configuration properties just enter the configuration PID, select a <a href='https://sling.apache.org/documentation/bundles/configuration-installer-factory.html'>serialization format</a> and click 'Print'"); + + tr(pw); + tdLabel(pw, "PID"); + tdContent(pw); + + pw.print("<input type='text' name='"); + pw.print(PARAMETER_PID); + pw.print("' value='"); + if ( pid != null ) { + pw.print(escapeXml(pid)); + } + + pw.println("' class='input' size='120'>"); + closeTd(pw); + closeTr(pw); + + tr(pw); + tdLabel(pw, "Remove Properties"); + tdContent(pw); + + pw.print("<input type='checkbox' name='"); + pw.print(PARAMETER_REMOVE_COMPONENT_DEFAULT_PROPERTIES); + pw.print("'"); + if ( removeComponentDefaultProperties ) { + pw.print(" checked"); + } + + pw.println(" id='"); + pw.print(PARAMETER_REMOVE_COMPONENT_DEFAULT_PROPERTIES); + pw.println("' class='input' value='true'>"); + pw.println("<label for='"); + pw.println(PARAMETER_REMOVE_COMPONENT_DEFAULT_PROPERTIES); + pw.println("'>Declarative Services Component Properties</label>"); + + if (Activator.MERGE_SCHEMES != null) { + pw.print("<input type='checkbox' name='"); + pw.print(PARAMETER_REMOVE_MERGED_DEFAULT_PROPERTIES); + pw.print("'"); + if ( removeMergedDefaultProperties ) { + pw.print(" checked"); + } + + pw.println(" id='"); + pw.print(PARAMETER_REMOVE_MERGED_DEFAULT_PROPERTIES); + pw.println("' class='input' value='true'>"); + pw.println("<label for='"); + pw.println(PARAMETER_REMOVE_MERGED_DEFAULT_PROPERTIES); + pw.println("'>Merged Properties</label>"); + } + closeTd(pw); + closeTr(pw); + + tr(pw); + tdLabel(pw, "Serialization Format"); + tdContent(pw); + pw.print("<select name='"); + pw.print(PARAMETER_FORMAT); + pw.println("'>"); + option(pw, "JSON", "OSGi Configurator JSON", format); + option(pw, "CONFIG", "Apache Felix Config", format); + option(pw, "PROPERTIES", "Java Properties", format); + option(pw, "PROPERTIES_XML", "Java Properties (XML)", format); + pw.println("</select>"); + + pw.println(" <input type='submit' value='Print' class='submit'>"); + + closeTd(pw); + closeTr(pw); + + if (pid != null && !pid.trim().isEmpty()) { + tr(pw); + tdLabel(pw, "Serialized Configuration Properties"); + tdContent(pw); + + Configuration configuration = configurationAdmin.getConfiguration(pid, null); + Dictionary<String, Object> properties = configuration.getProperties(); + if (properties == null) { + pw.print("<p class='ui-state-error-text'>"); + pw.print("No configuration properties for pid '" + escapeXml(pid) + "' found!"); + pw.println("</p>"); + } else { + properties = ConfigUtil.cleanConfiguration(properties); + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + if (removeMergedDefaultProperties) { + ConfigTaskCreator.removeDefaultProperties(infoProvider, configuration.getPid(), properties); + } + if (removeComponentDefaultProperties) { + removeComponentDefaultProperties(allComponentDescriptions, configuration.getPid(), configuration.getFactoryPid(), properties); + } + ConfigurationSerializerFactory.create(serializationFormat).serialize(properties, baos); + pw.println("<textarea rows=\"20\" cols=\"120\" id=\"output\" readonly>"); + pw.print(new String(baos.toByteArray(), StandardCharsets.UTF_8)); + pw.println("</textarea>"); + pw.println("<button type='button' id='copy'>Copy to Clipboard</a>"); + } catch (Exception e) { + pw.print("<p class='ui-state-error-text'>"); + pw.print("Error serializing pid '" + escapeXml(pid) + "': " + e.getMessage()); + pw.println("</p>"); + LOGGER.warn("Error serializing pid '{}'", pid, e); + } + } + closeTd(pw); + closeTr(pw); + } + + pw.println("</table>"); + pw.print("</form>"); + } + + private void tdContent(final PrintWriter pw) { + pw.print("<td class='content' colspan='2'>"); + } + + private void closeTd(final PrintWriter pw) { + pw.print("</td>"); + } + + private void closeTr(final PrintWriter pw) { + pw.println("</tr>"); + } + + private void tdLabel(final PrintWriter pw, final String label) { + pw.print("<td class='content'>"); + pw.print(label); + pw.println("</td>"); + } + + private void tr(final PrintWriter pw) { + pw.println("<tr class='content'>"); + } + + private void option(final PrintWriter pw, String value, String label, String selectedValue) { + pw.print("<option value='"); + pw.print(value); + pw.print("'"); + if (value.equals(selectedValue)) { + pw.print(" selected"); + } + pw.print(">"); + pw.print(label); + pw.println("</option>"); + } + + private void titleHtml(final PrintWriter pw, final String title, final String description) { + tr(pw); + pw.print("<th colspan='3' class='content container'>"); + pw.print(escapeXml(title)); + pw.println("</th>"); + closeTr(pw); + + if (description != null) { + tr(pw); + pw.print("<td colspan='3' class='content'>"); + pw.print(description); + pw.println("</th>"); + closeTr(pw); + } + } + + /** + * Copied from org.apache.sling.api.request.ResponseUtil + * Escape XML text + * @param input The input text + * @return The escaped text + */ + protected String escapeXml(final String input) { + if (input == null) { + return null; + } + + final StringBuilder b = new StringBuilder(input.length()); + for(int i = 0;i < input.length(); i++) { + final char c = input.charAt(i); + if(c == '&') { + b.append("&"); + } else if(c == '<') { + b.append("<"); + } else if(c == '>') { + b.append(">"); + } else if(c == '"') { + b.append("""); + } else if(c == '\'') { + b.append("'"); + } else { + b.append(c); + } + } + return b.toString(); + } + + /** + * Removes all configuration properties from the given dictionary whose values are equal to all connected DS component properties set in the descriptor. + * @param componentDescriptions + * @param pid + * @param factoryPid + * @param dict + */ + private void removeComponentDefaultProperties(final Collection<ComponentDescriptionDTO> componentDescriptions, final String pid, final String factoryPid, final Dictionary<String, Object> dict) { + String effectivePid = factoryPid != null ? factoryPid : pid; + Collection<ComponentDescriptionDTO> relevantComponentDescriptions = componentDescriptions.stream() + // find all with a matching pid + .filter(c -> Arrays.asList(c.configurationPid).contains(effectivePid)).collect(Collectors.toList()); + + final Enumeration<String> e = dict.keys(); + while(e.hasMoreElements()) { + final String key = e.nextElement(); + final Object newValue = dict.get(key); + if (relevantComponentDescriptions.stream().allMatch(c -> ConfigUtil.isSameValue(newValue, c.properties.get(key)))) { + dict.remove(key); + } + } + } +}
