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("&nbsp;&nbsp;<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("&amp;");
+            } else if(c == '<') {
+                b.append("&lt;");
+            } else if(c == '>') {
+                b.append("&gt;");
+            } else if(c == '"') {
+                b.append("&quot;");
+            } else if(c == '\'') {
+                b.append("&apos;");
+            } 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);
+            }
+        }
+    }
+}

Reply via email to