SLING-11864 Move web console plugin (#10)

Optionally remove metatype default properties, DS component properties
and merged properties
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>
@@ -85,6 +90,11 @@
         </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>
             <scope>provided</scope>
         </dependency>
@@ -119,6 +129,11 @@
             <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>
             <scope>test</scope>
diff --git a/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigInstallTask.java b/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigInstallTask.java
index 152633e..3e54698 100644
--- a/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigInstallTask.java
+++ b/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigInstallTask.java
@@ -21,10 +21,7 @@
 import java.io.IOException;
 import java.text.MessageFormat;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.Dictionary;
-import java.util.Enumeration;
-import java.util.Hashtable;
 import java.util.Iterator;
 import java.util.List;
 
diff --git a/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigTaskCreator.java b/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigTaskCreator.java
index e4bd388..a96abc6 100644
--- a/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigTaskCreator.java
+++ b/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigTaskCreator.java
@@ -21,7 +21,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Dictionary;
-import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.Hashtable;
 import java.util.List;
@@ -207,16 +206,7 @@
         if ( Activator.MERGE_SCHEMES != null ) {
             final Dictionary<String, Object> defaultProps = getDefaultProperties(infoProvider, pid);
             if ( defaultProps != null ) {
-                final Enumeration<String> keyEnum = defaultProps.keys();
-                while ( keyEnum.hasMoreElements() ) {
-                    final String key = keyEnum.nextElement();
-                    final Object value = defaultProps.get(key);
-
-                    final Object newValue = dict.get(key);
-                    if ( newValue != null && ConfigUtil.isSameValue(newValue, value)) {
-                        dict.remove(key);
-                    }
-                }
+                ConfigUtil.removeRedundantProperties(dict, defaultProps);
             }
         }
     }
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..738008e 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 @@
     }
 
     public static boolean isSameValue(final Object valA, final Object valB) {
+        if (valA == null && valB == null) {
+            return true;
+        }
+        if (valA == null || valB == null) {
+            return false;
+        }
         if ( valA.getClass().isArray() && valB.getClass().isArray()) {
             final Object[] arrA = convertToObjectArray(valA);
             final Object[] arrB = convertToObjectArray(valB);
@@ -312,4 +318,22 @@
             base.put(key, props.get(key));
         }
     }
+
+    /**
+     * Removes all properties existing with same key and value in {@code base} from {@code properties}
+     * @param properties the properties to check and modify
+     * @param base the base to compare with
+     */
+    public static void removeRedundantProperties(final Dictionary<String, Object> properties, final Dictionary<String, Object> base ) {
+        final Enumeration<String> keyEnum = base.keys();
+        while ( keyEnum.hasMoreElements() ) {
+            final String key = keyEnum.nextElement();
+            final Object value = base.get(key);
+
+            final Object newValue = properties.get(key);
+            if ( newValue != null && isSameValue(newValue, value)) {
+                properties.remove(key);
+            }
+        }
+    }
 }
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..307bca3
--- /dev/null
+++ b/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigurationSerializerWebConsolePlugin.java
@@ -0,0 +1,399 @@
+/*
+ * 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.net.URL;
+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.Hashtable;
+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.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.service.component.annotations.Activate;
+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.osgi.service.metatype.MetaTypeService;
+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_COMPONENT_DEFAULT_PROPERTIES = "removeComponentDefaultProps";
+    private static final String PARAMETER_REMOVE_METATYPE_DEFAULT_PROPERTIES = "removeMetatypeDefaultProps";
+    private static final String PARAMETER_REMOVE_MERGED_DEFAULT_PROPERTIES = "removeMergedDefaultProps";
+
+    /** The logger */
+    private final Logger LOGGER =  LoggerFactory.getLogger(ConfigurationSerializerWebConsolePlugin.class);
+
+    @Reference
+    ConfigurationAdmin configurationAdmin;
+
+    @Reference
+    MetaTypeService metatypeService;
+
+    @Reference
+    private InfoProvider infoProvider;
+
+    @Reference
+    private ServiceComponentRuntime scr;
+
+    private final BundleContext bundleContext;
+
+    @Activate
+    public ConfigurationSerializerWebConsolePlugin(BundleContext bundleContext) {
+        this.bundleContext = bundleContext;
+    }
+
+    @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 removeMetatypeDefaultProperties;
+        final boolean removeComponentDefaultProperties;
+        final boolean removeMergedDefaultProperties;
+        // initial loading?
+        if (format == null) {
+            removeMetatypeDefaultProperties = true;
+            removeComponentDefaultProperties = true;
+            removeMergedDefaultProperties = true;
+        } else {
+            removeMetatypeDefaultProperties = Boolean.parseBoolean(request.getParameter(PARAMETER_REMOVE_METATYPE_DEFAULT_PROPERTIES));
+            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();
+        }
+        
+        MetatypeHandler metatypeHandler = new MetatypeHandler(metatypeService, bundleContext);
+        Dictionary<String, Object> mergedProperties = ConfigTaskCreator.getDefaultProperties(infoProvider, pid);
+        if (mergedProperties == null) {
+            mergedProperties = new Hashtable<>();
+        }
+        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' minlength='3'>");
+        closeTd(pw);
+        closeTr(pw);
+
+        tr(pw);
+        tdLabel(pw, "Remove Properties");
+        tdContent(pw);
+
+        pw.print("<input type='checkbox' name='");
+        pw.print(PARAMETER_REMOVE_METATYPE_DEFAULT_PROPERTIES);
+        pw.print("'");
+        if ( removeMetatypeDefaultProperties ) {
+            pw.print(" checked");
+        }
+        
+        pw.println(" id='");
+        pw.print(PARAMETER_REMOVE_METATYPE_DEFAULT_PROPERTIES);
+        pw.println("' class='input' value='true'>");
+        pw.println("<label for='");
+        pw.println(PARAMETER_REMOVE_METATYPE_DEFAULT_PROPERTIES);
+        pw.println("'>Metatype Default Properties</label>");
+
+        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>");
+        }
+        pw.println("<p>Selecting any of these options strips those properties which have the same name and value as one from any of the selected sources. The removed properties are very likely being redundant and therefore do not need to be added to serialized configs.</a>");
+        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 (removeMetatypeDefaultProperties) {
+                        metatypeHandler.updateConfiguration(configuration.getFactoryPid(), configuration.getPid(), properties, mergedProperties);
+                    }
+                    if (removeComponentDefaultProperties) {
+                        removeComponentDefaultProperties(allComponentDescriptions, configuration.getPid(), configuration.getFactoryPid(), properties, mergedProperties);
+                    }
+                    if (removeMergedDefaultProperties) {
+                        ConfigUtil.removeRedundantProperties(properties, mergedProperties);
+                    }
+                    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
+     * and which are not set in the merged (i.e. inherited) properties.
+     * @param componentDescriptions
+     * @param pid
+     * @param factoryPid
+     * @param dict
+     * @param mergedProperties
+     */
+    private void removeComponentDefaultProperties(final Collection<ComponentDescriptionDTO> componentDescriptions, final String pid, final String factoryPid, final Dictionary<String, Object> dict, final Dictionary<String, Object> mergedProperties) {
+        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)) 
+                                   && mergedProperties.get(key) == null)) {
+                dict.remove(key);
+            }
+        }
+    }
+
+    String getRelativeResourcePrefix() {
+        return RES_LOC;
+    }
+
+    /**
+     * Method to retrieve static resources from this bundle.
+     */
+    @SuppressWarnings("unused")
+    private URL getResource(final String path) {
+        if (path.startsWith("/" + getRelativeResourcePrefix())) {
+            // strip label
+            int index = path.indexOf('/', 1);
+            if (index <= 0) {
+                throw new IllegalStateException("The relativeResourcePrefix must contain at least one '/'");
+            }
+            return this.getClass().getResource(path.substring(index));
+        }
+        return null;
+    }
+}
diff --git a/src/main/java/org/apache/sling/installer/factories/configuration/impl/MetatypeHandler.java b/src/main/java/org/apache/sling/installer/factories/configuration/impl/MetatypeHandler.java
index 883643c..7fd3feb 100644
--- a/src/main/java/org/apache/sling/installer/factories/configuration/impl/MetatypeHandler.java
+++ b/src/main/java/org/apache/sling/installer/factories/configuration/impl/MetatypeHandler.java
@@ -35,8 +35,8 @@
 
     private final BundleContext bundleContext;
 
-    public MetatypeHandler(final Object mts, final BundleContext bundleContext) {
-        this.srv = (MetaTypeService)mts;
+    public MetatypeHandler(final MetaTypeService mts, final BundleContext bundleContext) {
+        this.srv = mts;
         this.bundleContext = bundleContext;
     }
     
diff --git a/src/main/java/org/apache/sling/installer/factories/configuration/impl/WebconsoleConfigurationHandler.java b/src/main/java/org/apache/sling/installer/factories/configuration/impl/WebconsoleConfigurationHandler.java
index 3aa0161..7563347 100644
--- a/src/main/java/org/apache/sling/installer/factories/configuration/impl/WebconsoleConfigurationHandler.java
+++ b/src/main/java/org/apache/sling/installer/factories/configuration/impl/WebconsoleConfigurationHandler.java
@@ -25,23 +25,22 @@
 import org.apache.felix.webconsole.spi.ValidationException;
 import org.apache.sling.installer.api.info.InfoProvider;
 import org.osgi.framework.BundleContext;
+import org.osgi.service.metatype.MetaTypeService;
 import org.osgi.util.tracker.ServiceTracker;
 
 public class WebconsoleConfigurationHandler implements ConfigurationHandler {
 
-    static final String META_TYPE_NAME = "org.osgi.service.metatype.MetaTypeService"; 
-
     private final InfoProvider infoProvider;
 
-    private final ServiceTracker<Object, Object> metatypeTracker;
+    private final ServiceTracker<MetaTypeService, MetaTypeService> metatypeTracker;
 
     private final BundleContext bundleContext;
 
     public WebconsoleConfigurationHandler(final BundleContext context, final InfoProvider infoProvider) {
         this.infoProvider = infoProvider;
         this.bundleContext = context;
-        this.metatypeTracker = new ServiceTracker<>(context, META_TYPE_NAME, null);
-        this.metatypeTracker.open();    
+        this.metatypeTracker = new ServiceTracker<>(context, MetaTypeService.class, null);
+        this.metatypeTracker.open();
     }
 
     public void deactivate() {
@@ -66,12 +65,12 @@
     @Override
     public void updateConfiguration(final String factoryPid, final String pid, final Dictionary<String, Object> props)
             throws ValidationException, IOException {
-        final Object mts = this.metatypeTracker.getService();
+        final MetaTypeService mts = this.metatypeTracker.getService();
         if ( mts != null ) {
             final Dictionary<String, Object> defaultProps = ConfigTaskCreator.getDefaultProperties(infoProvider, pid);
             final MetatypeHandler mt = new MetatypeHandler(mts, this.bundleContext);
             mt.updateConfiguration(factoryPid, pid, props, defaultProps);
-        }        
+        }
     }
     
 }
diff --git a/src/main/resources/res/ui/clipboard.js b/src/main/resources/res/ui/clipboard.js
new file mode 100644
index 0000000..197f933
--- /dev/null
+++ b/src/main/resources/res/ui/clipboard.js
@@ -0,0 +1,31 @@
+/**
+ * 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.
+ */
+function copyToClipboard(inputElement) {
+  inputElement.select();
+  document.execCommand("copy");
+}
+
+window.onload = function() {
+  button = document.querySelector("#copy");
+  if (button != null) {
+    button.addEventListener("click", function() {
+      copyToClipboard(document.querySelector("#output"));
+    });
+  }
+}