blob: 4d97ce70b42914f1592ceb22f5578661ef709d4f [file] [log] [blame]
/*
* 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;
import java.io.IOException;
import java.util.Arrays;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.Hashtable;
import org.apache.felix.cm.PersistenceManager;
import org.apache.felix.cm.impl.helper.TargetedPID;
import org.osgi.framework.Constants;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.log.LogService;
/**
* The <code>ConfigurationImpl</code> is the backend implementation of the
* Configuration Admin Service Specification <i>Configuration object</i>
* (section 104.4). Instances of this class are shared by multiple instances of
* the {@link ConfigurationAdapter} class, whose instances are actually returned
* to clients.
*/
public class ConfigurationImpl
{
/*
* Concurrency note: There is a slight (but real) chance of a race condition
* between a configuration update and a ManagedService[Factory] registration.
* Per the specification a ManagedService must be called with configuration
* or null when registered and a ManagedService must be called with currently
* existing configuration when registered. Also the ManagedService[Factory]
* must be updated when the configuration is updated.
*
* Consider now this situation of two threads T1 and T2:
*
* T1. create and update configuration
* ConfigurationImpl.update persists configuration and sets field
* Thread preempted
*
* T2. ManagedServiceUpdate constructor reads configuration
* Uses configuration already persisted by T1 for update
* Schedules task to update service with the configuration
*
* T1. Runs again creating the UpdateConfiguration task with the
* configuration persisted before being preempted
* Schedules task to update service
*
* Update Thread:
* Updates ManagedService with configuration prepared by T2
* Updates ManagedService with configuration prepared by T1
*
* The correct behaviour would be here, that the second call to update
* would not take place. We cannot at this point in time easily fix
* this issue. Also, it seems that changes for this to happen are
* small.
*
* This class provides modification counter (lastModificationTime)
* which is incremented on each change of the configuration. This
* helps the update tasks in the ConfigurationManager to log the
* revision of the configuration supplied.
*/
/**
* The name of a synthetic property stored in the persisted configuration
* data to indicate that the configuration data is new, that is created but
* never updated (value is "_felix_.cm.newConfiguration").
* <p>
* This special property is stored by the
* {@link #ConfigurationImpl(ConfigurationManager, PersistenceManager, String, String, String)}
* constructor, when the configuration is first created and persisted and is
* interpreted by the
* {@link #ConfigurationImpl(ConfigurationManager, PersistenceManager, Dictionary)}
* method when the configuration data is loaded in a new object.
* <p>
* The goal of this property is to keep the information on whether
* configuration data is new (but persisted as per the spec) or has already
* been assigned with possible no data.
*/
private static final String CONFIGURATION_NEW = "_felix_.cm.newConfiguration";
private static final String PROPERTY_LOCKED = ":org.apache.felix.configadmin.locked:";
private static final String PROPERTY_REVISION = ":org.apache.felix.configadmin.revision:";
/**
* The factory PID of this configuration or <code>null</code> if this
* is not a factory configuration.
*/
private final TargetedPID factoryPID;
/**
* The statically bound bundle location, which is set explicitly by calling
* the Configuration.setBundleLocation(String) method or when the
* configuration was created with the two-argument method.
*/
private volatile String staticBundleLocation;
/**
* The bundle location from dynamic binding. This value is set as the
* configuration or factory is assigned to a ManagedService[Factory].
*/
private volatile String dynamicBundleLocation;
/**
* The configuration data of this configuration instance. This is a private
* copy of the properties of which a copy is made when the
* {@link #getProperties()} method is called. This field is
* <code>null</code> if the configuration has been created and never been
* updated with acutal configuration properties.
*/
private volatile CaseInsensitiveDictionary properties;
/**
* Flag indicating that this configuration has been deleted.
*
* @see #isDeleted()
*/
private volatile boolean isDeleted;
/**
* Configuration revision counter incremented each time the
* {@link #properties} is set (in the constructor or the
* {@link #configure(Dictionary)} method. This counter is
* persisted transparently so that {@link NotCachablePersistenceManager}
* can provide a proper change count. The persistence is forward
* compatible such that previously persisted configurations are
* handled gracefully.
*/
private volatile long revision;
private volatile boolean locked;
/**
* The {@link ConfigurationManager configuration manager} instance which
* caused this configuration object to be created.
*/
private final ConfigurationManager configurationManager;
// the persistence manager storing this factory mapping
private final PersistenceManager persistenceManager;
// the basic ID of this instance
private final TargetedPID baseId;
public ConfigurationImpl( ConfigurationManager configurationManager, PersistenceManager persistenceManager,
Dictionary<String, Object> properties )
{
if ( configurationManager == null )
{
throw new IllegalArgumentException( "ConfigurationManager must not be null" );
}
if ( persistenceManager == null )
{
throw new IllegalArgumentException( "PersistenceManager must not be null" );
}
this.configurationManager = configurationManager;
this.persistenceManager = persistenceManager;
this.baseId = new TargetedPID( ( String ) properties.remove( Constants.SERVICE_PID ) );
final String factoryPid = ( String ) properties.remove( ConfigurationAdmin.SERVICE_FACTORYPID );
this.factoryPID = ( factoryPid == null ) ? null : new TargetedPID( factoryPid );
this.isDeleted = false;
// set bundle location from persistence and/or check for dynamic binding
this.staticBundleLocation = ( String ) properties.remove( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) ;
this.dynamicBundleLocation = configurationManager.getDynamicBundleLocation( this.baseId.toString() );
// set the properties internally
configureFromPersistence( properties );
}
ConfigurationImpl( ConfigurationManager configurationManager, PersistenceManager persistenceManager, String pid,
String factoryPid, String bundleLocation ) throws IOException
{
if ( configurationManager == null )
{
throw new IllegalArgumentException( "ConfigurationManager must not be null" );
}
if ( persistenceManager == null )
{
throw new IllegalArgumentException( "PersistenceManager must not be null" );
}
this.configurationManager = configurationManager;
this.persistenceManager = persistenceManager;
this.baseId = new TargetedPID( pid );
this.factoryPID = ( factoryPid == null ) ? null : new TargetedPID( factoryPid );
this.isDeleted = false;
// set bundle location from persistence and/or check for dynamic binding
this.staticBundleLocation = bundleLocation;
this.dynamicBundleLocation = configurationManager.getDynamicBundleLocation( this.baseId.toString() );
// first "update"
this.properties = null;
this.revision = 1;
// this is a new configuration object, store immediately unless
// the new configuration object is created from a factory, in which
// case the configuration is only stored when first updated
if ( factoryPid == null )
{
storeNewConfiguration();
}
}
/**
* Returns <code>true</code> if the ConfigurationManager of this
* configuration is still active.
*/
boolean isActive()
{
return configurationManager.isActive();
}
void storeSilently()
{
try
{
this.store();
}
catch ( IOException ioe )
{
Log.logger.log( LogService.LOG_ERROR, "Persisting ID {0} failed", new Object[]
{ this.baseId, ioe } );
}
}
static protected void replaceProperty( Dictionary<String, Object> properties, String key, String value )
{
if ( value == null )
{
properties.remove( key );
}
else
{
properties.put( key, value );
}
}
public void delete() throws IOException
{
this.isDeleted = true;
this.persistenceManager.delete( this.getPidString() );
configurationManager.setDynamicBundleLocation( this.getPidString(), null );
configurationManager.deleted( this );
}
public String getPidString()
{
return this.baseId.toString();
}
public TargetedPID getPid()
{
return this.baseId;
}
public String getFactoryPidString()
{
return (factoryPID == null) ? null : factoryPID.toString();
}
public TargetedPID getFactoryPid()
{
return factoryPID;
}
/**
* Returns the "official" bundle location as visible from the outside
* world of code calling into the Configuration.getBundleLocation() method.
* <p>
* In other words: The {@link #getStaticBundleLocation()} is returned if
* not <code>null</code>. Otherwise the {@link #getDynamicBundleLocation()}
* is returned (which may also be <code>null</code>).
*/
String getBundleLocation()
{
if ( staticBundleLocation != null )
{
return staticBundleLocation;
}
return dynamicBundleLocation;
}
String getDynamicBundleLocation()
{
return dynamicBundleLocation;
}
String getStaticBundleLocation()
{
return staticBundleLocation;
}
void setStaticBundleLocation( final String bundleLocation )
{
// CM 1.4; needed for bundle location change at the end
final String oldBundleLocation = getBundleLocation();
// 104.15.2.8 The bundle location will be set persistently
this.staticBundleLocation = bundleLocation;
storeSilently();
// FELIX-3360: Always clear dynamic binding if a new static
// location is set. The static location is the relevant binding
// for a configuration unless it is not explicitly set.
setDynamicBundleLocation( null, false );
// CM 1.4
this.configurationManager.locationChanged( this, oldBundleLocation );
}
void setDynamicBundleLocation( final String bundleLocation, final boolean dispatchConfiguration )
{
// CM 1.4; needed for bundle location change at the end
final String oldBundleLocation = getBundleLocation();
this.dynamicBundleLocation = bundleLocation;
this.configurationManager.setDynamicBundleLocation( this.getPidString(), bundleLocation );
// CM 1.4
if ( dispatchConfiguration )
{
this.configurationManager.locationChanged( this, oldBundleLocation );
}
}
/**
* Dynamically binds this configuration to the given location unless
* the configuration is already bound (statically or dynamically). In
* the case of this configuration to be dynamically bound a
* <code>CM_LOCATION_CHANGED</code> event is dispatched.
*/
void tryBindLocation( final String bundleLocation )
{
if ( this.getBundleLocation() == null )
{
Log.logger.log( LogService.LOG_DEBUG, "Dynamically binding config {0} to {1}", new Object[]
{ getPidString(), bundleLocation } );
setDynamicBundleLocation( bundleLocation, true );
}
}
/**
* Returns an optionally deep copy of the properties of this configuration
* instance.
* <p>
* This method returns a copy of the internal dictionary. If the
* <code>deepCopy</code> parameter is true array and collection values are
* copied into new arrays or collections. Otherwise just a new dictionary
* referring to the same objects is returned.
*
* @param deepCopy
* <code>true</code> if a deep copy is to be returned.
* @return the configuration properties
*/
public Dictionary<String, Object> getProperties( boolean deepCopy )
{
// no properties yet
if ( properties == null )
{
return null;
}
CaseInsensitiveDictionary props = new CaseInsensitiveDictionary( properties, deepCopy );
// fix special properties (pid, factory PID, bundle location)
setAutoProperties( props, false );
return props;
}
/* (non-Javadoc)
* @see org.osgi.service.cm.Configuration#update()
*/
public void update() throws IOException
{
// read configuration from persistence (again)
if ( persistenceManager.exists( getPidString() ) )
{
@SuppressWarnings("unchecked")
Dictionary<String, Object> properties = persistenceManager.load( getPidString() );
// ensure serviceReference pid
String servicePid = ( String ) properties.get( Constants.SERVICE_PID );
if ( servicePid != null && !getPidString().equals( servicePid ) )
{
throw new IOException( "PID of configuration file does match requested PID; expected " + getPidString()
+ ", got " + servicePid );
}
// we're doing a local update, so override the properties revision
properties.put( PROPERTY_REVISION, Long.valueOf(getRevision()) );
configureFromPersistence( properties );
}
// update the service but do not fire an CM_UPDATED event
configurationManager.updated( this, false );
}
/**
* @see org.osgi.service.cm.Configuration#update(java.util.Dictionary)
*/
public void update( Dictionary<String, ?> properties ) throws IOException
{
CaseInsensitiveDictionary newProperties = new CaseInsensitiveDictionary( properties );
Log.logger.log( LogService.LOG_DEBUG, "Updating config {0} with {1}", new Object[]
{ getPidString(), newProperties } );
setAutoProperties( newProperties, true );
// persist new configuration
newProperties.put( PROPERTY_REVISION, Long.valueOf(getRevision()) );
persistenceManager.store( getPidString(), newProperties );
// finally assign the configuration for use
configure( newProperties );
// update the service and fire an CM_UPDATED event
configurationManager.updated( this, true );
}
//---------- Object overwrites --------------------------------------------
@Override
public boolean equals( Object obj )
{
if ( obj == this )
{
return true;
}
if ( obj instanceof Configuration )
{
return getPidString().equals( ( ( Configuration ) obj ).getPid() );
}
return false;
}
@Override
public int hashCode()
{
return getPidString().hashCode();
}
@Override
public String toString()
{
return "Configuration PID=" + getPidString() + ", factoryPID=" + factoryPID + ", bundleLocation=" + getBundleLocation();
}
// ---------- private helper -----------------------------------------------
/**
* Stores the configuration if it is a newly factory configuration
* which has not been persisted yet.
* <p>
* This is used to ensure a configuration c as in
* <pre>
* Configuration cf = cm.createFactoryConfiguration(factoryPid);
* Configuration c = cm.getConfiguration(cf.getPid());
* </pre>
* is persisted after <code>getConfiguration</code> while
* <code>createConfiguration</code> alone does not persist yet.
*/
void ensureFactoryConfigPersisted() throws IOException
{
if ( this.factoryPID != null && isNew() && !persistenceManager.exists( getPidString() ) )
{
storeNewConfiguration();
}
}
/**
* Persists a new (freshly created) configuration with a marker for
* it to be a new configuration.
*
* @throws IOException If an error occurrs storing the configuraiton
*/
private void storeNewConfiguration() throws IOException
{
Dictionary<String, Object> props = new Hashtable<>();
setAutoProperties( props, true );
props.put( CONFIGURATION_NEW, Boolean.TRUE );
props.put( PROPERTY_REVISION, Long.valueOf(getRevision()) );
persistenceManager.store( getPidString(), props );
}
void store() throws IOException
{
// we don't need a deep copy, since we are not modifying
// any value in the dictionary itself. we are just adding
// properties to it, which are required for storing
Dictionary<String, Object> props = getProperties( false );
// if this is a new configuration, we just use an empty Dictionary
if ( props == null )
{
props = new Hashtable<>();
// add automatic properties including the bundle location (if
// statically bound)
setAutoProperties( props, true );
}
else
{
replaceProperty( props, ConfigurationAdmin.SERVICE_BUNDLELOCATION, getStaticBundleLocation() );
}
if ( this.locked )
{
props.put(PROPERTY_LOCKED, this.locked);
}
else
{
props.remove(PROPERTY_LOCKED);
}
// only store now, if this is not a new configuration
props.put( PROPERTY_REVISION, Long.valueOf(getRevision()) );
persistenceManager.store( getPidString(), props );
}
/**
* Returns the revision of this configuration object.
* <p>
* When getting both the configuration properties and this revision
* counter, the two calls should be synchronized on this instance to
* ensure configuration values and revision counter match.
*/
public long getRevision()
{
return revision;
}
/**
* Returns <code>false</code> if this configuration contains configuration
* properties. Otherwise <code>true</code> is returned and this is a
* newly creted configuration object whose {@link #update(Dictionary)}
* method has never been called.
*/
boolean isNew()
{
return properties == null;
}
/**
* Returns <code>true</code> if this configuration has already been deleted
* on the persistence.
*/
boolean isDeleted()
{
return isDeleted;
}
private void configureFromPersistence( Dictionary<String, Object> properties )
{
// if the this is not an empty/new configuration, accept the properties
// otherwise just set the properties field to null
if ( properties.get( CONFIGURATION_NEW ) == null )
{
configure( properties );
}
else
{
configure( null );
}
}
private void configure( final Dictionary<String, Object> properties )
{
final Object revisionValue = properties == null ? null : properties.get(PROPERTY_REVISION);
final Object lockedValue = properties == null ? null : properties.get(PROPERTY_LOCKED);
if ( lockedValue != null )
{
this.locked = true;
}
final CaseInsensitiveDictionary newProperties;
if ( properties == null )
{
newProperties = null;
}
else
{
// remove predefined properties
clearAutoProperties( properties );
// ensure CaseInsensitiveDictionary
if ( properties instanceof CaseInsensitiveDictionary )
{
newProperties = ( CaseInsensitiveDictionary ) properties;
}
else
{
newProperties = new CaseInsensitiveDictionary( properties );
}
}
synchronized ( this )
{
this.properties = newProperties;
this.revision = (revisionValue != null) ? 1 + ((Long)revisionValue).longValue() : ++revision ;
}
}
void setAutoProperties( Dictionary<String, Object> properties, boolean withBundleLocation )
{
// set pid and factory pid in the properties
replaceProperty( properties, Constants.SERVICE_PID, getPidString() );
replaceProperty( properties, ConfigurationAdmin.SERVICE_FACTORYPID, getFactoryPidString() );
// bundle location is not set here
if ( withBundleLocation )
{
replaceProperty( properties, ConfigurationAdmin.SERVICE_BUNDLELOCATION, getStaticBundleLocation() );
}
else
{
properties.remove( ConfigurationAdmin.SERVICE_BUNDLELOCATION );
}
properties.remove( PROPERTY_LOCKED );
properties.remove( PROPERTY_REVISION );
}
static void setAutoProperties( Dictionary<String, Object> properties, String pid, String factoryPid )
{
replaceProperty( properties, Constants.SERVICE_PID, pid );
replaceProperty( properties, ConfigurationAdmin.SERVICE_FACTORYPID, factoryPid );
properties.remove( ConfigurationAdmin.SERVICE_BUNDLELOCATION );
properties.remove( PROPERTY_LOCKED );
properties.remove( PROPERTY_REVISION );
}
private static final String[] AUTO_PROPS = new String[] {
Constants.SERVICE_PID,
ConfigurationAdmin.SERVICE_FACTORYPID,
ConfigurationAdmin.SERVICE_BUNDLELOCATION,
PROPERTY_LOCKED, PROPERTY_REVISION
};
static void clearAutoProperties( Dictionary<String, Object> properties )
{
for(final String p : AUTO_PROPS)
{
properties.remove( p );
}
}
public void setLocked(final boolean flag) throws IOException
{
this.locked = flag;
store();
}
/**
* Compare the two properties, ignoring auto properties
* @param props1 Set of properties
* @param props2 Set of properties
* @return {@code true} if the set of properties is equal
*/
static boolean equals( Dictionary<String, Object> props1, Dictionary<String, Object> props2)
{
if (props1 == null) {
if (props2 == null) {
return true;
} else {
return false;
}
} else if (props2 == null) {
return false;
}
final int count1 = getCount(props1);
final int count2 = getCount(props2);
if ( count1 != count2 )
{
return false;
}
final Enumeration<String> keys = props1.keys();
while ( keys.hasMoreElements() )
{
final String key = keys.nextElement();
if ( !isAutoProp(key) )
{
final Object val1 = props1.get(key);
final Object val2 = props2.get(key);
if ( val1 == null )
{
if ( val2 != null )
{
return false;
}
}
else
{
if ( val2 == null )
{
return false;
}
// arrays are compared using Arrays.equals
if ( val1.getClass().isArray() )
{
if ( !val2.getClass().isArray() )
{
return false;
}
final Object[] a1 = convertToObjectArray(val1);
final Object[] a2 = convertToObjectArray(val2);
if ( ! Arrays.equals(a1, a2) )
{
return false;
}
}
else if ( !val1.equals(val2) )
{
return false;
}
}
}
}
return true;
}
/**
* Convert the object to an array
* @param value The array
* @return an object array
*/
private static Object[] convertToObjectArray(final Object value)
{
final Object[] values;
if (value instanceof long[])
{
final long[] a = (long[])value;
values = new Object[a.length];
for(int i=0;i<a.length;i++)
{
values[i] = a[i];
}
}
else if (value instanceof int[]) {
final int[] a = (int[])value;
values = new Object[a.length];
for(int i=0;i<a.length;i++)
{
values[i] = a[i];
}
} else if (value instanceof double[])
{
final double[] a = (double[])value;
values = new Object[a.length];
for(int i=0;i<a.length;i++)
{
values[i] = a[i];
}
}
else if (value instanceof byte[])
{
final byte[] a = (byte[])value;
values = new Object[a.length];
for(int i=0;i<a.length;i++)
{
values[i] = a[i];
}
}
else if (value instanceof float[])
{
final float[] a = (float[])value;
values = new Object[a.length];
for(int i=0;i<a.length;i++)
{
values[i] = a[i];
}
}
else if (value instanceof short[])
{
final short[] a = (short[])value;
values = new Object[a.length];
for(int i=0;i<a.length;i++)
{
values[i] = a[i];
}
}
else if (value instanceof boolean[])
{
final boolean[] a = (boolean[])value;
values = new Object[a.length];
for(int i=0;i<a.length;i++)
{
values[i] = a[i];
}
}
else if (value instanceof char[])
{
final char[] a = (char[])value;
values = new Object[a.length];
for(int i=0;i<a.length;i++)
{
values[i] = a[i];
}
}
else
{
values = (Object[]) value;
}
return values;
}
static boolean isAutoProp(final String name)
{
for(final String p : AUTO_PROPS)
{
if ( p.equals(name) )
{
return true;
}
}
return false;
}
static int getCount( Dictionary<String, Object> props )
{
int count = (props == null ? 0 : props.size());
if ( props != null )
{
for(final String p : AUTO_PROPS)
{
if ( props.get(p) != null )
{
count--;
}
}
}
return count;
}
public boolean isLocked()
{
return this.locked;
}
final ConfigurationManager getConfigurationManager()
{
return this.configurationManager;
}
}