blob: 1122d441d4b6cd10e50014abe65bee352504ca53 [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.dm.impl;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Dictionary;
import java.util.Properties;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;
import org.apache.felix.dm.Component;
import org.apache.felix.dm.ConfigurationDependency;
import org.apache.felix.dm.Logger;
import org.apache.felix.dm.PropertyMetaData;
import org.apache.felix.dm.context.AbstractDependency;
import org.apache.felix.dm.context.DependencyContext;
import org.apache.felix.dm.context.Event;
import org.apache.felix.dm.context.EventType;
import org.apache.felix.dm.impl.metatype.MetaTypeProviderImpl;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
/**
* Implementation for a configuration dependency.
*
* @author <a href="mailto:dev@felix.apache.org">Felix Project Team</a>
*/
public class ConfigurationDependencyImpl extends AbstractDependency<ConfigurationDependency> implements ConfigurationDependency, ManagedService {
private Dictionary<String, Object> m_settings;
private String m_pid;
private ServiceRegistration m_registration;
private volatile Class<?> m_configType;
private MetaTypeProviderImpl m_metaType;
private final AtomicBoolean m_updateInvokedCache = new AtomicBoolean();
private final Logger m_logger;
private final BundleContext m_context;
private boolean m_needsInstance = true;
private final static int UPDATE_MAXWAIT = 30000; // max time to wait until a component has handled a configuration change event.
public ConfigurationDependencyImpl() {
this(null, null);
}
public ConfigurationDependencyImpl(BundleContext context, Logger logger) {
m_context = context;
m_logger = logger;
setRequired(true);
setCallback("updated");
}
public ConfigurationDependencyImpl(ConfigurationDependencyImpl prototype) {
super(prototype);
m_context = prototype.m_context;
m_pid = prototype.m_pid;
m_logger = prototype.m_logger;
m_metaType = prototype.m_metaType != null ? new MetaTypeProviderImpl(prototype.m_metaType, this, null) : null;
m_needsInstance = prototype.needsInstance();
m_configType = prototype.m_configType;
}
@Override
public Class<?> getAutoConfigType() {
return null; // we don't support auto config mode.
}
@Override
public DependencyContext createCopy() {
return new ConfigurationDependencyImpl(this);
}
/**
* Sets a callback method invoked on the instantiated component.
*/
public ConfigurationDependencyImpl setCallback(String callback) {
super.setCallbacks(callback, null);
return this;
}
/**
* Sets a callback method on an external callback instance object.
* The component is not yet instantiated at the time the callback is invoked.
* We check if callback instance is null, in this case, the callback will be invoked on the instantiated component.
*/
public ConfigurationDependencyImpl setCallback(Object instance, String callback) {
boolean needsInstantiatedComponent = (instance == null);
return setCallback(instance, callback, needsInstantiatedComponent);
}
/**
* Sets a callback method on an external callback instance object.
* If needsInstance == true, the component is instantiated at the time the callback is invoked.
* We check if callback instance is null, in this case, the callback will be invoked on the instantiated component.
*/
public ConfigurationDependencyImpl setCallback(Object instance, String callback, boolean needsInstance) {
super.setCallbacks(instance, callback, null);
m_needsInstance = needsInstance;
return this;
}
/**
* Sets a type-safe callback method invoked on the instantiated component.
*/
public ConfigurationDependency setCallback(String callback, Class<?> configType) {
setCallback(callback);
m_configType = configType;
return this;
}
/**
* Sets a type-safe callback method on an external callback instance object.
* The component is not yet instantiated at the time the callback is invoked.
*/
public ConfigurationDependency setCallback(Object instance, String callback, Class<?> configType) {
setCallback(instance, callback);
m_configType = configType;
return this;
}
/**
* Sets a type-safe callback method on an external callback instance object.
* If needsInstance == true, the component is instantiated at the time the callback is invoked.
*/
public ConfigurationDependencyImpl setCallback(Object instance, String callback, Class<?> configType, boolean needsInstance) {
setCallback(instance, callback, needsInstance);
m_configType = configType;
return this;
}
/**
* This method indicates to ComponentImpl if the component must be instantiated when this Dependency is started.
* If the callback has to be invoked on the component instance, then the component
* instance must be instantiated at the time the Dependency is started because when "CM" calls ConfigurationDependencyImpl.updated()
* callback, then at this point we have to synchronously delegate the callback to the component instance, and re-throw to CM
* any exceptions (if any) thrown by the component instance updated callback.
*/
@Override
public boolean needsInstance() {
return m_needsInstance;
}
@Override
public void start() {
BundleContext context = m_component.getBundleContext();
if (context != null) { // If null, we are in a test environment
Properties props = new Properties();
props.put(Constants.SERVICE_PID, m_pid);
ManagedService ms = this;
if (m_metaType != null) {
ms = m_metaType;
}
m_registration = context.registerService(ManagedService.class.getName(), ms, props);
}
super.start();
}
@Override
public void stop() {
if (m_registration != null) {
try {
m_registration.unregister();
} catch (IllegalStateException e) {}
m_registration = null;
}
super.stop();
}
public ConfigurationDependency setPid(String pid) {
ensureNotActive();
m_pid = pid;
return this;
}
@Override
public String getSimpleName() {
return m_pid;
}
@Override
public String getFilter() {
return null;
}
public String getType() {
return "configuration";
}
public ConfigurationDependency add(PropertyMetaData properties)
{
createMetaTypeImpl();
m_metaType.add(properties);
return this;
}
public ConfigurationDependency setDescription(String description)
{
createMetaTypeImpl();
m_metaType.setDescription(description);
return this;
}
public ConfigurationDependency setHeading(String heading)
{
createMetaTypeImpl();
m_metaType.setName(heading);
return this;
}
public ConfigurationDependency setLocalization(String path)
{
createMetaTypeImpl();
m_metaType.setLocalization(path);
return this;
}
@SuppressWarnings("unchecked")
@Override
public Dictionary<String, Object> getProperties() {
if (m_settings == null) {
throw new IllegalStateException("cannot find configuration");
}
return m_settings;
}
@SuppressWarnings({"unchecked", "rawtypes"})
@Override
public void updated(final Dictionary settings) throws ConfigurationException {
m_updateInvokedCache.set(false);
Dictionary<String, Object> oldSettings = null;
synchronized (this) {
oldSettings = m_settings;
}
if (oldSettings == null && settings == null) {
// CM has started but our configuration is not still present in the CM database: ignore
return;
}
// If this is initial settings, or a configuration update, we handle it synchronously.
// We'll conclude that the dependency is available only if invoking updated did not cause
// any ConfigurationException.
// However, we still want to schedule the event in the component executor, to make sure that the
// callback is invoked safely. So, we use a Callable and a FutureTask that allows to handle the
// configuration update through the component executor. We still wait for the result because
// in case of any configuration error, we have to return it from the current thread.
Callable<ConfigurationException> result = new Callable<ConfigurationException>() {
@Override
public ConfigurationException call() throws Exception {
try {
invokeUpdated(settings); // either the callback instance or the component instances, if available.
} catch (ConfigurationException e) {
return e;
}
return null;
}
};
// Schedule the configuration update in the component executor. In Normal case, the task is immediately executed.
// But in a highly concurrent system, and if the component is being reconfigured, the component may be currently busy
// (handling a service dependency event for example), so the task will be enqueued in the component executor, and
// we'll wait for the task execution by using a FutureTask:
FutureTask<ConfigurationException> ft = new FutureTask<>(result);
m_component.getExecutor().execute(ft);
try {
ConfigurationException confError = ft.get(UPDATE_MAXWAIT, TimeUnit.MILLISECONDS);
if (confError != null) {
throw confError; // will be logged by the Configuration Admin service;
}
}
catch (ExecutionException error) {
throw new ConfigurationException(null, "Configuration update error, unexpected exception.", error);
} catch (InterruptedException error) {
// will be logged by the Configuration Admin service;
throw new ConfigurationException(null, "Configuration update interrupted.", error);
} catch (TimeoutException error) {
// will be logged by the Configuration Admin service;
throw new ConfigurationException(null, "Component did not handle configuration update timely.", error);
}
// At this point, we have accepted the configuration.
synchronized (this) {
m_settings = settings;
}
if ((oldSettings == null) && (settings != null)) {
// Notify the component that our dependency is available.
m_component.handleEvent(this, EventType.ADDED, new ConfigurationEventImpl(m_pid, settings));
}
else if ((oldSettings != null) && (settings != null)) {
// Notify the component that our dependency has changed.
m_component.handleEvent(this, EventType.CHANGED, new ConfigurationEventImpl(m_pid, settings));
}
else if ((oldSettings != null) && (settings == null)) {
// Notify the component that our dependency has been removed.
// Notice that the component will be stopped, and then all required dependencies will be unbound
// (including our configuration dependency).
m_component.handleEvent(this, EventType.REMOVED, new ConfigurationEventImpl(m_pid, oldSettings));
}
}
@Override
public void invokeCallback(EventType type, Event ... event) {
switch (type) {
case ADDED:
try {
invokeUpdated(m_settings);
} catch (ConfigurationException e) {
logConfigurationException(e);
}
break;
case CHANGED:
// We already did that synchronously, from our updated method
break;
case REMOVED:
// The state machine is stopping us. We have to invoke updated(null).
// Reset for the next time the state machine calls invokeCallback(ADDED)
m_updateInvokedCache.set(false);
break;
default:
break;
}
}
/**
* Creates the various signatures and arguments combinations used for the configuration-type style callbacks.
*
* @param service the service for which the callback should be applied;
* @param configType the configuration type to use (can be <code>null</code>);
* @param settings the actual configuration settings.
*/
static CallbackTypeDef createCallbackType(Logger logger, Component service, Class<?> configType, Dictionary<?, ?> settings) {
Class<?>[][] sigs = new Class[][] { { Dictionary.class }, { Component.class, Dictionary.class }, {} };
Object[][] args = new Object[][] { { settings }, { service, settings }, {} };
if (configType != null) {
try {
// if the configuration is null, it means we are losing it, and since we pass a null dictionary for other callback
// (that accepts a Dictionary), then we should have the same behavior and also pass a null conf proxy object when
// the configuration is lost.
Object configurable = settings != null ? Configurable.create(configType, settings) : null;
logger.debug("Using configuration-type injecting using %s as possible configType.", configType.getSimpleName());
sigs = new Class[][] { { Dictionary.class }, { Component.class, Dictionary.class }, { Component.class, configType }, { configType }, {} };
args = new Object[][] { { settings }, { service, settings }, { service, configurable }, { configurable }, {} };
}
catch (Exception e) {
// This is not something we can recover from, use the defaults above...
logger.warn("Failed to create configurable for configuration type %s!", e, configType);
}
}
return new CallbackTypeDef(sigs, args);
}
private void invokeUpdated(Dictionary<?, ?> settings) throws ConfigurationException {
if (m_updateInvokedCache.compareAndSet(false, true)) {
// FELIX-5155: if component impl is an internal DM adapter, we must not invoke the callback on it
// because in case there is an external callback instance specified for the configuration callback,
// then we don't want to invoke it now. The external callback instance will be invoked
// on the other actual configuration dependency copied into the actual component instance created by the
// adapter.
Object mainComponentInstance = m_component.getInstance();
if (mainComponentInstance instanceof AbstractDecorator) {
return;
}
Object[] instances = super.getInstances(); // either the callback instance or the component instances
if (instances == null) {
return;
}
CallbackTypeDef callbackInfo = createCallbackType(m_logger, m_component, m_configType, settings);
boolean callbackFound = false;
for (int i = 0; i < instances.length; i++) {
try {
InvocationUtil.invokeCallbackMethod(instances[i], m_add, callbackInfo.m_sigs, callbackInfo.m_args);
callbackFound |= true;
}
catch (InvocationTargetException e) {
// The component has thrown an exception during it's callback invocation.
if (e.getTargetException() instanceof ConfigurationException) {
// the callback threw an OSGi ConfigurationException: just re-throw it.
throw (ConfigurationException) e.getTargetException();
}
else {
// wrap the callback exception into a ConfigurationException.
throw new ConfigurationException(null, "Configuration update failed", e.getTargetException());
}
}
catch (NoSuchMethodException e) {
// if the method does not exist, ignore it
}
catch (Throwable t) {
// wrap any other exception as a ConfigurationException.
throw new ConfigurationException(null, "Configuration update failed", t);
}
}
if (! callbackFound) {
String[] instanceClasses = Stream.of(instances).map(c -> c.getClass().getName()).toArray(String[]::new);
m_logger.log(Logger.LOG_ERROR, "\"" + m_add + "\" configuration callback not found in any of the component classes: " + Arrays.toString(instanceClasses));
}
}
}
private synchronized void createMetaTypeImpl() {
if (m_metaType == null) {
m_metaType = new MetaTypeProviderImpl(m_pid, m_context, m_logger, this, null);
}
}
private void logConfigurationException(ConfigurationException e) {
m_logger.log(Logger.LOG_ERROR, "Got exception while handling configuration update for pid " + m_pid, e);
}
}