FELIX-3481 Add helper class to manage configurations with targeted PIDs
FELIX-3577 Start factoring out some inner classes

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1356232 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/helper/BaseTracker.java b/configadmin/src/main/java/org/apache/felix/cm/impl/helper/BaseTracker.java
new file mode 100644
index 0000000..d57bd41
--- /dev/null
+++ b/configadmin/src/main/java/org/apache/felix/cm/impl/helper/BaseTracker.java
@@ -0,0 +1,209 @@
+/*
+ * 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.cm.impl.helper;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.List;
+
+import org.apache.felix.cm.impl.CaseInsensitiveDictionary;
+import org.apache.felix.cm.impl.ConfigurationImpl;
+import org.apache.felix.cm.impl.ConfigurationManager;
+import org.apache.felix.cm.impl.RankingComparator;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.log.LogService;
+import org.osgi.util.tracker.ServiceTracker;
+
+/**
+ * The <code>BaseTracker</code> is the base class for tracking
+ * <code>ManagedService</code> and <code>ManagedServiceFactory</code>
+ * services. It maps their <code>ServiceRegistration</code> to the
+ * {@link ConfigurationMap} mapping their service PIDs to provided
+ * configuration.
+ */
+public abstract class BaseTracker<S> extends ServiceTracker<S, ConfigurationMap>
+{
+    protected final ConfigurationManager cm;
+
+
+    protected BaseTracker( ConfigurationManager cm, Class<S> type )
+    {
+        super( cm.getBundleContext(), type.getName(), null );
+        this.cm = cm;
+        open();
+    }
+
+
+    public ConfigurationMap addingService( ServiceReference<S> reference )
+    {
+        final String[] pids = getServicePid( reference );
+        configure( reference, pids );
+        return new ConfigurationMap( pids );
+    }
+
+
+    @Override
+    public void modifiedService( ServiceReference<S> reference, ConfigurationMap service )
+    {
+        String[] pids = getServicePid( reference );
+        if ( service.isDifferentPids( pids ) )
+        {
+            configure( reference, pids );
+            service.setConfiguredPids( pids );
+        }
+    }
+
+
+//    public void removedService( ServiceReference<ManagedServiceFactory> reference,
+//        ServiceHolder<ManagedServiceFactory> holder )
+//    {
+//        // nothing really to do
+//    }
+
+
+    //                 cm.configure( pids, reference, msf);
+    protected void configure( ServiceReference<S> reference, String[] pids )
+    {
+        if ( pids != null )
+        {
+            S service = getRealService( reference );
+            if ( service != null )
+            {
+                try
+                {
+                    if ( this.cm.isLogEnabled( LogService.LOG_DEBUG ) )
+                    {
+                        this.cm.log( LogService.LOG_DEBUG, "configure(ManagedService {0})", new Object[]
+                            { ConfigurationManager.toString( reference ) } );
+                    }
+
+                    for ( int i = 0; i < pids.length; i++ )
+                    {
+                        this.cm.configure( pids[i], reference, service, isFactory() );
+                    }
+                }
+                finally
+                {
+                    ungetRealService( reference );
+                }
+            }
+        }
+    }
+
+
+    public final List<ServiceReference<S>> getServices( final TargetedPID pid )
+    {
+        ServiceReference<S>[] refs = this.getServiceReferences();
+        if ( refs != null )
+        {
+            ArrayList<ServiceReference<S>> result = new ArrayList<ServiceReference<S>>( refs.length );
+            for ( ServiceReference<S> ref : refs )
+            {
+                if ( pid.matchesTarget( ref ) )
+                {
+                    ConfigurationMap map = this.getService( ref );
+                    if ( map != null && map.accepts( pid.getServicePid() ) )
+                    {
+                        result.add( ref );
+                    }
+                }
+            }
+
+            if ( result.size() > 1 )
+            {
+                Collections.sort( result, RankingComparator.SRV_RANKING );
+            }
+
+            return result;
+        }
+
+        return Collections.emptyList();
+    }
+
+
+    protected abstract boolean isFactory();
+
+    // Updates
+    public abstract void provide( ServiceReference<S> service, ConfigurationImpl config, Dictionary<String, ?> properties );
+
+
+    public abstract void remove( ServiceReference<S> service, ConfigurationImpl config );
+
+
+    protected final S getRealService( ServiceReference<S> reference )
+    {
+        return this.context.getService( reference );
+    }
+
+
+    protected final void ungetRealService( ServiceReference<S> reference )
+    {
+        this.context.ungetService( reference );
+    }
+
+    protected final Dictionary getProperties( Dictionary<String, ?> rawProperties, String targetPid, ServiceReference service )
+    {
+        Dictionary props = new CaseInsensitiveDictionary( rawProperties );
+        this.cm.callPlugins( props, targetPid, service, null /* config */ );
+        return props;
+    }
+
+    /**
+     * Returns the <code>service.pid</code> property of the service reference as
+     * an array of strings or <code>null</code> if the service reference does
+     * not have a service PID property.
+     * <p>
+     * The service.pid property may be a single string, in which case a single
+     * element array is returned. If the property is an array of string, this
+     * array is returned. If the property is a collection it is assumed to be a
+     * collection of strings and the collection is converted to an array to be
+     * returned. Otherwise (also if the property is not set) <code>null</code>
+     * is returned.
+     *
+     * @throws NullPointerException
+     *             if reference is <code>null</code>
+     * @throws ArrayStoreException
+     *             if the service pid is a collection and not all elements are
+     *             strings.
+     */
+    private static String[] getServicePid( ServiceReference reference )
+    {
+        Object pidObj = reference.getProperty( Constants.SERVICE_PID );
+        if ( pidObj instanceof String )
+        {
+            return new String[]
+                { ( String ) pidObj };
+        }
+        else if ( pidObj instanceof String[] )
+        {
+            return ( String[] ) pidObj;
+        }
+        else if ( pidObj instanceof Collection )
+        {
+            Collection pidCollection = ( Collection ) pidObj;
+            return ( String[] ) pidCollection.toArray( new String[pidCollection.size()] );
+        }
+
+        return null;
+    }
+
+}
\ No newline at end of file
diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/helper/ConfigurationMap.java b/configadmin/src/main/java/org/apache/felix/cm/impl/helper/ConfigurationMap.java
new file mode 100644
index 0000000..553a111
--- /dev/null
+++ b/configadmin/src/main/java/org/apache/felix/cm/impl/helper/ConfigurationMap.java
@@ -0,0 +1,82 @@
+/*
+ * 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.cm.impl.helper;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class ConfigurationMap
+{
+    private Map<String, Object> configurations;
+
+
+    public ConfigurationMap( final String[] configuredPids )
+    {
+        this.configurations = Collections.emptyMap();
+        setConfiguredPids( configuredPids );
+    }
+
+    public boolean accepts(final String servicePid) {
+        return configurations.containsKey( servicePid );
+    }
+
+    public void setConfiguredPids( String[] configuredPids )
+    {
+        HashMap<String, Object> newConfigs = new HashMap<String, Object>();
+        if ( configuredPids != null )
+        {
+            for ( String pid : configuredPids )
+            {
+                newConfigs.put( pid, this.configurations.get( pid ) );
+            }
+        }
+        this.configurations = newConfigs;
+    }
+
+
+    public boolean isDifferentPids( final String[] pids )
+    {
+        if ( this.configurations.isEmpty() && pids == null )
+        {
+            return false;
+        }
+        else if ( this.configurations.isEmpty() )
+        {
+            return true;
+        }
+        else if ( pids == null )
+        {
+            return true;
+        }
+        else if ( this.configurations.size() != pids.length )
+        {
+            return true;
+        }
+        else
+        {
+            Set<String> thisPids = this.configurations.keySet();
+            HashSet<String> otherPids = new HashSet<String>( Arrays.asList( pids ) );
+            return !thisPids.equals( otherPids );
+        }
+    }
+}
\ No newline at end of file
diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/helper/ManagedServiceFactoryTracker.java b/configadmin/src/main/java/org/apache/felix/cm/impl/helper/ManagedServiceFactoryTracker.java
new file mode 100644
index 0000000..c9a36ca
--- /dev/null
+++ b/configadmin/src/main/java/org/apache/felix/cm/impl/helper/ManagedServiceFactoryTracker.java
@@ -0,0 +1,87 @@
+/*
+ * 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.cm.impl.helper;
+
+import java.util.Dictionary;
+
+import org.apache.felix.cm.impl.ConfigurationImpl;
+import org.apache.felix.cm.impl.ConfigurationManager;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.cm.ManagedServiceFactory;
+
+public class ManagedServiceFactoryTracker extends BaseTracker<ManagedServiceFactory>
+{
+
+    public ManagedServiceFactoryTracker( ConfigurationManager cm )
+    {
+        super( cm, ManagedServiceFactory.class );
+    }
+
+
+    @Override
+    protected boolean isFactory()
+    {
+        return true;
+    }
+
+
+    @Override
+    public void provide( ServiceReference<ManagedServiceFactory> reference, final ConfigurationImpl config, final Dictionary<String, ?> rawProps )
+    {
+        ManagedServiceFactory service = getRealService( reference );
+        if ( service != null )
+        {
+            try
+            {
+                Dictionary props = getProperties( rawProps, config.getFactoryPid(), reference );
+                service.updated( config.getPid(), props );
+            }
+            catch ( Throwable t )
+            {
+                this.cm.handleCallBackError( t, reference, config );
+            }
+            finally
+            {
+                this.ungetRealService( reference );
+            }
+        }
+    }
+
+
+    @Override
+    public void remove( ServiceReference<ManagedServiceFactory> reference, final ConfigurationImpl config )
+    {
+        ManagedServiceFactory service = this.getRealService( reference );
+        if ( service != null )
+        {
+            try
+            {
+                service.deleted( config.getPid() );
+            }
+            catch ( Throwable t )
+            {
+                this.cm.handleCallBackError( t, reference, config );
+            }
+            finally
+            {
+                this.ungetRealService( reference );
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/helper/ManagedServiceTracker.java b/configadmin/src/main/java/org/apache/felix/cm/impl/helper/ManagedServiceTracker.java
new file mode 100644
index 0000000..2b6bbea
--- /dev/null
+++ b/configadmin/src/main/java/org/apache/felix/cm/impl/helper/ManagedServiceTracker.java
@@ -0,0 +1,84 @@
+/*
+ * 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.cm.impl.helper;
+
+import java.util.Dictionary;
+
+import org.apache.felix.cm.impl.ConfigurationImpl;
+import org.apache.felix.cm.impl.ConfigurationManager;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.cm.ManagedService;
+
+public class ManagedServiceTracker extends BaseTracker<ManagedService>
+{
+
+
+    public ManagedServiceTracker( ConfigurationManager cm )
+    {
+        super( cm, ManagedService.class );
+    }
+
+
+    @Override
+    protected boolean isFactory()
+    {
+        return false;
+    }
+
+
+    @Override
+    public void provide( ServiceReference<ManagedService> service, final ConfigurationImpl config, Dictionary<String, ?> properties )
+    {
+        ManagedService srv = this.getRealService( service );
+        if ( srv != null )
+        {
+            try
+            {
+                Dictionary props = getProperties( properties, config.getPid(), service );
+                srv.updated( props );
+            }
+            catch ( Throwable t )
+            {
+                this.cm.handleCallBackError( t, service, config );
+            }
+            finally
+            {
+                this.ungetRealService( service );
+            }
+        }
+    }
+
+    @Override
+    public void remove( ServiceReference<ManagedService> service, final ConfigurationImpl config )
+    {
+        ManagedService srv = this.getRealService( service );
+        try
+        {
+            srv.updated( null );
+        }
+        catch ( Throwable t )
+        {
+            this.cm.handleCallBackError( t, service, config );
+        }
+        finally
+        {
+            this.ungetRealService( service );
+        }
+    }
+}
\ No newline at end of file
diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/helper/TargetedPID.java b/configadmin/src/main/java/org/apache/felix/cm/impl/helper/TargetedPID.java
new file mode 100644
index 0000000..2d31da5
--- /dev/null
+++ b/configadmin/src/main/java/org/apache/felix/cm/impl/helper/TargetedPID.java
@@ -0,0 +1,267 @@
+/*
+ * 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.cm.impl.helper;
+
+
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
+
+
+/**
+ * The <code>TargetedPID</code> class represents a targeted PID as read
+ * from a configuration object.
+ * <p>
+ * For a factory configuration the <code>TargetedPID</code> represents
+ * the factory PID of the configuration. Otherwise it represents the
+ * PID itself of the configuration.
+ */
+public class TargetedPID
+{
+
+    private final String rawPid;
+
+    private final String servicePid;
+
+    private final String symbolicName;
+    private final String version;
+    private final String location;
+
+
+    public TargetedPID( final String rawPid )
+    {
+        this.rawPid = rawPid;
+
+        if ( rawPid.indexOf( '|' ) < 0 )
+        {
+            this.servicePid = rawPid;
+            this.symbolicName = null;
+            this.version = null;
+            this.location = null;
+        }
+        else
+        {
+            int start = 0;
+            int end = rawPid.indexOf( '|' );
+            this.servicePid = rawPid.substring( start, end );
+
+            start = end + 1;
+            end = rawPid.indexOf( '|', start );
+            if ( end >= 0 )
+            {
+                this.symbolicName = rawPid.substring( start, end );
+                start = end + 1;
+                end = rawPid.indexOf( '|', start );
+                if ( end >= 0 )
+                {
+                    this.version = rawPid.substring( start, end );
+                    this.location = rawPid.substring( end + 1 );
+                }
+                else
+                {
+                    this.version = rawPid.substring( start );
+                    this.location = null;
+                }
+            }
+            else
+            {
+                this.symbolicName = rawPid.substring( start );
+                this.version = null;
+                this.location = null;
+            }
+        }
+    }
+
+
+    /**
+     * Returns true if the target of this PID (bundle symbolic name,
+     * version, and location) match the bundle registering the referenced
+     * service.
+     * <p>
+     * This method just checks the target not the PID value itself, so
+     * this method returning <code>true</code> does not indicate whether
+     * the service actually is registered with a service PID equal to the
+     * raw PID of this targeted PID.
+     * <p>
+     * This method also returns <code>false</code> if the service has
+     * concurrently been unregistered and the registering bundle is now
+     * <code>null</code>.
+     *
+     * @param reference <code>ServiceReference</code> to the registered
+     *      service
+     * @return <code>true</code> if the referenced service matches the
+     *      target of this PID.
+     */
+    public boolean matchesTarget( ServiceReference<?> reference )
+    {
+        // This is not really targeted
+        if ( this.symbolicName == null )
+        {
+            return true;
+        }
+
+        final Bundle serviceBundle = reference.getBundle();
+
+        // already unregistered
+        if ( serviceBundle == null )
+        {
+            return false;
+        }
+
+        // bundle symbolic names don't match
+        if ( !this.symbolicName.equals( serviceBundle.getSymbolicName() ) )
+        {
+            return false;
+        }
+
+        // assert version match
+        if ( this.version == null )
+        {
+            return true;
+        }
+        else if ( !this.version.equals( serviceBundle.getVersion().toString() ) )
+        {
+            return false;
+        }
+
+        // assert bundle location match
+        return this.location == null || this.location.equals( serviceBundle.getLocation() );
+    }
+
+
+    /**
+     * Returns the service PID of this targeted PID which basically is
+     * the targeted PID without the targeting information.
+     * @return
+     */
+    public String getServicePid()
+    {
+        return servicePid;
+    }
+
+    /**
+     * Returns how string this <code>TargetedPID</code> matches the
+     * given <code>ServiceReference</code>. The return value is one of
+     * those listed in the table:
+     *
+     * <table>
+     * <tr><th>level</th><th>Description</th></tr>
+     * <tr><td>-1</td><td>The targeted PID does not match at all. This means
+     *      that either the service PID, the bundle's symbolic name, the
+     *      bundle's version or bundle's location does not match the
+     *      respective non-<code>null</code> parts of the targeted PID.
+     *      This value is also returned if the raw PID fully matches the
+     *      service PID.</td></tr>
+     * <tr><td>0</td><td>The targeted PID only has the service PID which
+     *      also matches. The bundle's symbolic name, version, and
+     *      location are not considered.</td></tr>
+     * <tr><td>1</td><td>The targeted PID only has the service PID and
+     *      bundle symbolic name which both match. The bundle's version and
+     *      location are not considered.</td></tr>
+     * <tr><td>2</td><td>The targeted PID only has the service PID, bundle
+     *      symbolic name and version which all match. The bundle's
+     *      location is not considered.</td></tr>
+     * <tr><td>3</td><td>The targeted PID has the service PID as well as
+     *      the bundle symbolic name, version, and location which all
+     *      match.</td></tr>
+     * </table>
+     *
+     * @param ref
+     * @return
+     */
+    public int matchLevel( final ServiceReference ref )
+    {
+        final Object servicePid = ref.getProperty( Constants.SERVICE_PID );
+
+        // in case the service PID contains | characters, this allows
+        // it to match the raw version of the targeted PID
+        if ( this.rawPid.equals( servicePid ) )
+        {
+            return 1;
+        }
+
+        if ( !this.servicePid.equals( servicePid ) )
+        {
+            return -1;
+        }
+
+        if ( this.symbolicName == null )
+        {
+            return 0;
+        }
+
+        final Bundle refBundle = ref.getBundle();
+        if ( !this.symbolicName.equals( refBundle.getSymbolicName() ) )
+        {
+            return -1;
+        }
+
+        if ( this.version == null )
+        {
+            return 1;
+        }
+
+        if ( !this.version.equals( refBundle.getHeaders().get( Constants.BUNDLE_VERSION ) ) )
+        {
+            return -1;
+        }
+
+        if ( this.location == null )
+        {
+            return 2;
+        }
+
+        if ( !this.location.equals( refBundle.getLocation() ) )
+        {
+            return -1;
+        }
+
+        return 3;
+    }
+
+
+    @Override
+    public int hashCode()
+    {
+        return this.rawPid.hashCode();
+    }
+
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( obj == null )
+        {
+            return true;
+        }
+        else if ( obj == this )
+        {
+            return true;
+        }
+
+        // assume equality if same class and raw PID equals
+        if ( this.getClass() == obj.getClass() )
+        {
+            return this.rawPid.equals( ( ( TargetedPID ) obj ).rawPid );
+        }
+
+        // not the same class or different raw PID
+        return false;
+    }
+}