blob: 98ec4c021844063ca2f6be3504f9924ea20a4080 [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 static org.apache.felix.dm.impl.ServiceUtil.toR6Dictionary;
import java.util.Arrays;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.Objects;
import java.util.Properties;
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;
import org.osgi.service.metatype.MetaTypeProvider;
/**
* 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 volatile Dictionary<String, Object> m_settings;
private volatile String m_pid;
private ServiceRegistration<?> m_registration;
private volatile Class<?>[] m_configTypes;
private volatile MetaTypeProviderImpl m_metaType;
private boolean m_mayInvokeUpdateCallback;
private final Logger m_logger;
private final BundleContext m_context;
private volatile boolean m_needsInstance = true;
private volatile boolean m_optional;
private volatile boolean m_needsInstanceCalled;
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_configTypes = prototype.m_configTypes;
}
@Override
public ConfigurationDependencyImpl setRequired(boolean required) {
m_optional = ! required;
super.setRequired(true); // always required
return this;
}
@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 = (m_needsInstanceCalled) ? m_needsInstance : (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);
needsInstance(needsInstance);
return this;
}
/**
* Sets a type-safe callback method invoked on the instantiated component.
*/
public ConfigurationDependency setCallback(String callback, Class<?> configType) {
Objects.nonNull(configType);
setCallback(callback);
m_configTypes = configType == null ? null : new Class<?>[] { configType };
m_pid = (m_pid == null) ? configType.getName() : m_pid;
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) {
Objects.nonNull(configType);
setCallback(instance, callback);
m_configTypes = configType == null ? null : new Class<?>[] { configType };
m_pid = (m_pid == null) ? configType.getName() : m_pid;
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_configTypes = configType == null ? null : new Class<?>[] { configType };
return this;
}
/**
* Specifies if the component instance must be started when this dependency is started. True by default.
*/
@Override
public ConfigurationDependencyImpl needsInstance(boolean needsInstance) {
m_needsInstance = needsInstance;
m_needsInstanceCalled = true;
return this;
}
@Override
public ConfigurationDependencyImpl setConfigType(Class<?> ... configTypes) {
m_configTypes = configTypes;
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;
props.put(MetaTypeProvider.METATYPE_PID, m_pid);
String[] ifaces = new String[] { ManagedService.class.getName(), MetaTypeProvider.class.getName() };
m_registration = context.registerService(ifaces, ms, toR6Dictionary(props));
} else {
m_registration = context.registerService(ManagedService.class.getName(), ms, toR6Dictionary(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("rawtypes")
@Override
public void updated(Dictionary settings) throws ConfigurationException {
// Handle the update in the component executor thread. Any exception thrown during the component updated callback will be
// synchronously awaited and re-thrown to the CM thread.
// We schedule the update in the component executor in order to avoid race conditions,
// like when the component is stopping while we receive a configuration update, or if the component restarts
// while the configuration is being updated, or if the getProperties method is invoked while we are losing the configuration ...
// there are many racy situations, and the safe way to handle them is to schedule the updated callback in the component executor.
InvocationUtil.invokeUpdated(m_component.getExecutor(), () -> doUpdated(settings));
}
@SuppressWarnings({"unchecked", "rawtypes"})
private void doUpdated(Dictionary settings) throws Exception {
// Reset the flag that tells if the callback can be invoked.
m_mayInvokeUpdateCallback = true;
Dictionary<String, Object> oldSettings = m_settings;
// FELIX-5192: we have to handle the following race condition: one thread stops a component (removes it from a DM object);
// another thread removes the configuration (from ConfigurationAdmin). in this case we may be called in our
// ManagedService.updated(null), but our component instance has been destroyed and does not exist anymore.
// In this case: do nothing.
if (! super.isStarted()) {
return;
}
if (settings == null && m_optional) {
// Provide a default empty configuration
settings = new Hashtable<>();
settings.put(Constants.SERVICE_PID, m_pid);
}
if (oldSettings == null && settings == null) {
// CM has started but our configuration is not still present in the CM database.
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.
invokeUpdated(settings);
// At this point, we have accepted the configuration.
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 {
// Won't invoke if we already invoked from the doUpdate method.
// The case when we really invoke may happen when the component is stopped , then restarted.
// At this point, we have to re-invoke the component updated callback.
invokeUpdated(((ConfigurationEventImpl) event[0]).getProperties());
} catch (Throwable err) {
logConfigurationException(err);
}
break;
case CHANGED:
// We already did that synchronously, from our doUpdated method
break;
case REMOVED:
// The state machine is stopping us. Reset for the next time the state machine calls invokeCallback(ADDED)
m_mayInvokeUpdateCallback = true;
break;
default:
break;
}
}
private static <T> T[] concat(T first, T[] second) {
T[] result = Arrays.copyOf(second, second.length + 1);
result[0] = first;
System.arraycopy(second, 0, result, 1, second.length);
return result;
}
private static <T> T[] concat(T first, T second, T[] third) {
T[] result = Arrays.copyOf(third, third.length + 2);
result[0] = first;
result[1] = second;
System.arraycopy(third, 0, result, 2, third.length);
return result;
}
static CallbackTypeDef createCallbackType(Logger logger, Component service, Class<?>[] configTypes, Dictionary<?, ?> settings) {
Class<?>[][] sigs = new Class[][] { { Dictionary.class }, { Component.class, Dictionary.class }, {} };
Object[][] args = new Object[][] { { settings }, { service, settings }, {} };
if (configTypes != null && configTypes.length > 0 && configTypes[0] != 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.
Dictionary<String, Object> declaredServiceProperties = ServiceUtil.toR6Dictionary(EMPTY_PROPERTIES);
if (service instanceof ComponentImpl) {
declaredServiceProperties = ((ComponentImpl) service).getDeclaredServiceProperties();
}
Object[] configurables = new Object[configTypes.length];
for (int i = 0 ; i < configTypes.length; i ++) {
configurables[i] = settings != null ? Configurable.create(configTypes[i], settings, declaredServiceProperties) : null;
logger.debug("Using configuration-type injecting using %s as possible configType.", configTypes[i].getSimpleName());
}
sigs = new Class[][] {
{ Dictionary.class },
{ Component.class, Dictionary.class },
concat(Component.class, configTypes),
configTypes ,
concat(Dictionary.class, configTypes),
concat(Component.class, Dictionary.class, configTypes),
{}
};
args = new Object[][] {
{ settings },
{ service, settings },
concat(service, configurables),
configurables,
concat(settings, configurables),
concat(service, settings, configurables),
{}
};
}
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, configTypes != null ? Arrays.toString(configTypes) : null);
}
}
return new CallbackTypeDef(sigs, args);
}
// Called from the configuration component internal queue.
private void invokeUpdated(Dictionary<?, ?> settings) throws Exception {
if (m_mayInvokeUpdateCallback) {
m_mayInvokeUpdateCallback = false;
// 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 || m_component.injectionDisabled()) {
return;
}
Object[] instances = super.getInstances(); // never null, either the callback instance or the component instances
CallbackTypeDef callbackInfo = createCallbackType(m_logger, m_component, m_configTypes, settings);
boolean callbackFound = false;
for (int i = 0; i < instances.length; i++) {
try {
// Only inject if the component instance is not a prototype instance
InvocationUtil.invokeCallbackMethod(instances[i], m_add, callbackInfo.m_sigs, callbackInfo.m_args);
callbackFound |= true;
}
catch (NoSuchMethodException e) {
// if the method does not exist, ignore it
}
}
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(Throwable err) {
m_logger.log(Logger.LOG_ERROR, "Got exception while handling configuration update for pid " + m_pid, err);
}
}