/** * 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.aries.blueprint.container; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Dictionary; import java.util.HashSet; import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import org.apache.aries.blueprint.BlueprintConstants; import org.apache.aries.blueprint.ComponentDefinitionRegistry; import org.apache.aries.blueprint.Interceptor; import org.apache.aries.blueprint.ServiceProcessor; import org.apache.aries.blueprint.container.AggregateConverter.Convertible; import org.apache.aries.blueprint.container.BeanRecipe.UnwrapperedBeanHolder; import org.apache.aries.blueprint.di.AbstractRecipe; import org.apache.aries.blueprint.di.CollectionRecipe; import org.apache.aries.blueprint.di.ExecutionContext; import org.apache.aries.blueprint.di.MapRecipe; import org.apache.aries.blueprint.di.Recipe; import org.apache.aries.blueprint.di.Repository; import org.apache.aries.blueprint.proxy.CollaboratorFactory; import org.apache.aries.blueprint.proxy.ProxyUtils; import org.apache.aries.blueprint.utils.JavaUtils; import org.apache.aries.blueprint.utils.ReflectionUtils; import org.apache.aries.blueprint.utils.ServiceListener; import org.apache.aries.blueprint.utils.ServiceUtil; import org.apache.aries.proxy.InvocationListener; import org.osgi.framework.Bundle; import org.osgi.framework.Constants; import org.osgi.framework.FrameworkUtil; import org.osgi.framework.ServiceFactory; import org.osgi.framework.ServiceReference; import org.osgi.framework.ServiceRegistration; import org.osgi.service.blueprint.container.ComponentDefinitionException; import org.osgi.service.blueprint.container.ReifiedType; import org.osgi.service.blueprint.reflect.ComponentMetadata; import org.osgi.service.blueprint.reflect.RefMetadata; import org.osgi.service.blueprint.reflect.ServiceMetadata; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A <code>Recipe</code> to export services into the OSGi registry. * * @version $Rev$, $Date$ */ public class ServiceRecipe extends AbstractRecipe { private static final Logger LOGGER = LoggerFactory.getLogger(ServiceRecipe.class); final static String LOG_ENTRY = "Method entry: {}, args {}"; final static String LOG_EXIT = "Method exit: {}, returning {}"; private final BlueprintContainerImpl blueprintContainer; private final ServiceMetadata metadata; private final Recipe serviceRecipe; private final CollectionRecipe listenersRecipe; private final MapRecipe propertiesRecipe; private final List<Recipe> explicitDependencies; private Map properties; private final AtomicReference<ServiceRegistration> registration = new AtomicReference<ServiceRegistration>(); private Map registrationProperties; private List<ServiceListener> listeners; private volatile Object service; private final Object monitor = new Object(); private final AtomicInteger activeCalls = new AtomicInteger(0); private volatile boolean quiesce; /** Only ever access when holding a lock on <code>monitor</code> */ private Collection<DestroyCallback> destroyCallbacks = new ArrayList<DestroyCallback>(); private boolean initialServiceRegistration = true; public ServiceRecipe(String name, BlueprintContainerImpl blueprintContainer, ServiceMetadata metadata, Recipe serviceRecipe, CollectionRecipe listenersRecipe, MapRecipe propertiesRecipe, List<Recipe> explicitDependencies) { super(name); this.prototype = false; this.blueprintContainer = blueprintContainer; this.metadata = metadata; this.serviceRecipe = serviceRecipe; this.listenersRecipe = listenersRecipe; this.propertiesRecipe = propertiesRecipe; this.explicitDependencies = explicitDependencies; } public Recipe getServiceRecipe() { return serviceRecipe; } public CollectionRecipe getListenersRecipe() { return listenersRecipe; } @Override public List<Recipe> getConstructorDependencies() { List<Recipe> recipes = new ArrayList<Recipe>(); if (explicitDependencies != null) { recipes.addAll(explicitDependencies); } return recipes; } public List<Recipe> getDependencies() { List<Recipe> recipes = new ArrayList<Recipe>(); if (serviceRecipe != null) { recipes.add(serviceRecipe); } if (listenersRecipe != null) { recipes.add(listenersRecipe); } if (propertiesRecipe != null) { recipes.add(propertiesRecipe); } recipes.addAll(getConstructorDependencies()); return recipes; } protected Object internalCreate() throws ComponentDefinitionException { ServiceRegistrationProxy proxy = new ServiceRegistrationProxy(); addPartialObject(proxy); internalGetService(null, null); // null bundle means we don't want to retrieve the actual service when used with a ServiceFactory return proxy; } public boolean isRegistered() { return registration.get() != null; } public void register() { int state = blueprintContainer.getBundleContext().getBundle().getState(); if (state != Bundle.ACTIVE && state != Bundle.STARTING) { return; } createExplicitDependencies(); Hashtable props = new Hashtable(); if (properties == null) { properties = (Map) createRecipe(propertiesRecipe); } props.putAll(properties); if (metadata.getRanking() == 0) { props.remove(Constants.SERVICE_RANKING); } else { props.put(Constants.SERVICE_RANKING, metadata.getRanking()); } String componentName = getComponentName(); if (componentName != null) { props.put(BlueprintConstants.COMPONENT_NAME_PROPERTY, componentName); } else { props.remove(BlueprintConstants.COMPONENT_NAME_PROPERTY); } for (ServiceProcessor processor : blueprintContainer.getProcessors(ServiceProcessor.class)) { processor.updateProperties(new PropertiesUpdater(), props); } registrationProperties = props; Set<String> classes = getClasses(); String[] classArray = classes.toArray(new String[classes.size()]); LOGGER.debug("Registering service {} with interfaces {} and properties {}", new Object[] { name, classes, props }); if (registration.get() == null) { ServiceRegistration reg = blueprintContainer.registerService(classArray, new TriggerServiceFactory(this, metadata), props); if (!registration.compareAndSet(null, reg) && registration.get() != reg) { ServiceUtil.safeUnregisterService(reg); } } initialServiceRegistration = false; } public void unregister() { ServiceRegistration reg = registration.get(); if (reg != null) { LOGGER.debug("Unregistering service {}", name); // This method needs to allow reentrance, so if we need to make sure the registration is // set to null before actually unregistering the service if (listeners != null) { LOGGER.debug("Calling listeners for service unregistration"); for (ServiceListener listener : listeners) { listener.unregister(service, registrationProperties); } } ServiceUtil.safeUnregisterService(reg); registration.compareAndSet(reg, null); } } protected ServiceReference getReference() { ServiceRegistration reg = registration.get(); if (reg == null) { throw new IllegalStateException("Service is not registered"); } else { return reg.getReference(); } } protected void setProperties(Dictionary props) { ServiceRegistration reg = registration.get(); if (reg == null) { throw new IllegalStateException("Service is not registered"); } else { reg.setProperties(props); // TODO: set serviceProperties? convert somehow? should listeners be notified of this? } } protected Object internalGetService() { return internalGetService(blueprintContainer.getBundleContext().getBundle(), null); } /** * Create the service object. * * @param bundle * @param registration * @return */ private Object internalGetService(Bundle bundle, ServiceRegistration registration) { LOGGER.debug("Retrieving service for bundle {} and service registration {}", bundle, registration); createService(); Object service = this.service; // We need the real service ... if (bundle != null) { if (service instanceof ServiceFactory) { service = ((ServiceFactory) service).getService(bundle, registration); } if (service == null) { throw new IllegalStateException("service is null"); } // Check if the service actually implement all the requested interfaces validateClasses(service); // We're not really interested in the service, but perform some sanity checks nonetheless } else { if (!(service instanceof ServiceFactory)) { // Check if the service actually implement all the requested interfaces validateClasses(service); } } return service; } private void createService() { try { if (service == null) { LOGGER.debug("Creating service instance"); //We can't use the BlueprintRepository because we don't know what interfaces //to use yet! We have to be a bit smarter. ExecutionContext old = ExecutionContext.Holder.setContext(blueprintContainer.getRepository()); try { Object o = serviceRecipe.create(); if (o instanceof Convertible) { o = blueprintContainer.getRepository().convert(o, new ReifiedType(Object.class)); validateClasses(o); } else if (o instanceof UnwrapperedBeanHolder) { UnwrapperedBeanHolder holder = (UnwrapperedBeanHolder) o; if (holder.unwrapperedBean instanceof ServiceFactory) { //If a service factory is used, make sure the proxy classes implement this //interface so that later on, internalGetService will create the real //service from it. LOGGER.debug("{} implements ServiceFactory, creating proxy that also implements this", holder.unwrapperedBean); Collection<Class<?>> cls = getClassesForProxying(holder.unwrapperedBean); cls.add(blueprintContainer.loadClass("org.osgi.framework.ServiceFactory")); o = BeanRecipe.wrap(holder, cls); } else { validateClasses(holder.unwrapperedBean); o = BeanRecipe.wrap(holder, getClassesForProxying(holder.unwrapperedBean)); } } else if (!(o instanceof ServiceFactory)) { validateClasses(o); } service = o; } catch (Exception e) { LOGGER.error("Error retrieving service from " + this, e); throw new ComponentDefinitionException(e); } finally { ExecutionContext.Holder.setContext(old); } LOGGER.debug("Service created: {}", service); } // When the service is first requested, we need to create listeners and call them if (!initialServiceRegistration && listeners == null) { LOGGER.debug("Creating listeners"); if (listenersRecipe != null) { listeners = (List) createRecipe(listenersRecipe); } else { listeners = Collections.emptyList(); } LOGGER.debug("Listeners created: {}", listeners); if (registration.get() != null) { LOGGER.debug("Calling listeners for initial service registration"); for (ServiceListener listener : listeners) { listener.register(service, registrationProperties); } } else { LOGGER.debug("Calling listeners for initial service unregistration"); for (ServiceListener listener : listeners) { listener.unregister(service, registrationProperties); } } } } catch (RuntimeException e) { LOGGER.error("Error retrieving service from " + this, e); throw e; } } private void validateClasses(Object service) { // Check if the service actually implement all the requested interfaces if (metadata.getAutoExport() == ServiceMetadata.AUTO_EXPORT_DISABLED) { Set<String> allClasses = new HashSet<String>(); ReflectionUtils.getSuperClasses(allClasses, service.getClass()); ReflectionUtils.getImplementedInterfaces(allClasses, service.getClass()); //This call is safe because we know that we don't need to call internalGet to get the answer Set<String> classes = getClasses(); classes.removeAll(allClasses); if (!classes.isEmpty()) { throw new ComponentDefinitionException("The service implementation does not implement the required interfaces: " + classes); } } } public Object getService(Bundle bundle, ServiceRegistration registration) { /** getService() can get called before registerService() returns with the registration object. * So we need to set the registration object in case registration listeners call * getServiceReference(). */ this.registration.compareAndSet(null, registration); return internalGetService(bundle, registration); } public void ungetService(Bundle bundle, ServiceRegistration registration, Object service) { if (this.service instanceof ServiceFactory) { ((ServiceFactory) this.service).ungetService(bundle, registration, service); } } /** * Be careful to avoid calling this method from internalGetService or createService before the service * field has been set. If you get this wrong you will get a StackOverflowError! * @return */ private Set<String> getClasses() { Set<String> classes; switch (metadata.getAutoExport()) { case ServiceMetadata.AUTO_EXPORT_INTERFACES: classes = ReflectionUtils.getImplementedInterfaces(new HashSet<String>(), internalGetService().getClass()); break; case ServiceMetadata.AUTO_EXPORT_CLASS_HIERARCHY: classes = ReflectionUtils.getSuperClasses(new HashSet<String>(), internalGetService().getClass()); break; case ServiceMetadata.AUTO_EXPORT_ALL_CLASSES: classes = ReflectionUtils.getSuperClasses(new HashSet<String>(), internalGetService().getClass()); classes = ReflectionUtils.getImplementedInterfaces(classes, internalGetService().getClass()); break; default: classes = new HashSet<String>(metadata.getInterfaces()); break; } return classes; } /** * Get the classes we need to proxy, for auto-export interfaces only, those * will be just the interfaces implemented by the bean, for auto-export classes * or everything, then just proxying the real bean class will give us everything we * need, if none of the above then we need the class forms of the interfaces in * the metadata * * Note that we use a template object here because we have already instantiated the bean * that we're going to proxy. We can't call internalGetService because it would Stack Overflow. * @return * @throws ClassNotFoundException */ private Collection<Class<?>> getClassesForProxying(Object template) throws ClassNotFoundException { Collection<Class<?>> classes; switch (metadata.getAutoExport()) { case ServiceMetadata.AUTO_EXPORT_INTERFACES: classes = ReflectionUtils.getImplementedInterfacesAsClasses(new HashSet<Class<?>>(), template.getClass()); break; case ServiceMetadata.AUTO_EXPORT_CLASS_HIERARCHY: case ServiceMetadata.AUTO_EXPORT_ALL_CLASSES: classes = ProxyUtils.asList(template.getClass()); break; default: classes = new HashSet<Class<?>>(convertStringsToClasses(metadata.getInterfaces())); break; } return classes; } private Collection<? extends Class<?>> convertStringsToClasses( List<String> interfaces) throws ClassNotFoundException { Set<Class<?>> classes = new HashSet<Class<?>>(); for(String s : interfaces) { classes.add(blueprintContainer.loadClass(s)); } return classes; } private void createExplicitDependencies() { if (explicitDependencies != null) { for (Recipe recipe : explicitDependencies) { createRecipe(recipe); } } } private Object createRecipe(Recipe recipe) { String name = recipe.getName(); Repository repo = blueprintContainer.getRepository(); if (repo.getRecipe(name) != recipe) { repo.putRecipe(name, recipe); } return repo.create(name); } private String getComponentName() { if (metadata.getServiceComponent() instanceof RefMetadata) { RefMetadata ref = (RefMetadata) metadata.getServiceComponent(); return ref.getComponentId(); } else { return null; } } protected void incrementActiveCalls() { // can be improved with LongAdder but Java 8 or backport (like guava) is needed activeCalls.incrementAndGet(); } protected void decrementActiveCalls() { int currentCount = activeCalls.decrementAndGet(); if (currentCount == 0 && quiesce) { List<DestroyCallback> callbacksToCall; synchronized (monitor) { callbacksToCall = new ArrayList<DestroyCallback>(destroyCallbacks); destroyCallbacks.clear(); } for(DestroyCallback cbk : callbacksToCall) { cbk.callback(); } } } /* The following problem is possible sometimes: some threads already got a service and start to call it but incrementActiveCalls have not called yet as a result activeCalls after service unregistration is not strongly decreasing and can be 0 but then not 0 */ public void quiesce(DestroyCallback destroyCallback) { unregister(); quiesce = true; DestroyCallback safeDestroyCallback = new DestroyOnceCallback(destroyCallback); synchronized (monitor) { destroyCallbacks.add(safeDestroyCallback); } if (activeCalls.get() == 0) { safeDestroyCallback.callback(); synchronized (monitor) { destroyCallbacks.remove(safeDestroyCallback); } } } private static class DestroyOnceCallback implements DestroyCallback { private final DestroyCallback destroyCallback; private final AtomicBoolean isDestroyed = new AtomicBoolean(false); public DestroyOnceCallback(DestroyCallback destroyCallback) { this.destroyCallback = destroyCallback; } @Override public void callback() { if (isDestroyed.compareAndSet(false, true)) { destroyCallback.callback(); } } } private class TriggerServiceFactory implements ServiceFactory { private ServiceRecipe serviceRecipe; private ComponentMetadata cm; private ServiceMetadata sm; private boolean isQuiesceAvailable; public TriggerServiceFactory(ServiceRecipe serviceRecipe, ServiceMetadata cm) { this.serviceRecipe = serviceRecipe; this.cm = cm; this.sm = cm; this.isQuiesceAvailable = isClassAvailable("org.apache.aries.quiesce.participant.QuiesceParticipant"); } public Object getService(Bundle bundle, ServiceRegistration registration) { Object original = ServiceRecipe.this.getService(bundle, registration); LOGGER.debug(LOG_ENTRY, "getService", original); List<Interceptor> interceptors = new ArrayList<Interceptor>(); ComponentDefinitionRegistry reg = blueprintContainer.getComponentDefinitionRegistry(); List<Interceptor> registeredInterceptors = reg.getInterceptors(cm); if (registeredInterceptors != null) { interceptors.addAll(registeredInterceptors); } // Add quiesce interceptor if needed if (isQuiesceAvailable) { interceptors.add(new QuiesceInterceptor(serviceRecipe)); } // Exit if no interceptors configured if (interceptors.isEmpty()) { return original; } Object intercepted; try { Bundle b = FrameworkUtil.getBundle(original.getClass()); if (b == null) { // we have a class from the framework parent, so use our bundle for proxying. b = blueprintContainer.getBundleContext().getBundle(); } InvocationListener collaborator = CollaboratorFactory.create(cm, interceptors); intercepted = blueprintContainer.getProxyManager().createInterceptingProxy(b, getClassesForProxying(original), original, collaborator); } catch (Exception u) { Bundle b = blueprintContainer.getBundleContext().getBundle(); LOGGER.info("Unable to create a proxy object for the service " + getName() + " defined in bundle " + b.getSymbolicName() + "/" + b.getVersion() + " with id. Returning the original object instead.", u); LOGGER.debug(LOG_EXIT, "getService", original); return original; } LOGGER.debug(LOG_EXIT, "getService", intercepted); return intercepted; } public void ungetService(Bundle bundle, ServiceRegistration registration, Object service) { ServiceRecipe.this.ungetService(bundle, registration, service); } } private class ServiceRegistrationProxy implements ServiceRegistration { public ServiceReference getReference() { return ServiceRecipe.this.getReference(); } public void setProperties(Dictionary properties) { ServiceRecipe.this.setProperties(properties); } public void unregister() { throw new UnsupportedOperationException(); } } private class PropertiesUpdater implements ServiceProcessor.ServicePropertiesUpdater { public String getId() { return metadata.getId(); } public void updateProperties(Dictionary properties) { Hashtable table = JavaUtils.getProperties(ServiceRecipe.this.getReference()); JavaUtils.copy(table, properties); ServiceRecipe.this.setProperties(table); } } private boolean isClassAvailable(String clazz) { try { getClass().getClassLoader().loadClass(clazz); return true; } catch (ClassNotFoundException e) { return false; } catch (NoClassDefFoundError e) { return false; } } }