This is an automated email from the ASF dual-hosted git repository.

cziegeler pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/felix-dev.git


The following commit(s) were added to refs/heads/master by this push:
     new d600205  FELIX-6367 : Provide SPI for configuration management (WiP)
d600205 is described below

commit d6002050b461cf58458b1d52509cac8be5238fe9
Author: Carsten Ziegeler <[email protected]>
AuthorDate: Sun Aug 29 10:32:45 2021 +0200

    FELIX-6367 : Provide SPI for configuration management (WiP)
---
 webconsole/pom.xml                                 |   5 +-
 .../internal/configuration/ConfigAdminSupport.java | 750 ++-------------------
 .../internal/configuration/ConfigJsonSupport.java  | 494 ++++++++++++++
 .../internal/configuration/ConfigManager.java      |  64 +-
 .../internal/configuration/ConfigurationUtil.java  | 150 +++++
 .../webconsole/internal/misc/ServletSupport.java   |  51 ++
 .../felix/webconsole/spi/ConfigurationHandler.java |  64 ++
 .../felix/webconsole/spi/ValidationException.java  |  32 +
 .../apache/felix/webconsole/spi/package-info.java  |  21 +
 9 files changed, 903 insertions(+), 728 deletions(-)

diff --git a/webconsole/pom.xml b/webconsole/pom.xml
index 864195b..7485a13 100644
--- a/webconsole/pom.xml
+++ b/webconsole/pom.xml
@@ -41,8 +41,9 @@
         <webconsole.exports>
             org.apache.felix.webconsole;provide:=true,
             org.apache.felix.webconsole.bundleinfo;provide:=true,
-            org.apache.felix.webconsole.i18n;provide:=true
-        </webconsole.exports>
+            org.apache.felix.webconsole.i18n;provide:=true,
+            org.apache.felix.webconsole.spi
+       </webconsole.exports>
     </properties>
 
     <scm>
diff --git 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/configuration/ConfigAdminSupport.java
 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/configuration/ConfigAdminSupport.java
index b876f26..e7b7aad 100644
--- 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/configuration/ConfigAdminSupport.java
+++ 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/configuration/ConfigAdminSupport.java
@@ -20,8 +20,6 @@ package org.apache.felix.webconsole.internal.configuration;
 
 
 import java.io.IOException;
-import java.io.PrintWriter;
-import java.lang.reflect.Array;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Dictionary;
@@ -29,41 +27,26 @@ import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Hashtable;
-import java.util.Iterator;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
-import java.util.Map.Entry;
 import java.util.Set;
-import java.util.SortedMap;
-import java.util.TreeMap;
 import java.util.Vector;
-import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import javax.servlet.http.HttpServletRequest;
 
-import org.apache.felix.utils.json.JSONWriter;
-import org.apache.felix.webconsole.internal.Util;
-import org.osgi.framework.Bundle;
-import org.osgi.framework.BundleContext;
+import org.apache.felix.webconsole.internal.misc.ServletSupport;
+import org.apache.felix.webconsole.spi.ConfigurationHandler;
 import org.osgi.framework.Constants;
-import org.osgi.framework.Filter;
 import org.osgi.framework.InvalidSyntaxException;
-import org.osgi.framework.ServiceReference;
-import org.osgi.framework.Version;
 import org.osgi.service.cm.Configuration;
 import org.osgi.service.cm.ConfigurationAdmin;
-import org.osgi.service.cm.ManagedService;
-import org.osgi.service.cm.ManagedServiceFactory;
 import org.osgi.service.metatype.AttributeDefinition;
-import org.osgi.service.metatype.ObjectClassDefinition;
 
 
-class ConfigAdminSupport
-{
+class ConfigAdminSupport {
 
-    private static final String PROPERTY_FACTORYCONFIG_NAMEHINT = 
"webconsole.configurationFactory.nameHint";
+    public static final String PROPERTY_FACTORYCONFIG_NAMEHINT = 
"webconsole.configurationFactory.nameHint";
     public static final Set<String> CONFIG_PROPERTIES_HIDE = new HashSet<>();
     static {
         CONFIG_PROPERTIES_HIDE.add(PROPERTY_FACTORYCONFIG_NAMEHINT);
@@ -71,97 +54,42 @@ class ConfigAdminSupport
         CONFIG_PROPERTIES_HIDE.add(ConfigurationAdmin.SERVICE_FACTORYPID);
         CONFIG_PROPERTIES_HIDE.add(Constants.SERVICE_PID);
     }
-    private static final Pattern NAMEHINT_PLACEHOLER_REGEXP = 
Pattern.compile("\\{([^\\{\\}]*)}");
+    public static final Pattern NAMEHINT_PLACEHOLER_REGEXP = 
Pattern.compile("\\{([^\\{\\}]*)}");
 
-    private final BundleContext bundleContext;
     private final ConfigurationAdmin service;
 
-    private final ConfigManager configManager;
+    private final ServletSupport servletSupport;
 
+    private final List<ConfigurationHandler> configurationHandlers;
 
     /**
+     * Create a new support instance
+     * @param support The servlet support for logging and bundle context
+     * @param service The configuration admin service
      *
-     * @param bundleContext
-     * @param service
-     *
-     * @throws ClassCastException if {@code service} is not a MetaTypeService 
instances
+     * @throws ClassCastException if {@code service} is not a 
ConfigurationAdmin instances
      */
-    ConfigAdminSupport( final ConfigManager configManager, final BundleContext 
bundleContext, final Object service )
-    {
-        this.configManager = configManager;
-        this.bundleContext = bundleContext;
+    ConfigAdminSupport( final ServletSupport support,
+            final Object service,
+            final List<ConfigurationHandler> handlers ) {
+        this.servletSupport = support;
         this.service = ( ConfigurationAdmin ) service;
+        this.configurationHandlers = handlers;
     }
 
-
-    public BundleContext getBundleContext()
-    {
-        return bundleContext;
+    public ConfigJsonSupport getJsonSupport() {
+        return new ConfigJsonSupport(this.servletSupport, 
getMetaTypeSupport(), this.service);
     }
 
-    MetaTypeServiceSupport getMetaTypeSupport()
-    {
-        Object metaTypeService = configManager.getService( 
ConfigManager.META_TYPE_NAME );
-        if ( metaTypeService != null )
-        {
-            return new MetaTypeServiceSupport( this.getBundleContext(), 
metaTypeService );
-        }
-
-        return null;
-    }
-
-
-    final Configuration getConfiguration( String pid )
-    {
-        if ( pid != null )
-        {
-            try
-            {
-                // we use listConfigurations to not create configuration
-                // objects persistently without the user providing actual
-                // configuration
-                String filter = '(' + Constants.SERVICE_PID + '=' + pid + ')';
-                Configuration[] configs = this.service.listConfigurations( 
filter );
-                if ( configs != null && configs.length > 0 )
-                {
-                    return configs[0];
-                }
-            }
-            catch ( InvalidSyntaxException ise )
-            {
-                // should print message
-            }
-            catch ( IOException ioe )
-            {
-                // should print message
-            }
+    MetaTypeServiceSupport getMetaTypeSupport() {
+        Object metaTypeService = servletSupport.getService( 
ConfigManager.META_TYPE_NAME );
+        if ( metaTypeService != null ) {
+            return new MetaTypeServiceSupport( 
servletSupport.getBundleContext(), metaTypeService );
         }
 
-        // fallback to no configuration at all
         return null;
     }
 
-
-    final Configuration getConfiguration( String pid, String factoryPid ) 
throws IOException
-    {
-        if ( factoryPid != null && ( pid == null || pid.equals( 
ConfigManager.PLACEHOLDER_PID ) ) )
-        {
-            return this.service.createFactoryConfiguration( factoryPid, null );
-        }
-
-        return this.service.getConfiguration( pid, null );
-    }
-
-
-    Configuration getPlaceholderConfiguration( final String factoryPid )
-    {
-        return new PlaceholderConfiguration( factoryPid );
-    }
-
-    String getPlaceholderPid() {
-        return ConfigManager.PLACEHOLDER_PID;
-    }
-
     boolean shouldSet(final PropertyDescriptor ad, final String value, final 
boolean isUpdate) 
     {
         if ( ad.hasMetatype() && !isUpdate )
@@ -197,15 +125,23 @@ class ConfigAdminSupport
         return true;
     }
 
+    /**
+     * Apply the update to the configuration
+     * @param request The request
+     * @param pid The pid 
+     * @param propertyList The list of properties
+     * @param isUpdate {@code true} if this is a rest call, false if it is 
done via the webconsole UI
+     * @return {@code true} when the configuration got updated, {@code false} 
when the codnfiugration got deleted
+     * @throws IOException
+     */
     boolean applyConfiguration( final HttpServletRequest request, final String 
pid, final String propertyList, final boolean isUpdate )
             throws IOException
     {
         final String factoryPid = request.getParameter( 
ConfigManager.FACTORY_PID );
-        final Configuration config = getConfiguration( pid, factoryPid );
+        final Configuration config = 
ConfigurationUtil.getOrCreateConfiguration( this.service, 
this.configurationHandlers, pid, factoryPid );
 
         Dictionary<String, Object> props = config.getProperties();
-        if ( props == null )
-        {
+        if ( props == null ) {
             props = new Hashtable<>();
         }
 
@@ -237,7 +173,7 @@ class ConfigAdminSupport
                 if ( value != null
                     && ( attributeType != AttributeDefinition.PASSWORD || 
!MetaTypeSupport.PASSWORD_PLACEHOLDER_VALUE.equals( value ) ) )
                 {
-                    if ( shouldSet(ad, value, isUpdate) ) 
+                    if ( this.configurationHandlers.isEmpty() && shouldSet(ad, 
value, isUpdate) ) 
                     {
                         props.put( propName, value );
                     }
@@ -253,7 +189,7 @@ class ConfigAdminSupport
                 final String value = request.getParameter( paramName );
                 if ( value != null )
                 {
-                    if ( shouldSet(ad, value, isUpdate) ) 
+                    if ( this.configurationHandlers.isEmpty() && shouldSet(ad, 
value, isUpdate) ) 
                     {
                         try
                         {
@@ -328,7 +264,7 @@ class ConfigAdminSupport
                     valueArray[i] = vec.get(i).toString();
                 }
                 
-                final boolean shouldSet = shouldSet(ad, valueArray, isUpdate);
+                final boolean shouldSet = this.configurationHandlers.isEmpty() 
&& shouldSet(ad, valueArray, isUpdate);
 
                 if ( ad.getCardinality() < 0 )
                 {
@@ -375,14 +311,16 @@ class ConfigAdminSupport
             props = updateProps;
         }
 
-        if ( props.isEmpty() )
-        {
-            config.delete();
+        // call update handlers
+        for(final ConfigurationHandler h : this.configurationHandlers) {
+            h.updateConfiguration(factoryPid, pid, props);
+        }
+  
+        if ( props.isEmpty() ) {
+            this.deleteConfiguration(config);
 
             return false;
-        }
-        else
-        {
+        } else {
             final String location = 
request.getParameter(ConfigManager.LOCATION);
             if ( location == null || location.trim().length() == 0 || 
ConfigManager.UNBOUND_LOCATION.equals(location) )
             {
@@ -413,597 +351,29 @@ class ConfigAdminSupport
         }
     }
 
-
-    void printConfigurationJson( PrintWriter pw, String pid, Configuration 
config, String pidFilter,
-            String locale )
-    {
-
-        JSONWriter result = new JSONWriter( pw );
-
-        if ( pid != null )
-        {
-            try
-            {
-                result.object();
-                this.configForm( result, pid, config, pidFilter, locale );
-                result.endObject();
-            }
-            catch ( Exception e )
-            {
-                configManager.log( "Error reading configuration PID " + pid, e 
);
-            }
-        }
-
-    }
-
-
-    void configForm( JSONWriter json, String pid, Configuration config, String 
pidFilter, String locale )
-            throws IOException
-    {
-
-        json.key( ConfigManager.PID );
-        json.value( pid );
-
-        if ( pidFilter != null )
-        {
-            json.key( ConfigManager.PID_FILTER );
-            json.value( pidFilter );
-        }
-
-        Dictionary<String, Object> props = null;
-        if ( config != null )
-        {
-            props = config.getProperties();
-        }
-        if ( props == null )
-        {
-            props = new Hashtable<>();
-        }
-
-        boolean doSimpleMerge = true;
-        final MetaTypeServiceSupport mtss = getMetaTypeSupport();
-        if ( mtss != null )
-        {
-            ObjectClassDefinition ocd = null;
-            if ( config != null )
-            {
-                ocd = mtss.getObjectClassDefinition( config, locale );
-            }
-            if ( ocd == null )
-            {
-                ocd = mtss.getObjectClassDefinition( pid, locale );
-            }
-            if ( ocd != null )
-            {
-                mtss.mergeWithMetaType( props, ocd, json, 
CONFIG_PROPERTIES_HIDE );
-                doSimpleMerge = false;
-            }
-        }
-
-        if (doSimpleMerge)
-        {
-            json.key( "title" ).value( pid ); //$NON-NLS-1$
-            json.key( "description" ).value( //$NON-NLS-1$
-                    "This form is automatically generated from existing 
properties because no property "
-                    + "descriptors are available for this configuration. This 
may be cause by the absence "
-                    + "of the OSGi Metatype Service or the absence of a 
MetaType descriptor for this configuration." );
-
-            json.key( "properties" ).object(); //$NON-NLS-1$
-            for ( Enumeration<String> pe = props.keys(); pe.hasMoreElements(); 
)
-            {
-                final String id = pe.nextElement();
-
-                // ignore well known special properties
-                if ( !id.equals( Constants.SERVICE_PID ) && !id.equals( 
Constants.SERVICE_DESCRIPTION )
-                        && !id.equals( Constants.SERVICE_ID ) && !id.equals( 
Constants.SERVICE_VENDOR )
-                        && !id.equals( 
ConfigurationAdmin.SERVICE_BUNDLELOCATION )
-                        && !id.equals( ConfigurationAdmin.SERVICE_FACTORYPID ) 
)
-                {
-                    final Object value = props.get( id );
-                    final PropertyDescriptor ad = 
MetaTypeServiceSupport.createAttributeDefinition( id, value );
-                    json.key( id );
-                    MetaTypeServiceSupport.attributeToJson( json, ad, value );
-                }
-            }
-            json.endObject();
-        }
-
-        if ( config != null )
-        {
-            this.addConfigurationInfo( config, json, locale );
-        }
-    }
-
-
-    void addConfigurationInfo( Configuration config, JSONWriter json, String 
locale )
-            throws IOException
-    {
-
-        if ( config.getFactoryPid() != null )
-        {
-            json.key( ConfigManager.FACTORY_PID );
-            json.value( config.getFactoryPid() );
-        }
-
-        String bundleLocation = config.getBundleLocation();
-        if ( ConfigManager.UNBOUND_LOCATION.equals(bundleLocation) )
-        {
-            bundleLocation = null;
-        }
-        String location;
-        if ( bundleLocation == null )
-        {
-            location = ""; //$NON-NLS-1$
-        }
-        else
-        {
-            // if the configuration is bound to a bundle location which
-            // is not related to an installed bundle, we just print the
-            // raw bundle location binding
-            Bundle bundle = MetaTypeServiceSupport.getBundle( 
this.getBundleContext(), bundleLocation );
-            if ( bundle == null )
-            {
-                location = bundleLocation;
-            }
-            else
-            {
-                Dictionary<String, String> headers = bundle.getHeaders( locale 
);
-                String name = headers.get( Constants.BUNDLE_NAME );
-                if ( name == null )
-                {
-                    location = bundle.getSymbolicName();
-                }
-                else
-                {
-                    location = name + " (" + bundle.getSymbolicName() + ')'; 
//$NON-NLS-1$
-                }
-
-                Version v = Version.parseVersion( headers.get( 
Constants.BUNDLE_VERSION ) );
-                location += ", Version " + v.toString();
-            }
-        }
-        json.key( "bundleLocation" ); //$NON-NLS-1$
-        json.value( location );
-        // raw bundle location and service locations
-        final String pid = config.getPid();
-        String serviceLocation = ""; //$NON-NLS-1$
-        try
-        {
-            final ServiceReference<?>[] refs = 
getBundleContext().getServiceReferences(
-                    (String)null,
-                    "(&(" + Constants.OBJECTCLASS + '=' + 
ManagedService.class.getName() //$NON-NLS-1$
-                    + ")(" + Constants.SERVICE_PID + '=' + pid + "))"); 
//$NON-NLS-1$ //$NON-NLS-2$
-            if ( refs != null && refs.length > 0 )
-            {
-                serviceLocation = refs[0].getBundle().getLocation();
-            }
-        }
-        catch (Throwable t)
-        {
-            configManager.log( "Error getting service associated with 
configuration " + pid, t );
-        }
-        json.key( "bundle_location" ); //$NON-NLS-1$
-        json.value ( bundleLocation );
-        json.key( "service_location" ); //$NON-NLS-1$
-        json.value ( serviceLocation );
-    }
-
-
-    private final Bundle getBoundBundle(Configuration config)
-    {
-        if (null == config)
-            return null;
-        final String location = config.getBundleLocation();
-        if (null == location)
-            return null;
-
-        final Bundle bundles[] = getBundleContext().getBundles();
-        for (int i = 0; bundles != null && i < bundles.length; i++)
-        {
-            if (bundles[i].getLocation().equals(location))
-                return bundles[i];
-
-        }
-        return null;
-    }
-
-
-    final boolean listConfigurations( JSONWriter jw, String pidFilter, String 
locale, Locale loc )
-    {
-        boolean hasConfigurations = false;
-        try
-        {
-            // start with ManagedService instances
-            Map<String, String> optionsPlain = 
getServices(ManagedService.class.getName(), pidFilter,
-                    locale, true);
-
-            // next are the MetaType informations without ManagedService
-            final MetaTypeServiceSupport mtss = getMetaTypeSupport();
-            if ( mtss != null )
-            {
-                addMetaTypeNames( optionsPlain, mtss.getPidObjectClasses( 
locale ), pidFilter, Constants.SERVICE_PID );
-            }
-
-            // add in existing configuration (not duplicating ManagedServices)
-            Configuration[] cfgs = this.service.listConfigurations(pidFilter);
-            for (int i = 0; cfgs != null && i < cfgs.length; i++)
-            {
-
-                // ignore configuration object if an entry already exists in 
the map
-                // or if it is invalid
-                final String pid = cfgs[i].getPid();
-                if (optionsPlain.containsKey(pid) || 
!ConfigManager.isAllowedPid(pid) )
-                {
-                    continue;
-                }
-
-                // insert and entry for the PID
-                if ( mtss != null )
-                {
-                    try
-                    {
-                        ObjectClassDefinition ocd = 
mtss.getObjectClassDefinition( cfgs[i], locale );
-                        if ( ocd != null )
-                        {
-                            optionsPlain.put( pid, ocd.getName() );
-                            continue;
-                        }
-                    }
-                    catch ( IllegalArgumentException t )
-                    {
-                        // MetaTypeProvider.getObjectClassDefinition might 
throw illegal
-                        // argument exception. So we must catch it here, 
otherwise the
-                        // other configurations will not be shown
-                        // See https://issues.apache.org/jira/browse/FELIX-2390
-                    }
-                }
-
-                // no object class definition, use plain PID
-                optionsPlain.put( pid, pid );
-            }
-
-            jw.key("pids");//$NON-NLS-1$
-            jw.array();
-            for ( Iterator<String> ii = optionsPlain.keySet().iterator(); 
ii.hasNext(); )
-            {
-                hasConfigurations = true;
-                String id = ii.next();
-                Object name = optionsPlain.get( id );
-
-                final Configuration config = this.getConfiguration( id );
-                jw.object();
-                jw.key("id").value( id ); //$NON-NLS-1$
-                jw.key( "name").value( name ); //$NON-NLS-1$
-                if ( null != config )
-                {
-                    // FELIX-3848
-                    jw.key("has_config").value( true ); //$NON-NLS-1$
-
-                    final String fpid = config.getFactoryPid();
-                    if ( null != fpid )
-                    {
-                        jw.key("fpid").value( fpid ); //$NON-NLS-1$
-                        final String val = 
getConfigurationFactoryNameHint(config, mtss);
-                        if ( val != null )
-                        {
-                            jw.key( "nameHint").value(val ); //$NON-NLS-1$
-                        }
-                    }
-
-                    final Bundle bundle = getBoundBundle( config );
-                    if ( null != bundle )
-                    {
-                        jw.key( "bundle").value( bundle.getBundleId() ); 
//$NON-NLS-1$
-                        jw.key( "bundle_name").value( Util.getName( bundle, 
loc ) ); //$NON-NLS-1$
-                    }
-                }
-                jw.endObject();
-
-            }
-            jw.endArray();
-        }
-        catch (Exception e)
-        {
-            configManager.log("listConfigurations: Unexpected problem 
encountered", e);
-        }
-        return hasConfigurations;
-    }
-
-    /**
-     * Builds a "name hint" for factory configuration based on other property
-     * values of the config and a "name hint template" defined as hidden
-     * property in the service.
-     * @param props Service properties.
-     * @return Name hint or null if none is defined.
-     */
-    private static final String getConfigurationFactoryNameHint(Configuration 
config, MetaTypeServiceSupport mtss)
-    {
-        Dictionary<String, Object> props = config.getProperties();
-        Map<String, MetatypePropertyDescriptor> adMap = (mtss != null) ? 
mtss.getAttributeDefinitionMap(config, null) : null;
-        if (null == adMap)
-        {
-            return null;
-        }
-
-        // check for configured name hint template
-        String nameHint = 
getConfigurationPropertyValueOrDefault(PROPERTY_FACTORYCONFIG_NAMEHINT, props, 
adMap);
-        if (nameHint == null)
-        {
-            return null;
-        }
-
-        // search for all variable patterns in name hint and replace them with 
configured/default values
-        Matcher matcher = NAMEHINT_PLACEHOLER_REGEXP.matcher(nameHint);
-        StringBuffer sb = new StringBuffer();
-        while (matcher.find())
-        {
-            String propertyName = matcher.group(1);
-            String value = 
getConfigurationPropertyValueOrDefault(propertyName, props, adMap);
-            if (value == null) {
-                value = "";
-            }
-            matcher.appendReplacement(sb, matcherQuoteReplacement(value));
-        }
-        matcher.appendTail(sb);
-
-        // make sure name hint does not only contain whitespaces
-        nameHint = sb.toString().trim();
-        if (nameHint.length() == 0) {
-            return null;
-        }
-        else {
-            return nameHint;
-        }
-    }
-
-    /**
-     * Gets configured service property value, or default value if no value is 
configured.
-     * @param propertyName Property name
-     * @param props Service configuration properties map
-     * @param adMap Attribute definitions map
-     * @return Value or null if none found
-     */
-    private static String getConfigurationPropertyValueOrDefault(String 
propertyName, Dictionary<String, Object> props, Map<String, 
MetatypePropertyDescriptor> adMap) {
-        // get configured property value
-        Object value = props.get(propertyName);
-
-        if (value != null)
-        {
-            // convert array to string
-            if (value.getClass().isArray())
-            {
-                StringBuffer valueString = new StringBuffer();
-                for (int i = 0; i < Array.getLength(value); i++)
-                {
-                    if (i > 0)
-                    {
-                        valueString.append(", ");
-                    }
-                    valueString.append(Array.get(value, i));
-                }
-                return valueString.toString();
-            }
-            else
-            {
-                return value.toString();
-            }
-        }
-        else
-        {
-            // if not set try to get default value
-            PropertyDescriptor ad = adMap.get(propertyName);
-            if (ad != null && ad.getDefaultValue() != null && 
ad.getDefaultValue().length == 1)
-            {
-                return ad.getDefaultValue()[0];
-            }
-        }
-
-        return null;
-    }
-
-    /**
-     * Replacement for Matcher.quoteReplacement which is only available in JDK 
1.5 and up.
-     * @param str Unquoted string
-     * @return Quoted string
-     */
-    private static String matcherQuoteReplacement(String str)
-    {
-        StringBuffer sb = new StringBuffer();
-        for (int i = 0; i < str.length(); i++) {
-            char c = str.charAt(i);
-            if (c == '$' || c == '\\') {
-                sb.append('\\');
-            }
-            sb.append(c);
-        }
-        return sb.toString();
-    }
-
-    final void listFactoryConfigurations(JSONWriter jw, String pidFilter,
-            String locale)
-    {
-        try
-        {
-            final Map<String, String> optionsFactory = 
getServices(ManagedServiceFactory.class.getName(),
-                    pidFilter, locale, true);
-            final MetaTypeServiceSupport mtss = getMetaTypeSupport();
-            if ( mtss != null )
-            {
-                addMetaTypeNames( optionsFactory, 
mtss.getFactoryPidObjectClasses( locale ), pidFilter,
-                        ConfigurationAdmin.SERVICE_FACTORYPID );
-            }
-            jw.key("fpids");
-            jw.array();
-            for ( Iterator<String> ii = optionsFactory.keySet().iterator(); 
ii.hasNext(); )
-            {
-                String id = ii.next();
-                Object name = optionsFactory.get( id );
-                jw.object();
-                jw.key("id").value(id ); //$NON-NLS-1$
-                jw.key("name").value( name ); //$NON-NLS-1$
-                jw.endObject();
-            }
-            jw.endArray();
-        }
-        catch (Exception e)
-        {
-            configManager.log("listFactoryConfigurations: Unexpected problem 
encountered", e);
-        }
-    }
-
-    SortedMap<String, String> getServices( String serviceClass, String 
serviceFilter, String locale,
-            boolean ocdRequired ) throws InvalidSyntaxException
-    {
-        // sorted map of options
-        SortedMap<String, String> optionsFactory = new TreeMap<>( 
String.CASE_INSENSITIVE_ORDER );
-
-        // find all ManagedServiceFactories to get the factoryPIDs
-        ServiceReference<?>[] refs = 
this.getBundleContext().getServiceReferences( serviceClass, serviceFilter );
-        for ( int i = 0; refs != null && i < refs.length; i++ )
-        {
-            Object pidObject = refs[i].getProperty( Constants.SERVICE_PID );
-            // only include valid PIDs
-            if ( pidObject instanceof String && 
ConfigManager.isAllowedPid((String)pidObject) )
-            {
-                String pid = ( String ) pidObject;
-                String name = pid;
-                boolean haveOcd = !ocdRequired;
-                final MetaTypeServiceSupport mtss = getMetaTypeSupport();
-                if ( mtss != null )
-                {
-                    final ObjectClassDefinition ocd = 
mtss.getObjectClassDefinition( refs[i].getBundle(), pid, locale );
-                    if ( ocd != null )
-                    {
-                        name = ocd.getName();
-                        haveOcd = true;
-                    }
-                }
-
-                if ( haveOcd )
-                {
-                    optionsFactory.put( pid, name );
-                }
-            }
-        }
-
-        return optionsFactory;
+    public void deleteConfiguration(final String pid) throws IOException {
+        // only delete if the PID is not our place holder
+        if ( !ConfigurationUtil.getPlaceholderPid().equals( pid ) ) {
+            final Configuration config = 
ConfigurationUtil.findConfiguration(this.service, pid);
+            this.deleteConfiguration(config);
+        }            
     }
 
-    private void addMetaTypeNames( final Map<String, String> pidMap, final 
Map<String, ObjectClassDefinition> ocdCollection, final String filterSpec, 
final String type )
-    {
-        Filter filter = null;
-        if ( filterSpec != null )
-        {
-            try
-            {
-                filter = getBundleContext().createFilter( filterSpec );
-            }
-            catch ( InvalidSyntaxException not_expected )
-            {
-                /* filter is correct */
-            }
-        }
-
-        for ( Iterator<Map.Entry<String, ObjectClassDefinition>> ei = 
ocdCollection.entrySet().iterator(); ei.hasNext(); )
-        {
-            Entry<String, ObjectClassDefinition> ociEntry = ei.next();
-            final String pid = ociEntry.getKey();
-            final ObjectClassDefinition ocd = ociEntry.getValue();
-            if ( filter == null )
-            {
-                pidMap.put( pid, ocd.getName() );
+    void deleteConfiguration(final Configuration config) throws IOException {
+        // only delete if the PID is not our place holder
+        if ( config != null && !ConfigurationUtil.getPlaceholderPid().equals( 
config.getPid() ) ) {
+            for(final ConfigurationHandler h : this.configurationHandlers) {
+                h.deleteConfiguration(config.getFactoryPid(), config.getPid());
             }
-            else
-            {
-                final Dictionary<String, Object> props = new Hashtable<>();
-                props.put( type, pid );
-                if ( filter.match( props ) )
-                {
-                    pidMap.put( pid, ocd.getName() );
-                }
-            }
-        }
+            config.delete();
+        }            
     }
 
-    private static class PlaceholderConfiguration implements Configuration
-    {
-
-        private final String factoryPid;
-        private String bundleLocation;
-
-
-        PlaceholderConfiguration( String factoryPid )
-        {
-            this.factoryPid = factoryPid;
-        }
-
-
-        @Override
-        public String getPid()
-        {
-            return ConfigManager.PLACEHOLDER_PID;
-        }
-
-
-        @Override
-        public String getFactoryPid()
-        {
-            return factoryPid;
-        }
-
-
-        @Override
-        public void setBundleLocation( String bundleLocation )
-        {
-            this.bundleLocation = bundleLocation;
-        }
-
-
-        @Override
-        public String getBundleLocation()
-        {
-            return bundleLocation;
-        }
-
-
-        @Override
-        public Dictionary<String, Object> getProperties()
-        {
-            // dummy configuration has no properties
-            return null;
-        }
-
-
-        @Override
-        public void update()
-        {
-            // dummy configuration cannot be updated
-        }
-
-
-        @Override
-        public void update( Dictionary<String, ?> properties )
-        {
-            // dummy configuration cannot be updated
-        }
-
-
-        @Override
-        public void delete()
-        {
-            // dummy configuration cannot be deleted
-        }
-
-
-        @Override
-        public long getChangeCount() {
-            // dummy configuration always returns 0
-            return 0;
-        }
+    public Configuration findConfiguration(final String pid) {
+        return ConfigurationUtil.findConfiguration(this.service, pid);
     }
 
-    public Configuration[] listConfigurations(String filter) throws 
IOException, InvalidSyntaxException
-    {
+    public Configuration[] listConfigurations(final String filter) throws 
IOException, InvalidSyntaxException {
         return this.service.listConfigurations(filter);
     }
 }
diff --git 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/configuration/ConfigJsonSupport.java
 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/configuration/ConfigJsonSupport.java
new file mode 100644
index 0000000..a3a50d5
--- /dev/null
+++ 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/configuration/ConfigJsonSupport.java
@@ -0,0 +1,494 @@
+/*
+ * 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.felix.webconsole.internal.configuration;
+
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.lang.reflect.Array;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.regex.Matcher;
+
+import org.apache.felix.utils.json.JSONWriter;
+import org.apache.felix.webconsole.internal.Util;
+import org.apache.felix.webconsole.internal.misc.ServletSupport;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Filter;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.Version;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.service.cm.ManagedService;
+import org.osgi.service.cm.ManagedServiceFactory;
+import org.osgi.service.metatype.ObjectClassDefinition;
+
+
+class ConfigJsonSupport {
+
+    private final ServletSupport servletSupport;
+
+    private final MetaTypeServiceSupport mtss;
+    
+    private final ConfigurationAdmin configurationAdmin;
+
+    public ConfigJsonSupport(final ServletSupport support, 
+            final MetaTypeServiceSupport mtss, 
+            final ConfigurationAdmin cfgAdmin) {
+        this.servletSupport = support;
+        this.mtss = mtss;
+        this.configurationAdmin = cfgAdmin;
+    }
+
+    public void printConfigurationJson( final PrintWriter pw, final String 
pid, final Configuration config, final String pidFilter,
+            final String locale ) {
+
+        final JSONWriter result = new JSONWriter( pw );
+
+        if ( pid != null ) {
+            try{
+                result.object();
+                this.configForm( result, pid, config, pidFilter, locale );
+                result.endObject();
+            } catch ( final Exception e ) {
+                this.servletSupport.log( "Error reading configuration PID " + 
pid, e );
+            }
+        }
+
+    }
+
+    void configForm( final JSONWriter json, final String pid, final 
Configuration config, final String pidFilter, final String locale )
+    throws IOException {
+        json.key( ConfigManager.PID );
+        json.value( pid );
+
+        if ( pidFilter != null ) {
+            json.key( ConfigManager.PID_FILTER );
+            json.value( pidFilter );
+        }
+
+        Dictionary<String, Object> props = null;
+        if ( config != null ) {
+            props = config.getProperties();
+        }
+        if ( props == null ) {
+            props = new Hashtable<>();
+        }
+
+        boolean doSimpleMerge = true;
+        if ( this.mtss != null ) {
+            ObjectClassDefinition ocd = null;
+            if ( config != null ) {
+                ocd = mtss.getObjectClassDefinition( config, locale );
+            }
+            if ( ocd == null ) {
+                ocd = mtss.getObjectClassDefinition( pid, locale );
+            }
+            if ( ocd != null ) {
+                mtss.mergeWithMetaType( props, ocd, json, 
ConfigAdminSupport.CONFIG_PROPERTIES_HIDE );
+                doSimpleMerge = false;
+            }
+        }
+
+        if (doSimpleMerge) {
+            json.key( "title" ).value( pid ); //$NON-NLS-1$
+            json.key( "description" ).value( //$NON-NLS-1$
+                    "This form is automatically generated from existing 
properties because no property "
+                    + "descriptors are available for this configuration. This 
may be cause by the absence "
+                    + "of the OSGi Metatype Service or the absence of a 
MetaType descriptor for this configuration." );
+
+            json.key( "properties" ).object(); //$NON-NLS-1$
+            for ( Enumeration<String> pe = props.keys(); pe.hasMoreElements(); 
) {
+                final String id = pe.nextElement();
+
+                // ignore well known special properties
+                if ( !id.equals( Constants.SERVICE_PID ) && !id.equals( 
Constants.SERVICE_DESCRIPTION )
+                        && !id.equals( Constants.SERVICE_ID ) && !id.equals( 
Constants.SERVICE_VENDOR )
+                        && !id.equals( 
ConfigurationAdmin.SERVICE_BUNDLELOCATION )
+                        && !id.equals( ConfigurationAdmin.SERVICE_FACTORYPID ) 
) {
+                    final Object value = props.get( id );
+                    final PropertyDescriptor ad = 
MetaTypeServiceSupport.createAttributeDefinition( id, value );
+                    json.key( id );
+                    MetaTypeServiceSupport.attributeToJson( json, ad, value );
+                }
+            }
+            json.endObject();
+        }
+
+        if ( config != null ) {
+            this.addConfigurationInfo( config, json, locale );
+        }
+    }
+
+
+    void addConfigurationInfo( final Configuration config, final JSONWriter 
json, final String locale )
+            throws IOException {
+
+        if ( config.getFactoryPid() != null ) {
+            json.key( ConfigManager.FACTORY_PID );
+            json.value( config.getFactoryPid() );
+        }
+
+        String bundleLocation = config.getBundleLocation();
+        if ( ConfigManager.UNBOUND_LOCATION.equals(bundleLocation) ) {
+            bundleLocation = null;
+        }
+        String location;
+        if ( bundleLocation == null ) {
+            location = ""; //$NON-NLS-1$
+        } else {
+            // if the configuration is bound to a bundle location which
+            // is not related to an installed bundle, we just print the
+            // raw bundle location binding
+            Bundle bundle = MetaTypeServiceSupport.getBundle( 
this.servletSupport.getBundleContext(), bundleLocation );
+            if ( bundle == null ) {
+                location = bundleLocation;
+            } else {
+                Dictionary<String, String> headers = bundle.getHeaders( locale 
);
+                String name = headers.get( Constants.BUNDLE_NAME );
+                if ( name == null ) {
+                    location = bundle.getSymbolicName();
+                } else {
+                    location = name + " (" + bundle.getSymbolicName() + ')'; 
//$NON-NLS-1$
+                }
+
+                Version v = Version.parseVersion( headers.get( 
Constants.BUNDLE_VERSION ) );
+                location += ", Version " + v.toString();
+            }
+        }
+        json.key( "bundleLocation" ); //$NON-NLS-1$
+        json.value( location );
+        // raw bundle location and service locations
+        final String pid = config.getPid();
+        String serviceLocation = ""; //$NON-NLS-1$
+        try {
+            final ServiceReference<?>[] refs = 
this.servletSupport.getBundleContext().getServiceReferences(
+                    (String)null,
+                    "(&(" + Constants.OBJECTCLASS + '=' + 
ManagedService.class.getName() //$NON-NLS-1$
+                    + ")(" + Constants.SERVICE_PID + '=' + pid + "))"); 
//$NON-NLS-1$ //$NON-NLS-2$
+            if ( refs != null && refs.length > 0 ) {
+                serviceLocation = refs[0].getBundle().getLocation();
+            }
+        } catch (final Throwable t) {
+            this.servletSupport.log( "Error getting service associated with 
configuration " + pid, t );
+        }
+        json.key( "bundle_location" ); //$NON-NLS-1$
+        json.value ( bundleLocation );
+        json.key( "service_location" ); //$NON-NLS-1$
+        json.value ( serviceLocation );
+    }
+
+    private final Bundle getBoundBundle(final Configuration config) {
+        if (null == config) {
+            return null;
+        }
+        final String location = config.getBundleLocation();
+        if (null == location) {
+            return null;
+        }
+
+        final Bundle bundles[] = 
this.servletSupport.getBundleContext().getBundles();
+        for (int i = 0; bundles != null && i < bundles.length; i++) {
+            if (bundles[i].getLocation().equals(location)) {
+                return bundles[i];
+            }
+        }
+        return null;
+    }
+
+    final boolean listConfigurations( final JSONWriter jw, final String 
pidFilter, final String locale, final Locale loc ) {
+        boolean hasConfigurations = false;
+        try {
+            // start with ManagedService instances
+            Map<String, String> optionsPlain = 
getServices(ManagedService.class.getName(), pidFilter,
+                    locale, true);
+
+            // next are the MetaType informations without ManagedService
+            if ( mtss != null ) {
+                addMetaTypeNames( optionsPlain, mtss.getPidObjectClasses( 
locale ), pidFilter, Constants.SERVICE_PID );
+            }
+
+            // add in existing configuration (not duplicating ManagedServices)
+            Configuration[] cfgs = 
this.configurationAdmin.listConfigurations(pidFilter);
+            for (int i = 0; cfgs != null && i < cfgs.length; i++)
+            {
+
+                // ignore configuration object if an entry already exists in 
the map
+                // or if it is invalid
+                final String pid = cfgs[i].getPid();
+                if (optionsPlain.containsKey(pid) || 
!ConfigManager.isAllowedPid(pid) )
+                {
+                    continue;
+                }
+
+                // insert and entry for the PID
+                if ( mtss != null )
+                {
+                    try
+                    {
+                        ObjectClassDefinition ocd = 
mtss.getObjectClassDefinition( cfgs[i], locale );
+                        if ( ocd != null )
+                        {
+                            optionsPlain.put( pid, ocd.getName() );
+                            continue;
+                        }
+                    }
+                    catch ( IllegalArgumentException t )
+                    {
+                        // MetaTypeProvider.getObjectClassDefinition might 
throw illegal
+                        // argument exception. So we must catch it here, 
otherwise the
+                        // other configurations will not be shown
+                        // See https://issues.apache.org/jira/browse/FELIX-2390
+                    }
+                }
+
+                // no object class definition, use plain PID
+                optionsPlain.put( pid, pid );
+            }
+
+            jw.key("pids");//$NON-NLS-1$
+            jw.array();
+            for ( Iterator<String> ii = optionsPlain.keySet().iterator(); 
ii.hasNext(); )
+            {
+                hasConfigurations = true;
+                String id = ii.next();
+                Object name = optionsPlain.get( id );
+
+                final Configuration config = 
ConfigurationUtil.findConfiguration( this.configurationAdmin, id );
+                jw.object();
+                jw.key("id").value( id ); //$NON-NLS-1$
+                jw.key( "name").value( name ); //$NON-NLS-1$
+                if ( null != config )
+                {
+                    // FELIX-3848
+                    jw.key("has_config").value( true ); //$NON-NLS-1$
+
+                    final String fpid = config.getFactoryPid();
+                    if ( null != fpid )
+                    {
+                        jw.key("fpid").value( fpid ); //$NON-NLS-1$
+                        final String val = 
getConfigurationFactoryNameHint(config, mtss);
+                        if ( val != null )
+                        {
+                            jw.key( "nameHint").value(val ); //$NON-NLS-1$
+                        }
+                    }
+
+                    final Bundle bundle = getBoundBundle( config );
+                    if ( null != bundle ) {
+                        jw.key( "bundle").value( bundle.getBundleId() ); 
//$NON-NLS-1$
+                        jw.key( "bundle_name").value( Util.getName( bundle, 
loc ) ); //$NON-NLS-1$
+                    }
+                }
+                jw.endObject();
+
+            }
+            jw.endArray();
+        } catch (final Exception e) {
+            this.servletSupport.log("listConfigurations: Unexpected problem 
encountered", e);
+        }
+        return hasConfigurations;
+    }
+
+    /**
+     * Builds a "name hint" for factory configuration based on other property
+     * values of the config and a "name hint template" defined as hidden
+     * property in the service.
+     * @param props Service properties.
+     * @return Name hint or null if none is defined.
+     */
+    private static final String getConfigurationFactoryNameHint(Configuration 
config, MetaTypeServiceSupport mtss) {
+        Dictionary<String, Object> props = config.getProperties();
+        Map<String, MetatypePropertyDescriptor> adMap = (mtss != null) ? 
mtss.getAttributeDefinitionMap(config, null) : null;
+        if (null == adMap) {
+            return null;
+        }
+
+        // check for configured name hint template
+        String nameHint = 
getConfigurationPropertyValueOrDefault(ConfigAdminSupport.PROPERTY_FACTORYCONFIG_NAMEHINT,
 props, adMap);
+        if (nameHint == null) {
+            return null;
+        }
+
+        // search for all variable patterns in name hint and replace them with 
configured/default values
+        Matcher matcher = 
ConfigAdminSupport.NAMEHINT_PLACEHOLER_REGEXP.matcher(nameHint);
+        StringBuffer sb = new StringBuffer();
+        while (matcher.find()) {
+            String propertyName = matcher.group(1);
+            String value = 
getConfigurationPropertyValueOrDefault(propertyName, props, adMap);
+            if (value == null) {
+                value = "";
+            }
+            matcher.appendReplacement(sb, matcherQuoteReplacement(value));
+        }
+        matcher.appendTail(sb);
+
+        // make sure name hint does not only contain whitespaces
+        nameHint = sb.toString().trim();
+        if (nameHint.length() == 0) {
+            return null;
+        } else {
+            return nameHint;
+        }
+    }
+
+    /**
+     * Gets configured service property value, or default value if no value is 
configured.
+     * @param propertyName Property name
+     * @param props Service configuration properties map
+     * @param adMap Attribute definitions map
+     * @return Value or null if none found
+     */
+    private static String getConfigurationPropertyValueOrDefault(String 
propertyName, Dictionary<String, Object> props, Map<String, 
MetatypePropertyDescriptor> adMap) {
+        // get configured property value
+        Object value = props.get(propertyName);
+
+        if (value != null) {
+            // convert array to string
+            if (value.getClass().isArray()) {
+                StringBuffer valueString = new StringBuffer();
+                for (int i = 0; i < Array.getLength(value); i++) {
+                    if (i > 0) {
+                        valueString.append(", ");
+                    }
+                    valueString.append(Array.get(value, i));
+                }
+                return valueString.toString();
+            } else {
+                return value.toString();
+            }
+        } else {
+            // if not set try to get default value
+            PropertyDescriptor ad = adMap.get(propertyName);
+            if (ad != null && ad.getDefaultValue() != null && 
ad.getDefaultValue().length == 1) {
+                return ad.getDefaultValue()[0];
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Replacement for Matcher.quoteReplacement which is only available in JDK 
1.5 and up.
+     * @param str Unquoted string
+     * @return Quoted string
+     */
+    private static String matcherQuoteReplacement(String str) {
+        StringBuffer sb = new StringBuffer();
+        for (int i = 0; i < str.length(); i++) {
+            char c = str.charAt(i);
+            if (c == '$' || c == '\\') {
+                sb.append('\\');
+            }
+            sb.append(c);
+        }
+        return sb.toString();
+    }
+
+    final void listFactoryConfigurations(JSONWriter jw, String pidFilter,
+            String locale) {
+        try {
+            final Map<String, String> optionsFactory = 
getServices(ManagedServiceFactory.class.getName(),
+                    pidFilter, locale, true);
+            if ( mtss != null ) {
+                addMetaTypeNames( optionsFactory, 
mtss.getFactoryPidObjectClasses( locale ), pidFilter,
+                        ConfigurationAdmin.SERVICE_FACTORYPID );
+            }
+            jw.key("fpids");
+            jw.array();
+            for ( Iterator<String> ii = optionsFactory.keySet().iterator(); 
ii.hasNext(); ) {
+                String id = ii.next();
+                Object name = optionsFactory.get( id );
+                jw.object();
+                jw.key("id").value(id ); //$NON-NLS-1$
+                jw.key("name").value( name ); //$NON-NLS-1$
+                jw.endObject();
+            }
+            jw.endArray();
+        } catch (final Exception e) {
+            this.servletSupport.log("listFactoryConfigurations: Unexpected 
problem encountered", e);
+        }
+    }
+
+    SortedMap<String, String> getServices( String serviceClass, String 
serviceFilter, String locale,
+            boolean ocdRequired ) throws InvalidSyntaxException {
+        // sorted map of options
+        SortedMap<String, String> optionsFactory = new TreeMap<>( 
String.CASE_INSENSITIVE_ORDER );
+
+        // find all ManagedServiceFactories to get the factoryPIDs
+        ServiceReference<?>[] refs = 
this.servletSupport.getBundleContext().getServiceReferences( serviceClass, 
serviceFilter );
+        for ( int i = 0; refs != null && i < refs.length; i++ ) {
+            Object pidObject = refs[i].getProperty( Constants.SERVICE_PID );
+            // only include valid PIDs
+            if ( pidObject instanceof String && 
ConfigManager.isAllowedPid((String)pidObject) ) {
+                String pid = ( String ) pidObject;
+                String name = pid;
+                boolean haveOcd = !ocdRequired;
+                if ( mtss != null ) {
+                    final ObjectClassDefinition ocd = 
mtss.getObjectClassDefinition( refs[i].getBundle(), pid, locale );
+                    if ( ocd != null ) {
+                        name = ocd.getName();
+                        haveOcd = true;
+                    }
+                }
+
+                if ( haveOcd ) {
+                    optionsFactory.put( pid, name );
+                }
+            }
+        }
+
+        return optionsFactory;
+    }
+
+    private void addMetaTypeNames( final Map<String, String> pidMap, final 
Map<String, ObjectClassDefinition> ocdCollection, final String filterSpec, 
final String type ) {
+        Filter filter = null;
+        if ( filterSpec != null ) {
+            try {
+                filter = this.servletSupport.getBundleContext().createFilter( 
filterSpec );
+            } catch ( InvalidSyntaxException not_expected ){
+                // filter is correct 
+            }
+        }
+
+        for ( Iterator<Map.Entry<String, ObjectClassDefinition>> ei = 
ocdCollection.entrySet().iterator(); ei.hasNext(); ) {
+            Entry<String, ObjectClassDefinition> ociEntry = ei.next();
+            final String pid = ociEntry.getKey();
+            final ObjectClassDefinition ocd = ociEntry.getValue();
+            if ( filter == null ) {
+                pidMap.put( pid, ocd.getName() );
+            } else {
+                final Dictionary<String, Object> props = new Hashtable<>();
+                props.put( type, pid );
+                if ( filter.match( props ) ) {
+                    pidMap.put( pid, ocd.getName() );
+                }
+            }
+        }
+    }
+}
diff --git 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/configuration/ConfigManager.java
 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/configuration/ConfigManager.java
index 37145c0..48a0f6e 100644
--- 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/configuration/ConfigManager.java
+++ 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/configuration/ConfigManager.java
@@ -16,10 +16,10 @@
  */
 package org.apache.felix.webconsole.internal.configuration;
 
-
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.StringWriter;
+import java.util.Collections;
 import java.util.Locale;
 import java.util.Map;
 
@@ -33,6 +33,8 @@ import org.apache.felix.webconsole.WebConsoleConstants;
 import org.apache.felix.webconsole.WebConsoleUtil;
 import org.apache.felix.webconsole.internal.OsgiManagerPlugin;
 import org.apache.felix.webconsole.internal.Util;
+import org.apache.felix.webconsole.internal.misc.ServletSupport;
+import org.osgi.framework.BundleContext;
 import org.osgi.framework.Constants;
 import org.osgi.framework.InvalidSyntaxException;
 import org.osgi.service.cm.Configuration;
@@ -42,7 +44,7 @@ import org.osgi.service.cm.Configuration;
  * The <code>ConfigManager</code> class is the Web Console plugin to
  * manage configurations.
  */
-public class ConfigManager extends SimpleWebConsolePlugin implements 
OsgiManagerPlugin
+public class ConfigManager extends SimpleWebConsolePlugin implements 
OsgiManagerPlugin, ServletSupport
 {
 
     private static final long serialVersionUID = 5021174538498622428L;
@@ -54,7 +56,6 @@ public class ConfigManager extends SimpleWebConsolePlugin 
implements OsgiManager
     static final String PID_FILTER = "pidFilter"; //$NON-NLS-1$
     static final String PID = "pid"; //$NON-NLS-1$
     static final String FACTORY_PID = "factoryPid"; //$NON-NLS-1$
-    static final String PLACEHOLDER_PID = "[Temporary PID replaced by real PID 
upon save]"; //$NON-NLS-1$
     static final String REFERER = "referer"; //$NON-NLS-1$
     static final String FACTORY_CREATE = "factoryCreate"; //$NON-NLS-1$
 
@@ -152,13 +153,7 @@ public class ConfigManager extends SimpleWebConsolePlugin 
implements OsgiManager
         {
             if ( request.getParameter( ConfigManager.ACTION_DELETE ) != null ) 
//$NON-NLS-1$
             {
-                // only delete if the PID is not our place holder
-                if ( !ConfigManager.PLACEHOLDER_PID.equals( pid ) )
-                {
-                    this.log( "applyConfiguration: Deleting configuration " + 
pid );
-                    cas.getConfiguration( pid, null ).delete();
-                }            
-
+                cas.deleteConfiguration( pid );
                 Util.sendJsonOk(response);
             } 
             else 
@@ -192,21 +187,17 @@ public class ConfigManager extends SimpleWebConsolePlugin 
implements OsgiManager
         final Configuration config;
 
         // should actually apply the configuration before redirecting
-        if ( request.getParameter( ACTION_CREATE ) != null )
-        {
-            config = cas.getPlaceholderConfiguration( pid );
+        if ( request.getParameter( ACTION_CREATE ) != null ) {
+            config = ConfigurationUtil.getPlaceholderConfiguration( pid );
             pid = config.getPid();
-        }
-        else
-        {
-            config = cas.getConfiguration( pid );
+        } else {
+            config = cas.findConfiguration( pid );
         }
 
         // check for configuration unbinding
         if ( request.getParameter( ACTION_UNBIND ) != null )
         {
-            if ( config != null && config.getBundleLocation() != null )
-            {
+            if ( config != null && config.getBundleLocation() != null ) {
                 config.setBundleLocation( UNBOUND_LOCATION );
 
             }
@@ -219,7 +210,7 @@ public class ConfigManager extends SimpleWebConsolePlugin 
implements OsgiManager
         response.setCharacterEncoding( "UTF-8" ); //$NON-NLS-1$
         final Locale loc = getLocale( request );
         final String locale = ( loc != null ) ? loc.toString() : null;
-        cas.printConfigurationJson( response.getWriter(), pid, config, 
pidFilter, locale );
+        cas.getJsonSupport().printConfigurationJson( response.getWriter(), 
pid, config, pidFilter, locale );
     }
 
 
@@ -327,7 +318,7 @@ public class ConfigManager extends SimpleWebConsolePlugin 
implements OsgiManager
                             {
                                 pw.print( ',' );
                             }
-                            ca.printConfigurationJson( pw, config.getPid(), 
config, null, locale );
+                            ca.getJsonSupport().printConfigurationJson( pw, 
config.getPid(), config, null, locale );
                             printComma = true;
                         }
                     }
@@ -410,21 +401,21 @@ public class ConfigManager extends SimpleWebConsolePlugin 
implements OsgiManager
         StringWriter json = new StringWriter();
         JSONWriter jw = new JSONWriter(json);
         jw.object();
-        final ConfigAdminSupport ca = getConfigurationAdminSupport();
+        final ConfigAdminSupport cas = getConfigurationAdminSupport();
         // check for osgi installer plugin
         @SuppressWarnings("unchecked")
         final Map<String, Object> labelMap = (Map<String, Object>) 
request.getAttribute(WebConsoleConstants.ATTR_LABEL_MAP);
         jw.key("jsonsupport").value( 
labelMap.containsKey("osgi-installer-config-printer") ); //$NON-NLS-1$
-        final boolean hasMetatype = ca.getMetaTypeSupport() != null;
-        jw.key("status").value( ca != null ? Boolean.TRUE : Boolean.FALSE); 
//$NON-NLS-1$
+        final boolean hasMetatype = cas.getMetaTypeSupport() != null;
+        jw.key("status").value( cas != null ? Boolean.TRUE : Boolean.FALSE); 
//$NON-NLS-1$
         jw.key("metatype").value( hasMetatype ? Boolean.TRUE : Boolean.FALSE); 
//$NON-NLS-1$
         boolean hasConfigs = true;
-        if ( ca != null )
+        if ( cas != null )
         {
-            hasConfigs = ca.listConfigurations( jw, pidFilter, locale, loc );
-            ca.listFactoryConfigurations( jw, pidFilter, locale );
+            hasConfigs = cas.getJsonSupport().listConfigurations( jw, 
pidFilter, locale, loc );
+            cas.getJsonSupport().listFactoryConfigurations( jw, pidFilter, 
locale );
         }
-        if ( !hasConfigs && !hasMetatype && ca != null ) {
+        if ( !hasConfigs && !hasMetatype && cas != null ) {
             jw.key("noconfigs").value(true); //$NON-NLS-1$
         } else {
             jw.key("noconfigs").value(false); //$NON-NLS-1$
@@ -433,9 +424,8 @@ public class ConfigManager extends SimpleWebConsolePlugin 
implements OsgiManager
         jw.endObject();
 
         // if a configuration is addressed, display it immediately
-        if ( request.getParameter( ACTION_CREATE ) != null && pid != null )
-        {
-            pid = PLACEHOLDER_PID; // new PlaceholderConfiguration( pid 
).getPid();
+        if ( request.getParameter( ACTION_CREATE ) != null && pid != null ) {
+            pid = ConfigurationUtil.getPlaceholderPid();
         }
 
 
@@ -457,14 +447,16 @@ public class ConfigManager extends SimpleWebConsolePlugin 
implements OsgiManager
         response.getWriter().print(TEMPLATE);
     }
 
-    private ConfigAdminSupport getConfigurationAdminSupport()
-    {
+    private ConfigAdminSupport getConfigurationAdminSupport() {
         Object configurationAdmin = getService( CONFIGURATION_ADMIN_NAME );
-        if ( configurationAdmin != null )
-        {
-            return new ConfigAdminSupport( this, this.getBundleContext(), 
configurationAdmin );
+        if ( configurationAdmin != null ) {
+            return new ConfigAdminSupport( this, configurationAdmin, 
Collections.emptyList() );
         }
         return null;
     }
+
+    public BundleContext getBundleContext() {
+        return super.getBundleContext();
+    }
 }
 
diff --git 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/configuration/ConfigurationUtil.java
 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/configuration/ConfigurationUtil.java
new file mode 100644
index 0000000..5ebff02
--- /dev/null
+++ 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/configuration/ConfigurationUtil.java
@@ -0,0 +1,150 @@
+/*
+ * 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.felix.webconsole.internal.configuration;
+
+
+import java.io.IOException;
+import java.util.Dictionary;
+import java.util.List;
+
+import org.apache.felix.webconsole.spi.ConfigurationHandler;
+import org.osgi.framework.Constants;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+
+
+public class ConfigurationUtil {
+
+    private static final String PLACEHOLDER_PID = "[Temporary PID replaced by 
real PID upon save]"; //$NON-NLS-1$
+
+    public static Configuration findConfiguration( final ConfigurationAdmin 
service, final String pid ) {
+        if ( pid != null ) {
+            try {
+                // we use listConfigurations to not create configuration
+                // objects persistently without the user providing actual
+                // configuration
+                String filter = '(' + Constants.SERVICE_PID + '=' + pid + ')';
+                Configuration[] configs = service.listConfigurations( filter );
+                if ( configs != null && configs.length > 0 ) {
+                    return configs[0];
+                }
+            } catch ( final InvalidSyntaxException | IOException e ) {
+                // should print message
+            }
+        }
+
+        // fallback to no configuration at all
+        return null;
+    }
+
+
+    public static Configuration getOrCreateConfiguration( final 
ConfigurationAdmin service, 
+            final List<ConfigurationHandler> handlers,
+            final String pid, 
+            final String factoryPid ) throws IOException {
+        Configuration cfg = null;
+        if ( !PLACEHOLDER_PID.equals(pid) ) {
+            cfg = findConfiguration(service, pid);
+        }
+        if ( cfg == null ) {
+            if ( factoryPid != null  ) {
+                for(final ConfigurationHandler handler : handlers) {
+                    handler.createFactoryConfiguration(factoryPid, null);
+                }
+                cfg = service.createFactoryConfiguration( factoryPid, null );
+            } else {
+                for(final ConfigurationHandler handler : handlers) {
+                    handler.createConfiguration(pid);
+                }        
+                cfg = service.getConfiguration( pid, null );
+            }
+        }
+        return cfg;
+    }
+
+
+    public static Configuration getPlaceholderConfiguration( final String 
factoryPid ) {
+        return new PlaceholderConfiguration( factoryPid );
+    }
+
+    public static String getPlaceholderPid() {
+        return PLACEHOLDER_PID;
+    }
+
+    private static class PlaceholderConfiguration implements Configuration {
+
+        private final String factoryPid;
+        private String bundleLocation;
+
+
+        PlaceholderConfiguration( final String factoryPid ) {
+            this.factoryPid = factoryPid;
+        }
+
+
+        @Override
+        public String getPid() {
+            return PLACEHOLDER_PID;
+        }
+
+
+        @Override
+        public String getFactoryPid() {
+            return factoryPid;
+        }
+
+        @Override
+        public void setBundleLocation( final String bundleLocation ) {
+            this.bundleLocation = bundleLocation;
+        }
+
+        @Override
+        public String getBundleLocation() {
+            return bundleLocation;
+        }
+
+        @Override
+        public Dictionary<String, Object> getProperties() {
+            // dummy configuration has no properties
+            return null;
+        }
+
+        @Override
+        public void update() {
+            // dummy configuration cannot be updated
+        }
+
+        @Override
+        public void update( Dictionary<String, ?> properties ) {
+            // dummy configuration cannot be updated
+        }
+
+        @Override
+        public void delete() {
+            // dummy configuration cannot be deleted
+        }
+
+        @Override
+        public long getChangeCount() {
+            // dummy configuration always returns 0
+            return 0;
+        }
+    }
+}
diff --git 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/misc/ServletSupport.java
 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/misc/ServletSupport.java
new file mode 100644
index 0000000..6f34c0c
--- /dev/null
+++ 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/misc/ServletSupport.java
@@ -0,0 +1,51 @@
+/*
+ * 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.felix.webconsole.internal.misc;
+
+import org.osgi.framework.BundleContext;
+
+public interface ServletSupport {
+
+    /**
+     * Log the message
+     * @param msg a log message
+     */
+    void log(String msg);
+
+    /**
+     * Log the message
+     * @param msg a log message
+     * @param t a throwable
+     */
+    void log(String message, Throwable t);
+
+    /**
+     * Get the bundle context
+     * @return The bundle contect
+     */
+    BundleContext getBundleContext();
+
+    /**
+     * Gets the service with the specified class name. 
+     *
+     * @param serviceName the service name to obtain
+     * @return the service or <code>null</code> if missing.
+     */
+    Object getService( String serviceName );
+}
diff --git 
a/webconsole/src/main/java/org/apache/felix/webconsole/spi/ConfigurationHandler.java
 
b/webconsole/src/main/java/org/apache/felix/webconsole/spi/ConfigurationHandler.java
new file mode 100644
index 0000000..f35f5e6
--- /dev/null
+++ 
b/webconsole/src/main/java/org/apache/felix/webconsole/spi/ConfigurationHandler.java
@@ -0,0 +1,64 @@
+/*
+ * 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.felix.webconsole.spi;
+
+import java.io.IOException;
+import java.util.Dictionary;
+
+/**
+ * A configuration handler allows to hook into the processing of 
configurations for
+ * the webconsole plugin.
+ * A handler can decide to hide configurations and properties but also 
implement
+ * additional validation.
+ * All configuration handlers are called in order of their service ranking, 
highest first.
+ */
+public interface ConfigurationHandler {
+
+    /**
+     * A new configuration with that pid should be created
+     * @param pid The pid
+     * @throws IOException For an error or {@link ValidationException} if 
creation is not allowed
+     */
+    void createConfiguration(String pid) throws IOException;
+
+    /**
+     * A new factory configuration with that pid should be created
+     * @param factoryPid The factory pid
+     * @param name Optional name, might be {@code null} if unknown
+     * @throws IOException For an error or {@link ValidationException} if 
creation is not allowed
+     */
+    void createFactoryConfiguration(String factoryPid, String name) throws 
IOException;
+
+    /**
+     * A configuration should be deleted
+     * @param factoryPid Optional factory pid
+     * @param pid The pid
+     * @throws IOException For an error or {@link ValidationException} if 
deletion is not allowed
+     */
+    void deleteConfiguration(String factoryPid, String pid) throws IOException;
+
+    /**
+     * A configuration should be updated
+     * @param factoryPid Optional factory pid
+     * @param pid The pid
+     * @param props Mutable dictionary
+     * @throws IOException For an error or {@link ValidationException} if 
deletion is not allowed
+     */
+    void updateConfiguration(String factoryPid, String pid, Dictionary<String, 
Object> props) throws IOException;
+}
\ No newline at end of file
diff --git 
a/webconsole/src/main/java/org/apache/felix/webconsole/spi/ValidationException.java
 
b/webconsole/src/main/java/org/apache/felix/webconsole/spi/ValidationException.java
new file mode 100644
index 0000000..a661b82
--- /dev/null
+++ 
b/webconsole/src/main/java/org/apache/felix/webconsole/spi/ValidationException.java
@@ -0,0 +1,32 @@
+/*
+ * 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.felix.webconsole.spi;
+
+import java.io.IOException;
+
+public class ValidationException extends IOException {
+
+    /**
+     * Create validation exception
+     * @param message The validation message
+     */
+    public ValidationException(final String message) {
+        super(message);
+    }    
+}
diff --git 
a/webconsole/src/main/java/org/apache/felix/webconsole/spi/package-info.java 
b/webconsole/src/main/java/org/apache/felix/webconsole/spi/package-info.java
new file mode 100644
index 0000000..c359a98
--- /dev/null
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/spi/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
[email protected]("1.0.0")
+package org.apache.felix.webconsole.spi;
+

Reply via email to