| /* |
| * 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); |
| } |
| } |