/****************************************************************************** * Copyright (c) 2006, 2010 VMware Inc., Oracle Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * and Apache License v2.0 which accompanies this distribution. * The Eclipse Public License is available at * http://www.eclipse.org/legal/epl-v10.html and the Apache License v2.0 * is available at http://www.opensource.org/licenses/apache2.0.php. * You may elect to redistribute this code under either of these licenses. * * Contributors: * VMware Inc. * Oracle Inc. *****************************************************************************/ package org.eclipse.gemini.blueprint.service.exporter.support; import java.security.AccessControlContext; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.Arrays; import java.util.Dictionary; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.eclipse.gemini.blueprint.context.BundleContextAware; import org.eclipse.gemini.blueprint.context.support.internal.classloader.ClassLoaderFactory; import org.eclipse.gemini.blueprint.context.support.internal.scope.OsgiBundleScope; import org.eclipse.gemini.blueprint.context.support.internal.security.SecurityUtils; import org.eclipse.gemini.blueprint.service.exporter.OsgiServicePropertiesResolver; import org.eclipse.gemini.blueprint.service.exporter.support.internal.controller.ExporterController; import org.eclipse.gemini.blueprint.service.exporter.support.internal.controller.ExporterInternalActions; import org.eclipse.gemini.blueprint.service.exporter.support.internal.support.LazyTargetResolver; import org.eclipse.gemini.blueprint.service.exporter.support.internal.support.ListenerNotifier; import org.eclipse.gemini.blueprint.service.exporter.support.internal.support.PublishingServiceFactory; import org.eclipse.gemini.blueprint.service.exporter.support.internal.support.ServiceRegistrationDecorator; import org.eclipse.gemini.blueprint.service.exporter.support.internal.support.ServiceRegistrationWrapper; import org.eclipse.gemini.blueprint.util.OsgiServiceUtils; import org.eclipse.gemini.blueprint.util.internal.ClassUtils; import org.eclipse.gemini.blueprint.util.internal.MapBasedDictionary; import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; import org.osgi.framework.ServiceFactory; import org.osgi.framework.ServiceRegistration; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.core.Ordered; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** * FactoryBean that transparently publishes other beans in the same application context as OSGi services returning the * ServiceRegistration for the given object. Also known as an <em>exporter</em> this class handle the registration and * unregistration of an OSGi service for the backing/target object. * * <p/> The service properties used when publishing the service are determined by the OsgiServicePropertiesResolver. The * default implementation uses <ul> <li>BundleSymbolicName=<bundle symbolic name></li> * <li>BundleVersion=<bundle version></li> <li>org.eclipse.gemini.blueprint.bean.name="<bean name></li> </ul> * * <p/> <strong>Note:</strong>If thread context class loader management is used ( * {@link #setContextClassLoader(ExportContextClassLoader)}, since proxying is required, the target class has to meet * certain criterion described in the Spring AOP documentation. In short, final classes are not supported when class * enhancement is used. * * @author Adrian Colyer * @author Costin Leau * @author Hal Hildebrand * @author Andy Piper * */ public class OsgiServiceFactoryBean extends AbstractOsgiServiceExporter implements BeanClassLoaderAware, BeanFactoryAware, BeanNameAware, BundleContextAware, FactoryBean<ServiceRegistration>, InitializingBean, Ordered { /** * Wrapper around internal commands. * * @author Costin Leau * */ private class Executor implements ExporterInternalActions { public void registerService() { OsgiServiceFactoryBean.this.registerService(); } public void registerServiceAtStartup(boolean register) { synchronized (lock) { registerAtStartup = register; } } public void unregisterService() { OsgiServiceFactoryBean.this.unregisterService(); } public void callUnregisterOnStartup() { OsgiServiceFactoryBean.this.resolver.startupUnregisterIfPossible(); } }; /** * Callback that propagates the changes in the service properties to the registration object. * * @author Costin Leau */ private class PropertiesMonitor implements ServicePropertiesChangeListener { public void propertiesChange(ServicePropertiesChangeEvent event) { serviceProperties = event.getServiceProperties(); Dictionary dictionary = mergeServiceProperties(serviceProperties, beanName); if (serviceRegistration != null) { serviceRegistration.setProperties(dictionary); } } } private static final Log log = LogFactory.getLog(OsgiServiceFactoryBean.class); private volatile BundleContext bundleContext; private volatile OsgiServicePropertiesResolver propertiesResolver; private volatile BeanFactory beanFactory; private volatile ServiceRegistrationDecorator serviceRegistration; private final ServiceRegistrationWrapper safeServiceRegistration = new ServiceRegistrationWrapper(null); private volatile Map serviceProperties; private volatile ServicePropertiesChangeListener propertiesListener; private volatile int ranking; private volatile String targetBeanName; private boolean hasNamedBean; private volatile Class<?>[] interfaces; private InterfaceDetector interfaceDetector = DefaultInterfaceDetector.DISABLED; private volatile ExportContextClassLoaderEnum contextClassLoader = ExportContextClassLoaderEnum.UNMANAGED; private volatile Object target; private volatile Class<?> targetClass; /** Default value is same as non-ordered */ private int order = Ordered.LOWEST_PRECEDENCE; private ClassLoader classLoader; /** class loader used by the aop infrastructure */ private ClassLoader aopClassLoader; /** exporter bean name */ private String beanName; /** registration sanity flag */ private boolean serviceRegistered = false; /** register at startup by default */ private boolean registerAtStartup = true; /** register the service by default */ private boolean registerService = true; /** synchronization lock */ private final Object lock = new Object(); /** internal behaviour controller */ private final ExporterController controller; private volatile LazyTargetResolver resolver; private ListenerNotifier notifier; private final AtomicBoolean activated = new AtomicBoolean(false); /** should the service be cached or not */ private boolean cacheTarget = false; public OsgiServiceFactoryBean() { controller = new ExporterController(new Executor()); } public void afterPropertiesSet() throws Exception { Assert.notNull(bundleContext, "required property 'bundleContext' has not been set"); hasNamedBean = StringUtils.hasText(targetBeanName); Assert.isTrue(hasNamedBean || target != null, "Either 'targetBeanName' or 'target' properties have to be set."); // if we have a name, we need a bean factory if (hasNamedBean) { Assert.notNull(beanFactory, "Required property 'beanFactory' has not been set."); } // initialize bean only when dealing with singletons and named beans if (hasNamedBean) { Assert.isTrue(beanFactory.containsBean(targetBeanName), "Cannot locate bean named '" + targetBeanName + "' inside the running bean factory."); if (beanFactory.isSingleton(targetBeanName)) { if (beanFactory instanceof ConfigurableListableBeanFactory) { ConfigurableListableBeanFactory clbf = (ConfigurableListableBeanFactory) beanFactory; BeanDefinition definition = clbf.getBeanDefinition(targetBeanName); if (!definition.isLazyInit()) { target = beanFactory.getBean(targetBeanName); targetClass = target.getClass(); } } } if (targetClass == null) { // lazily get the target class targetClass = beanFactory.getType(targetBeanName); } // when running inside a container, add the dependency between this bean and the target one addBeanFactoryDependency(); } else { targetClass = target.getClass(); } if (propertiesResolver == null) { propertiesResolver = new BeanNameServicePropertiesResolver(); ((BeanNameServicePropertiesResolver) propertiesResolver).setBundleContext(bundleContext); } // sanity check if (interfaces == null) { if (AutoExport.DISABLED.equals(interfaceDetector) || DefaultInterfaceDetector.DISABLED.equals(interfaceDetector)) throw new IllegalArgumentException( "No service interface(s) specified and auto-export discovery disabled; change at least one of these properties."); interfaces = new Class[0]; } // check visibility type else { if (!ServiceFactory.class.isAssignableFrom(targetClass)) { for (int interfaceIndex = 0; interfaceIndex < interfaces.length; interfaceIndex++) { Class<?> intf = interfaces[interfaceIndex]; Assert.isAssignable(intf, targetClass, "Exported service object does not implement the given interface: "); } } } // check service properties listener if (serviceProperties instanceof ServicePropertiesListenerManager) { propertiesListener = new PropertiesMonitor(); ((ServicePropertiesListenerManager) serviceProperties).addListener(propertiesListener); } boolean shouldRegisterAtStartup; synchronized (lock) { shouldRegisterAtStartup = registerAtStartup; } resolver = new LazyTargetResolver(target, beanFactory, targetBeanName, cacheTarget, getNotifier(), getLazyListeners()); if (shouldRegisterAtStartup) { registerService(); } } public void destroy() { if (propertiesListener != null) { if (serviceProperties instanceof ServicePropertiesListenerManager) { ((ServicePropertiesListenerManager) serviceProperties).removeListener(propertiesListener); } propertiesListener = null; } super.destroy(); } private void addBeanFactoryDependency() { if (beanFactory instanceof ConfigurableBeanFactory) { ConfigurableBeanFactory cbf = (ConfigurableBeanFactory) beanFactory; if (StringUtils.hasText(beanName) && cbf.containsBean(beanName)) { // no need to validate targetBeanName (already did) cbf.registerDependentBean(targetBeanName, BeanFactory.FACTORY_BEAN_PREFIX + beanName); cbf.registerDependentBean(targetBeanName, beanName); } } else { log.warn("The running bean factory cannot support dependencies between beans " + "- importer/exporter dependency cannot be enforced"); } } private Dictionary mergeServiceProperties(Map serviceProperties, String beanName) { MapBasedDictionary props = new MapBasedDictionary(); // add service properties if (serviceProperties != null) props.putAll(serviceProperties); // eliminate any property that might clash with the official ones props.remove(OsgiServicePropertiesResolver.BEAN_NAME_PROPERTY_KEY); props.remove(OsgiServicePropertiesResolver.SPRING_DM_BEAN_NAME_PROPERTY_KEY); props.remove(OsgiServicePropertiesResolver.BLUEPRINT_COMP_NAME); props.remove(Constants.SERVICE_RANKING); // override any user property that might clash with the official ones props.putAll(propertiesResolver.getServiceProperties(beanName)); if (ranking != 0) { props.put(Constants.SERVICE_RANKING, Integer.valueOf(ranking)); } return props; } /** * Publishes the given object as an OSGi service. It simply assembles the classes required for publishing and then * delegates the actual registration to a dedicated method. */ @Override void registerService() { synchronized (lock) { if (serviceRegistered || !registerService) return; else serviceRegistered = true; } // if we have a nested bean / non-Spring managed object String beanName = (!hasNamedBean ? null : targetBeanName); Dictionary serviceProperties = mergeServiceProperties(this.serviceProperties, beanName); Class<?>[] intfs = interfaces; // filter classes based on visibility ClassLoader beanClassLoader = ClassUtils.getClassLoader(targetClass); Class<?>[] autoDetectedClasses = ClassUtils.getVisibleClasses(interfaceDetector.detect(targetClass), beanClassLoader); if (log.isTraceEnabled()) log.trace("Autoexport mode [" + interfaceDetector + "] discovered on class [" + targetClass + "] classes " + ObjectUtils.nullSafeToString(autoDetectedClasses)); // filter duplicates Set<Class<?>> classes = new LinkedHashSet<Class<?>>(intfs.length + autoDetectedClasses.length); CollectionUtils.mergeArrayIntoCollection(intfs, classes); CollectionUtils.mergeArrayIntoCollection(autoDetectedClasses, classes); Class<?>[] mergedClasses = (Class[]) classes.toArray(new Class[classes.size()]); ServiceRegistration reg = registerService(mergedClasses, serviceProperties); serviceRegistration = new ServiceRegistrationDecorator(reg); safeServiceRegistration.swap(serviceRegistration); resolver.setDecorator(serviceRegistration); resolver.notifyIfPossible(); } /** * Registration method. * * @param classes * @param serviceProperties * @return the ServiceRegistration */ ServiceRegistration registerService(Class<?>[] classes, final Dictionary serviceProperties) { Assert.notEmpty(classes, "at least one class has to be specified for exporting " + "(if autoExport is enabled then maybe the object doesn't implement any interface)"); // create an array of classnames (used for registering the service) final String[] names = ClassUtils.toStringArray(classes); // sort the names in alphabetical order (eases debugging) Arrays.sort(names); log.info("Publishing service under classes [" + ObjectUtils.nullSafeToString(names) + "]"); ServiceFactory serviceFactory = new PublishingServiceFactory(resolver, classes, (ExportContextClassLoaderEnum.SERVICE_PROVIDER .equals(contextClassLoader)), classLoader, aopClassLoader, bundleContext); if (isBeanBundleScoped()) serviceFactory = new OsgiBundleScope.BundleScopeServiceFactory(serviceFactory); if (System.getSecurityManager() != null) { AccessControlContext acc = SecurityUtils.getAccFrom(beanFactory); final ServiceFactory serviceFactoryFinal = serviceFactory; return AccessController.doPrivileged(new PrivilegedAction<ServiceRegistration>() { public ServiceRegistration run() { return bundleContext.registerService(names, serviceFactoryFinal, serviceProperties); } }, acc); } else { return bundleContext.registerService(names, serviceFactory, serviceProperties); } } private boolean isBeanBundleScoped() { boolean bundleScoped = false; // if we do have a bundle scope, use ServiceFactory decoration if (targetBeanName != null) { if (beanFactory instanceof ConfigurableListableBeanFactory) { String beanScope = ((ConfigurableListableBeanFactory) beanFactory).getMergedBeanDefinition(targetBeanName) .getScope(); bundleScoped = OsgiBundleScope.SCOPE_NAME.equals(beanScope); } else // if for some reason, the passed in BeanFactory can't be // queried for scopes and we do // have a bean reference, apply scoped decoration. bundleScoped = true; } return bundleScoped; } public void setBeanClassLoader(ClassLoader classLoader) { this.classLoader = classLoader; this.aopClassLoader = ClassLoaderFactory.getAopClassLoaderFor(classLoader); } /** * {@inheritDoc} * * <p/> Returns a {@link ServiceRegistration} to the OSGi service for the target object. */ public ServiceRegistration getObject() throws Exception { // activate resolver.activate(); return safeServiceRegistration; } public Class<? extends ServiceRegistration> getObjectType() { return ServiceRegistration.class; } public boolean isSingleton() { return true; } void unregisterService() { synchronized (lock) { if (!serviceRegistered) return; else serviceRegistered = false; } unregisterService(serviceRegistration); serviceRegistration = null; } /** * Unregisters (literally stops) a service. * * @param registration */ void unregisterService(ServiceRegistration registration) { if (OsgiServiceUtils.unregisterService(registration)) { log.info("Unregistered service [" + registration + "]"); if (resolver != null) resolver.setDecorator(null); } } /** * Sets the context class loader management strategy to use when invoking operations on the exposed target bean. By * default, {@link ExportContextClassLoader#UNMANAGED} is used. * * <p/> <strong>Note:</strong> Since proxying is required for context class loader manager, the target class has to * meet certain criteria described in the Spring AOP documentation. In short, final classes are not supported when * class enhancement is used. * * @param ccl context class loader strategy to use * @see ExportContextClassLoader * @deprecated As of Spring DM 2.0, replaced by {@link #setExportContextClassLoader(ExportContextClassLoaderEnum)} */ public void setContextClassLoader(ExportContextClassLoader ccl) { Assert.notNull(ccl); this.contextClassLoader = ccl.getExportContextClassLoaderEnum(); } /** * Sets the context class loader management strategy to use when invoking operations on the exposed target bean. By * default, {@link ExportContextClassLoader#UNMANAGED} is used. * * <p/> <strong>Note:</strong> Since proxying is required for context class loader manager, the target class has to * meet certain criteria described in the Spring AOP documentation. In short, final classes are not supported when * class enhancement is used. * * @param ccl context class loader strategy to use * @see ExportContextClassLoader */ public void setExportContextClassLoader(ExportContextClassLoaderEnum ccl) { Assert.notNull(ccl); this.contextClassLoader = ccl; } /** * Returns the object exported as an OSGi service. * * @return the object exported as an OSGi service */ public Object getTarget() { return target; } /** * Sets the given object to be export as an OSGi service. Normally used when the exported service is a nested bean * or an object not managed by the Spring container. Note that the passed target instance is ignored if * {@link #setTargetBeanName(String)} is used. * * @param target the object to be exported as an OSGi service */ public void setTarget(Object target) { this.target = target; } /** * Returns the target bean name. * * @return the target object bean name */ public String getTargetBeanName() { return targetBeanName; } /** * Sets the name of the bean managed by the Spring container, which will be exported as an OSGi service. This method * is normally what most use-cases need, rather then {@link #setTarget(Object)}. * * @param name target bean name */ public void setTargetBeanName(String name) { this.targetBeanName = name; } /** * Sets the strategy used for automatically publishing classes. This allows the exporter to use the target class * hierarchy and/or interfaces for registering the OSGi service. By default, autoExport is disabled * {@link AutoExport#DISABLED}. * * @param classExporter class exporter used for automatically publishing service classes. * * @see AutoExport * @deprecated */ public void setAutoExport(AutoExport classExporter) { Assert.notNull(classExporter); this.interfaceDetector = classExporter; } /** * Sets the strategy used for automatically publishing classes. This allows the exporter to use the target class * hierarchy and/or interfaces for registering the OSGi service. By default, autoExport is disabled * {@link DefaultInterfaceDetector#DISABLED}. * * @param detector */ public void setInterfaceDetector(InterfaceDetector detector) { Assert.notNull(detector); this.interfaceDetector = detector; } /** * Returns the properties used when exporting the target as an OSGi service. * * @return properties used for exporting the target */ public Map getServiceProperties() { return serviceProperties; } /** * Sets the properties used when exposing the target as an OSGi service. If the given argument implements ( * {@link ServicePropertiesChangeListener}), any updates to the properties will be reflected by the service * registration. * * @param serviceProperties properties used for exporting the target as an OSGi service */ public void setServiceProperties(Map serviceProperties) { this.serviceProperties = serviceProperties; } /** * Returns the OSGi ranking used when publishing the service. * * @return service ranking used when publishing the service */ public int getRanking() { return ranking; } /** * Shortcut for setting the ranking property of the published service. * * * @param ranking service ranking * @see Constants#SERVICE_RANKING */ public void setRanking(int ranking) { this.ranking = ranking; } /** * Controls whether the service actually gets published or not. This can be used by application context creators to * control service creation without blocking the creation of the context * * @param register whether to register the service or not. The default is true. */ public void setRegisterService(boolean register) { registerService = register; // Use targetClass as a proxy for afterPropertiesSet() being called. if (registerService && targetClass != null) { registerService(); } } public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanFactory = beanFactory; } public void setBundleContext(BundleContext context) { this.bundleContext = context; } /** * Returns the property resolver used for publishing the service. * * @return service property resolver */ public OsgiServicePropertiesResolver getResolver() { return this.propertiesResolver; } /** * Sets the property resolver used when publishing the bean as an OSGi service. * * @param resolver service property resolver */ public void setResolver(OsgiServicePropertiesResolver resolver) { this.propertiesResolver = resolver; } /** * Returns the interfaces that will be considered when exporting the target as an OSGi service. * * @return interfaces under which the target will be published as an OSGi service */ public Class<?>[] getInterfaces() { return interfaces; } /** * Sets the interfaces advertised by the service.These will be advertised in the OSGi space and are considered when * looking for a service. * * @param interfaces array of classes to advertise */ public void setInterfaces(Class<?>[] interfaces) { this.interfaces = interfaces; } public int getOrder() { return order; } /** * Set the ordering which will apply to this class's implementation of Ordered, used when applying multiple * BeanPostProcessors. <p> Default value is <code>Integer.MAX_VALUE</code>, meaning that it's non-ordered. * * @param order ordering value */ public void setOrder(int order) { this.order = order; } /** * Returns the bean name of this class when configured inside a Spring container. * * @return the bean name for this class */ public String getBeanName() { return beanName; } public void setBeanName(String name) { this.beanName = name; } /** * Sets the caching of the exported target object. When enabled, the exporter will ignore the scope of the * target bean and use only the first resolved instance for registration. When disabled (default), the scope of the * target bean is considered and each service request, will be directed to the container. * * Set this option to 'true' to obtain OSGi 4.2 blueprint behaviour. */ public void setCacheTarget(boolean cacheTarget) { this.cacheTarget = cacheTarget; } }