/* * Copyright 2002-2006 the original author or authors. * * Licensed 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. * * Created on 23-Jan-2006 by Adrian Colyer */ package org.springframework.osgi.context; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Dictionary; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.osgi.framework.Bundle; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceReference; import org.osgi.util.tracker.ServiceTracker; import org.osgi.util.tracker.ServiceTrackerCustomizer; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.osgi.context.support.DefaultOsgiBundleXmlApplicationContextFactory; import org.springframework.osgi.context.support.OsgiBundleXmlApplicationContext; import org.springframework.osgi.context.support.OsgiBundleXmlApplicationContextFactory; import org.springframework.osgi.service.OsgiServiceUtils; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; /** * OSGi bundle activator class to be used as the bundle activator for OSGi * bundles using Spring services. When the bundle is activated it creates an * application context and destroys it when the bundled is stopped. * * By default the ContextLoaderBundleActivator will look for an application * context file in the location * /META-INF/<bundle-symbolic-name>-context.xml. You can override this * default behaviour by adding a bundle manifest header entry of the form * * Spring-Context: <comma-delimited list of context file locations> * * The manifest entry may contain any number of resource paths, separated by any * number of commas and spaces. * * TODO: support parent application context via additional header giving name of * parent application context service (by default this will be * <bundle-symbolic-name>-springApplicationContext. * * @author Adrian Colyer * @since 2.0 */ public class ContextLoaderBundleActivator implements BundleActivator { private static final String CONTEXT_LOCATION_HEADER = "Spring-Context"; private static final String PARENT_CONTEXT_SERVICE_NAME_HEADER = "Spring-Parent-Context"; private static final String CONTEXT_LOCATION_DELIMITERS = ", "; private static final String DEFAULT_CONTEXT_PREFIX = "/META-INF/"; private static final String DEFAULT_CONTEXT_POSTFIX = "-context.xml"; private static final String CONTEXT_OPTIONS = "Spring-Context-Options"; /** * is this really required? - if no parent is found, an exception is thrown * anyway */ private static final String FAIL_FAST_OPTION = "honor-dependent-services"; private OsgiBundleXmlApplicationContextFactory contextFactory = new DefaultOsgiBundleXmlApplicationContextFactory(); private ConfigurableApplicationContext applicationContext; private ServiceReference parentServiceReference; private ServiceTracker serviceTracker; private static final Log log = LogFactory.getLog(ContextLoaderBundleActivator.class); /** * BundleActivator.start */ public void start(BundleContext bundleContext) throws Exception { Bundle myBundle = bundleContext.getBundle(); log.info("starting bundle " + myBundle.getSymbolicName() + myBundle.getBundleId()); String[] applicationContextLocations = getApplicationContextLocations(myBundle); ApplicationContext parent = getParentApplicationContext(bundleContext); this.applicationContext = this.contextFactory.createApplicationContext(parent, bundleContext, applicationContextLocations); } /* * (non-Javadoc) * * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext) */ public void stop(BundleContext bundleContext) throws Exception { if (this.applicationContext != null) { this.applicationContext.close(); } if (this.parentServiceReference != null) { bundleContext.ungetService(this.parentServiceReference); } if (this.serviceTracker != null) serviceTracker.close(); } /** * Search for the Spring-Parent-Context application context. * * @param context * @return */ protected ApplicationContext getParentApplicationContext(final BundleContext context) { String parentContextServiceName = (String) context.getBundle().getHeaders().get( PARENT_CONTEXT_SERVICE_NAME_HEADER); if (parentContextServiceName == null) { if (log.isDebugEnabled()) log.debug("no need to look for a parent context"); return null; } else { if (log.isDebugEnabled()) log.debug("looking for a parent context..."); // try to find the service String filter = "(" + OsgiBundleXmlApplicationContext.APPLICATION_CONTEXT_SERVICE_NAME_HEADER + "=" + parentContextServiceName + ")"; parentServiceReference = OsgiServiceUtils.getService(context, ApplicationContext.class, filter); // TODO: register as service listener..., probably in a proxy to the // app context // that we create here and return instead. // Costin: done, should be verified though. return createApplicationContextProxy(context, parentServiceReference); } } /** * Create a proxy around the target application context. * * @param parent * @return */ protected ApplicationContext createApplicationContextProxy(BundleContext context, ServiceReference serviceReference) { LookupApplicationContextInvocationHandler handler = new LookupApplicationContextInvocationHandler(context, serviceReference, serviceTracker); // TODO: interfaces are detected dynamically - is this dangerous (for // example if the parent context changes) // As the child depends on it, recreating the parent context should // trigger the whole process again. ApplicationContext target = handler.getTarget(); Class[] ifaces = (target == null ? new Class[] { ApplicationContext.class } : ClassUtils.getAllInterfaces(target)); return (ApplicationContext) Proxy.newProxyInstance(getClass().getClassLoader(), ifaces, handler); } /** * Retrieves the org.springframework.context manifest header attribute and * parses it to create a String[] of resource names for creating the * application context. * * If the org.springframework.context header is not present, the default * <bundle-symbolic-name>-context.xml file will be returned. */ protected String[] getApplicationContextLocations(Bundle bundle) { Dictionary manifestHeaders = bundle.getHeaders(); String contextLocationsHeader = (String) manifestHeaders.get(CONTEXT_LOCATION_HEADER); if (contextLocationsHeader != null) { // (Dictionary does not offer a "containsKey" operation) return addBundlePrefixTo(StringUtils.tokenizeToStringArray(contextLocationsHeader, CONTEXT_LOCATION_DELIMITERS)); } else { String defaultName = DEFAULT_CONTEXT_PREFIX + bundle.getSymbolicName() + DEFAULT_CONTEXT_POSTFIX; return addBundlePrefixTo(new String[] { defaultName }); } } /** * add the "bundle:" prefix to the resource location paths in the given * argument. This ensures that only this bundle will be searched for * matching resources. * * Modifies the argument in place and returns it. */ private String[] addBundlePrefixTo(String[] resourcePaths) { for (int i = 0; i < resourcePaths.length; i++) { resourcePaths[i] = OsgiBundleResource.BUNDLE_URL_PREFIX + resourcePaths[i]; } return resourcePaths; } // for testing... protected void setApplicationContext(ConfigurableApplicationContext context) { this.applicationContext = context; } // for testing... protected void setParentServiceReference(ServiceReference ref) { this.parentServiceReference = ref; } // for testing... protected void setApplicationContextFactory(OsgiBundleXmlApplicationContextFactory factory) { this.contextFactory = factory; } /** * Simple lookup proxy using a ServiceTracker underneath. * * @author Costin Leau * */ private static class LookupApplicationContextInvocationHandler implements InvocationHandler { private final ServiceReference serviceReference; private ApplicationContext target; public LookupApplicationContextInvocationHandler(final BundleContext context, ServiceReference serviceRef, ServiceTracker serviceTracker) { this.serviceReference = serviceRef; ApplicationContext parent = (ApplicationContext) context.getService(serviceReference); serviceTracker = new ServiceTracker(context, serviceReference, new ServiceTrackerCustomizer() { public Object addingService(ServiceReference ref) { if (log.isDebugEnabled()) log.debug("parentApplicationContext has been discovered"); // multiple parent contexts are already handled by // OsgiServiceUtils.getService() target = (ApplicationContext) context.getService(ref); return target; } public void modifiedService(ServiceReference ref, Object service) { if (log.isDebugEnabled()) log.debug("parentApplicationContext has been modified"); // TODO: should we refresh the child context (happens // automatically if the parent is refreshed) } public void removedService(ServiceReference ref, Object service) { if (log.isDebugEnabled()) log.debug("parentApplicationContext has been removed"); target = null; // TODO: should we close the child context? } }); } /** * Used to get the discovered target object (for example for detecting * the implemented interfaces). * * @return */ protected ApplicationContext getTarget() { return target; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); if (methodName.equals("equals")) { // Only consider equal when proxies are identical. return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE); } else if (methodName.equals("hashCode")) { // Use hashCode of SessionFactory proxy. return new Integer(hashCode()); } try { if (target != null) return method.invoke(this.target, args); } catch (InvocationTargetException ex) { throw ex.getTargetException(); } throw new UnsupportedOperationException("no parentApplicationContext in place"); } } /** * Close suppressing invocation handler - proxy used as a 'shield' against * forbidden close inside an OSGi environment. * * Not used at the moment. * * @author Costin Leau * */ private static class CloseSuppresingApplicationContextInvocationHandler implements InvocationHandler { private final ApplicationContext target; public CloseSuppresingApplicationContextInvocationHandler(ApplicationContext appContext) { this.target = appContext; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); // suppress close calls: // applicationContext close // Lifecycle interface // TODO: what about refresh() ? if (methodName.equals("close") || methodName.equals("stop")) return null; if (methodName.equals("equals")) { // Only consider equal when proxies are identical. return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE); } else if (methodName.equals("hashCode")) { // Use hashCode of SessionFactory proxy. return new Integer(hashCode()); } try { return method.invoke(this.target, args); } catch (InvocationTargetException ex) { throw ex.getTargetException(); } } } }