blob: a338f15b1103be95c6929b82997f08ff12a1adbb [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.scr.impl.manager;
import java.io.IOException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Collection;
import java.util.Collections;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.felix.scr.impl.ComponentRegistry;
import org.apache.felix.scr.impl.logger.ScrLogger;
import org.apache.felix.scr.impl.metadata.TargetedPID;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.cm.ConfigurationEvent;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ConfigurationListener;
import org.osgi.service.cm.ConfigurationPermission;
import org.osgi.service.cm.ManagedService;
import org.osgi.service.log.LogService;
public abstract class RegionConfigurationSupport
{
private final ScrLogger logger;
private final ServiceReference<ConfigurationAdmin> caReference;
private final BundleContext caBundleContext;
private final Long bundleId;
private final AtomicInteger referenceCount = new AtomicInteger( 1 );
// the service registration of the ConfigurationListener service
private volatile ServiceRegistration<ConfigurationListener> m_registration;
/**
*
* @param bundleContext of the ConfigurationAdmin we are tracking
* @param registry
*/
public RegionConfigurationSupport(final ScrLogger logger, final ServiceReference<ConfigurationAdmin> reference, Bundle bundle)
{
this.logger = logger;
this.caReference = reference;
this.bundleId = bundle.getBundleId();
this.caBundleContext = bundle.getBundleContext();
}
public void start()
{
// register as listener for configurations
final Dictionary<String, Object> props = new Hashtable<>();
props.put( Constants.SERVICE_DESCRIPTION, "Declarative Services Configuration Support Listener" );
props.put( Constants.SERVICE_VENDOR, "The Apache Software Foundation" );
// If RegionConfigurationSupport *directly* implements ConfigurationListener then we get NoClassDefFoundError
// when SCR is started without a wiring to an exporter of Config Admin API. This construction allows the
// class loading exception to be caught and confined.
final ConfigurationListener serviceDelegator;
if ( System.getSecurityManager() != null ) {
serviceDelegator = new ConfigurationListener()
{
@Override
public void configurationEvent(final ConfigurationEvent event)
{
AccessController.doPrivileged(
new PrivilegedAction<Object>()
{
@Override
public Void run()
{
RegionConfigurationSupport.this.configurationEvent(event);
return null;
}
});
}
};
}
else
{
serviceDelegator = new ConfigurationListener()
{
@Override
public void configurationEvent(final ConfigurationEvent event)
{
RegionConfigurationSupport.this.configurationEvent(event);
}
};
}
this.m_registration = caBundleContext.registerService(ConfigurationListener.class, serviceDelegator, props );
}
public Long getBundleId()
{
return bundleId;
}
public boolean reference()
{
if ( referenceCount.get() == 0 )
{
return false;
}
referenceCount.incrementAndGet();
return true;
}
public boolean dereference()
{
if ( referenceCount.decrementAndGet() == 0 )
{
this.m_registration.unregister();
this.m_registration = null;
return true;
}
return false;
}
/**
* The return value is only relevant for the call from {@link #configurationEvent(ConfigurationEvent)}
* in the case of a deleted configuration which is not a factory configuration!
*/
public boolean configureComponentHolder(final ComponentHolder<?> holder)
{
// 112.7 configure unless configuration not required
if ( !holder.getComponentMetadata().isConfigurationIgnored() )
{
final BundleContext bundleContext = holder.getActivator().getBundleContext();
if ( bundleContext == null )
{
return false;// bundle was stopped concurrently with configuration deletion
}
final List<String> confPids = holder.getComponentMetadata().getConfigurationPid();
final ConfigurationAdmin ca = getConfigAdmin( bundleContext );
try
{
for ( final String confPid : confPids )
{
final Collection<Configuration> factory = findFactoryConfigurations( ca, confPid,
bundleContext.getBundle() );
if ( !factory.isEmpty() )
{
boolean created = false;
for (Configuration config : factory)
{
try
{
logger.log(LogService.LOG_DEBUG,
"Configuring holder {0} with change count {1}", null,
holder, config.getChangeCount());
if (checkBundleLocation(config,
bundleContext.getBundle()))
{
long changeCount = config.getChangeCount();
ServiceReference<ManagedService> ref = getManagedServiceReference(
bundleContext);
created |= holder.configurationUpdated(
new TargetedPID(config.getPid()),
new TargetedPID(config.getFactoryPid()),
config.getProcessedProperties(ref), changeCount);
}
}
catch (IllegalStateException e)
{
continue;
}
}
if ( !created )
{
return false;
}
}
else
{
// check for configuration and configure the holder
Configuration singleton = findSingletonConfiguration( ca, confPid, bundleContext.getBundle() );
if (singleton != null)
{
try
{
logger.log(LogService.LOG_DEBUG,
"Configuring holder {0} with change count {1}", null,
holder, singleton.getChangeCount());
if (singleton != null && checkBundleLocation(singleton,
bundleContext.getBundle()))
{
long changeCount = singleton.getChangeCount();
ServiceReference<ManagedService> ref = getManagedServiceReference(
bundleContext);
holder.configurationUpdated(
new TargetedPID(singleton.getPid()), null,
singleton.getProcessedProperties(ref),
changeCount);
}
else
{
return false;
}
}
catch (IllegalStateException e)
{
return false;
}
}
else
{
return false;
}
}
}
return !confPids.isEmpty();
}
finally
{
try
{
bundleContext.ungetService( caReference );
}
catch ( IllegalStateException e )
{
// ignore, bundle context was shut down during the above.
}
}
}
return false;
}
// ---------- ConfigurationListener
/**
* Called by the Configuration Admin service if a configuration is updated
* or removed.
* <p>
* This method is really only called upon configuration changes; it is not
* called for existing configurations upon startup of the Configuration
* Admin service. To bridge this gap, the
* {@link ComponentRegistry#serviceChanged(org.osgi.framework.ServiceEvent)} method called when the
* Configuration Admin service is registered calls #configureComponentHolders which calls this method for all
* existing configurations to be able to forward existing configurations to
* components.
*
* @param event The configuration change event
*/
public void configurationEvent(ConfigurationEvent event)
{
final TargetedPID pid = new TargetedPID( event.getPid() );
String rawFactoryPid = event.getFactoryPid();
final TargetedPID factoryPid = rawFactoryPid == null? null: new TargetedPID( rawFactoryPid );
// iterate over all components which must be configured with this pid
// (since DS 1.2, components may specify a specific configuration PID (112.4.4 configuration-pid)
Collection<ComponentHolder<?>> holders = getComponentHolders( factoryPid != null? factoryPid: pid );
logger.log( LogService.LOG_DEBUG,
"configurationEvent: Handling {0} of Configuration PID={1} for component holders {2}", null,
getEventType( event ), pid, holders );
for ( ComponentHolder<?> componentHolder : holders )
{
if ( !componentHolder.getComponentMetadata().isConfigurationIgnored() )
{
switch (event.getType())
{
case ConfigurationEvent.CM_DELETED:
if ( factoryPid != null || !configureComponentHolder( componentHolder ) )
{
componentHolder.configurationDeleted( pid, factoryPid );
}
break;
case ConfigurationEvent.CM_UPDATED:
{
final ComponentActivator activator = componentHolder.getActivator();
if ( activator == null )
{
break;
}
final BundleContext bundleContext = activator.getBundleContext();
if ( bundleContext == null )
{
break;
}
TargetedPID targetedPid = factoryPid == null? pid: factoryPid;
TargetedPID oldTargetedPID = componentHolder.getConfigurationTargetedPID( pid, factoryPid );
if ( factoryPid != null || targetedPid.equals( oldTargetedPID )
|| targetedPid.bindsStronger( oldTargetedPID ) )
{
final ConfigurationInfo configInfo = getConfigurationInfo( pid, targetedPid,
componentHolder, bundleContext );
if ( configInfo != null )
{
if ( checkBundleLocation( configInfo.getBundleLocation(), bundleContext.getBundle() ) )
{
// The below seems to be unnecessary - and if put in, the behaviour is not spec compliant anymore:
// if a component has a required configuration and a modified method, the component must not be
// reactivated
// If this is replacing a weaker targetedPID delete the old one.
// if ( factoryPid == null && !targetedPid.equals(oldTargetedPID) && oldTargetedPID != null)
//{
//componentHolder.configurationDeleted( pid, factoryPid );
//}
componentHolder.configurationUpdated( pid, factoryPid, configInfo.getProps(),
configInfo.getChangeCount() );
}
}
}
break;
}
case ConfigurationEvent.CM_LOCATION_CHANGED:
{
//TODO is this logic correct for factory pids????
final ComponentActivator activator = componentHolder.getActivator();
if ( activator == null )
{
break;
}
final BundleContext bundleContext = activator.getBundleContext();
if ( bundleContext == null )
{
break;
}
TargetedPID targetedPid = factoryPid == null? pid: factoryPid;
TargetedPID oldTargetedPID = componentHolder.getConfigurationTargetedPID( pid, factoryPid );
if ( targetedPid.equals( oldTargetedPID ) )
{
//this sets the location to this component's bundle if not already set. OK here
//since it used to be set to this bundle, ok to reset it
final ConfigurationInfo configInfo = getConfigurationInfo( pid, targetedPid,
componentHolder, bundleContext );
if ( configInfo != null )
{
logger.log( LogService.LOG_DEBUG,
"LocationChanged event, same targetedPID {0}, location now {1}, change count {2}", null,
targetedPid, configInfo.getBundleLocation(),
configInfo.getChangeCount() );
if ( configInfo.getProps() == null )
{
throw new IllegalStateException( "Existing Configuration with pid " + pid
+ " has had its properties set to null and location changed. We expected a delete event first." );
}
//this config was used on this component. Does it still match?
if ( !checkBundleLocation( configInfo.getBundleLocation(), bundleContext.getBundle() ) )
{
//no, delete it
componentHolder.configurationDeleted( pid, factoryPid );
//maybe there's another match
configureComponentHolder( componentHolder );
}
//else still matches
}
break;
}
boolean better = targetedPid.bindsStronger( oldTargetedPID );
if ( better )
{
//this sets the location to this component's bundle if not already set. OK here
//because if it is set to this bundle we will use it.
final ConfigurationInfo configInfo = getConfigurationInfo( pid, targetedPid,
componentHolder, bundleContext );
if ( configInfo != null )
{
logger.log( LogService.LOG_DEBUG,
"LocationChanged event, better targetedPID {0} compared to {1}, location now {2}, change count {3}", null,
targetedPid, oldTargetedPID, configInfo.getBundleLocation(),
configInfo.getChangeCount());
if ( configInfo.getProps() == null )
{
//location has been changed before any properties are set. We don't care. Wait for an updated event with the properties
break;
}
//this component was not configured with this config. Should it be now?
if ( checkBundleLocation( configInfo.getBundleLocation(), bundleContext.getBundle() ) )
{
if ( oldTargetedPID != null )
{
//this is a better match, delete old before setting new
componentHolder.configurationDeleted( pid, factoryPid );
}
componentHolder.configurationUpdated( pid, factoryPid, configInfo.getProps(),
configInfo.getChangeCount() );
}
}
}
//else worse match, do nothing
else
{
logger.log( LogService.LOG_DEBUG,
"LocationChanged event, worse targetedPID {0} compared to {1}, do nothing", null,
targetedPid, oldTargetedPID );
}
break;
}
default:
logger.log( LogService.LOG_WARNING, "Unknown ConfigurationEvent type {0}", null,
event.getType() );
}
}
}
}
protected abstract Collection<ComponentHolder<?>> getComponentHolders(TargetedPID pid);
private String getEventType(ConfigurationEvent event)
{
switch (event.getType())
{
case ConfigurationEvent.CM_UPDATED:
return "UPDATED";
case ConfigurationEvent.CM_DELETED:
return "DELETED";
case ConfigurationEvent.CM_LOCATION_CHANGED:
return "LOCATION CHANGED";
default:
return "Unkown event type: " + event.getType();
}
}
private static class ConfigurationInfo
{
private final Dictionary<String, Object> props;
private final String bundleLocation;
private final long changeCount;
public ConfigurationInfo(Dictionary<String, Object> props, String bundleLocation, long changeCount)
{
this.props = props;
this.bundleLocation = bundleLocation;
this.changeCount = changeCount;
}
public long getChangeCount()
{
return changeCount;
}
public Dictionary<String, Object> getProps()
{
return props;
}
public String getBundleLocation()
{
return bundleLocation;
}
}
/**
* This gets config admin, gets the requested configuration, extracts the info we need from it, and ungets config admin.
* Some versions of felix config admin do not allow access to configurations after the config admin instance they were obtained from
* are ungot. Extracting the info we need into "configInfo" solves this problem.
*
* @param pid TargetedPID for the desired configuration
* @param targetedPid the targeted factory pid for a factory configuration or the pid for a singleton configuration
* @param componentHolder ComponentHolder that holds the old change count (for r4 fake change counting)
* @param bundleContext BundleContext to get the CA from
* @return ConfigurationInfo object containing the info we need from the configuration.
*/
private ConfigurationInfo getConfigurationInfo(final TargetedPID pid, TargetedPID targetedPid,
ComponentHolder<?> componentHolder, final BundleContext bundleContext)
{
try
{
final ConfigurationAdmin ca = getConfigAdmin( bundleContext );
try
{
Configuration[] configs = ca.listConfigurations( filter( pid.getRawPid() ) );
if ( configs != null && configs.length > 0 )
{
for (Configuration config : configs)
{
try
{
ServiceReference<ManagedService> ref = getManagedServiceReference(
bundleContext);
return new ConfigurationInfo(
config.getProcessedProperties(ref),
config.getBundleLocation(), config.getChangeCount());
}
catch (IllegalStateException e)
{
continue;
}
}
}
}
catch ( IOException e )
{
logger.log( LogService.LOG_WARNING, "Failed reading configuration for pid={0}", e, pid);
}
catch ( InvalidSyntaxException e )
{
logger.log( LogService.LOG_WARNING, "Failed reading configuration for pid={0}", e, pid);
}
finally
{
bundleContext.ungetService( caReference );
}
}
catch ( IllegalStateException ise )
{
// If the bundle has been stopped concurrently
logger.log( LogService.LOG_WARNING, "Bundle in unexpected state", ise );
}
return null;
}
private ServiceReference<ManagedService> getManagedServiceReference(BundleContext bundleContext)
{
try {
Collection<ServiceReference<ManagedService>> refs = bundleContext.getServiceReferences(ManagedService.class,
"(&(service.bundleid=" + String.valueOf(bundleContext.getBundle().getBundleId()) + ")(!(service.pid=*)))");
if ( !refs.isEmpty() ) {
return refs.iterator().next();
}
} catch (InvalidSyntaxException e) {
// this should never happen,
}
return bundleContext.registerService(ManagedService.class, new ManagedService() {
@Override
public void updated(Dictionary<String, ?> properties) throws ConfigurationException {
// nothing to do
}
}, null).getReference();
}
private String filter(String rawPid)
{
return "(service.pid=" + rawPid + ")";
}
/**
* Returns the configuration whose PID equals the given pid. If no such
* configuration exists, <code>null</code> is returned.
*
* @param ca Configuration Admin service
* @param pid Pid for desired configuration
* @param bundle bundle of the component we are configuring (used in targeted pids)
* @return configuration with the specified Pid
*/
public Configuration findSingletonConfiguration(final ConfigurationAdmin ca, final String pid, Bundle bundle)
{
final String filter = getTargetedPidFilter( pid, bundle, Constants.SERVICE_PID );
final Configuration[] cfg = findConfigurations( ca, filter );
if ( cfg == null )
{
return null;
}
String longest = null;
Configuration best = null;
for ( Configuration config : cfg )
{
if (checkBundleLocation(config, bundle))
{
try
{
String testPid = config.getPid();
if (longest == null || testPid.length() > longest.length())
{
longest = testPid;
best = config;
}
}
catch (IllegalStateException e)
{
continue;
}
}
}
return best;
}
/**
* Returns all configurations whose factory PID equals the given factory PID
* or <code>null</code> if no such configurations exist
*
* @param ca ConfigurationAdmin service
* @param factoryPid factory Pid we want the configurations for
* @param bundle bundle we're working for (for location and location permission)
* @return the configurations specifying the supplied factory Pid.
*/
private Collection<Configuration> findFactoryConfigurations(final ConfigurationAdmin ca, final String factoryPid,
Bundle bundle)
{
final String filter = getTargetedPidFilter( factoryPid, bundle, ConfigurationAdmin.SERVICE_FACTORYPID );
Configuration[] configs = findConfigurations( ca, filter );
if ( configs == null )
{
return Collections.emptyList();
}
Map<String, Configuration> configsByPid = new HashMap<>();
for ( Configuration config : configs )
{
if (checkBundleLocation(config, bundle))
{
try
{
Configuration oldConfig = configsByPid.get(config.getPid());
if (oldConfig == null)
{
configsByPid.put(config.getPid(), config);
}
else
{
String newPid = config.getFactoryPid();
String oldPid = oldConfig.getFactoryPid();
if (newPid.length() > oldPid.length())
{
configsByPid.put(config.getPid(), config);
}
}
}
catch (IllegalStateException e)
{
continue;
}
}
}
return configsByPid.values();
}
private boolean checkBundleLocation(Configuration config, Bundle bundle)
{
if ( config == null )
{
return false;
}
String configBundleLocation = null;
try
{
configBundleLocation = config.getBundleLocation();
}
catch (IllegalStateException e)
{
return false;
}
return checkBundleLocation( configBundleLocation, bundle );
}
private boolean checkBundleLocation(String configBundleLocation, Bundle bundle)
{
boolean result;
if ( configBundleLocation == null )
{
result = true;
}
else if ( configBundleLocation.startsWith( "?" ) )
{
//multilocation
if ( System.getSecurityManager() != null )
{
result = bundle.hasPermission(
new ConfigurationPermission(configBundleLocation, ConfigurationPermission.TARGET));
}
else
{
result = true;
}
}
else
{
result = configBundleLocation.equals( bundle.getLocation() );
}
logger.log( LogService.LOG_DEBUG, "checkBundleLocation: location {0}, returning {1}", null,
configBundleLocation, result );
return result;
}
private Configuration[] findConfigurations(final ConfigurationAdmin ca, final String filter)
{
try
{
return ca.listConfigurations( filter );
}
catch ( IOException ioe )
{
logger.log( LogService.LOG_WARNING, "Problem listing configurations for filter={0}", ioe,
filter );
}
catch ( InvalidSyntaxException ise )
{
logger.log( LogService.LOG_ERROR, "Invalid Configuration selection filter {0}", ise, filter);
}
// no factories in case of problems
return null;
}
private String getTargetedPidFilter(String pid, Bundle bundle, String key)
{
String bsn = bundle.getSymbolicName();
String version = bundle.getVersion().toString();
String location = escape( bundle.getLocation() );
StringBuilder sb = new StringBuilder();
sb.append("(|(");
sb.append(key);
sb.append('=');
sb.append(pid);
sb.append(")(");
sb.append(key);
sb.append('=');
sb.append(pid);
sb.append('|');
sb.append(bsn);
sb.append(")(");
sb.append(key);
sb.append('=');
sb.append(pid);
sb.append('|');
sb.append(bsn);
sb.append('|');
sb.append(version);
sb.append(")(");
sb.append(key);
sb.append('=');
sb.append(pid);
sb.append('|');
sb.append(bsn);
sb.append('|');
sb.append(version);
sb.append('|');
sb.append(location);
sb.append("))");
return sb.toString();
}
/**
* see core spec 3.2.7. Escape \*() with preceding \
* @param value
* @return escaped string
*/
static final String escape(String value)
{
StringBuilder sb = null;
int index = 0;
for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
switch (c) {
case '\\':
case '*':
case '(':
case ')':
if (sb == null) {
sb = new StringBuilder();
}
sb.append(value, index, i);
sb.append('\\');
sb.append(c);
index = i + 1;
break;
}
}
if (sb == null) {
return value;
}
if (index < value.length()) {
sb.append(value, index, value.length());
}
return sb.toString();
}
private ConfigurationAdmin getConfigAdmin(BundleContext bundleContext)
{
return bundleContext.getService( caReference );
}
}