/* * Copyright: (c) 2004-2013 Mayo Foundation for Medical Education and * Research (MFMER). All rights reserved. MAYO, MAYO CLINIC, and the * triple-shield Mayo logo are trademarks and service marks of MFMER. * * Except as contained in the copyright notice above, or as used to identify * MFMER as the author of this software, the trade names, trademarks, service * marks, or product names of the copyright holder shall not be used in * advertising, promotion or otherwise in connection with this software without * prior written authorization of the copyright holder. * * 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. */ package edu.mayo.cts2.framework.core.plugin; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Map.Entry; import java.util.Properties; import java.util.ServiceLoader; import java.util.Set; import javax.annotation.Resource; import javax.servlet.ServletContext; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.ArrayUtils; import org.apache.felix.cm.impl.ConfigurationManager; import org.apache.felix.framework.Felix; import org.apache.felix.framework.Logger; import org.apache.felix.framework.util.FelixConstants; import org.apache.felix.framework.util.StringMap; import org.osgi.framework.Bundle; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; import org.osgi.framework.BundleException; import org.osgi.framework.Constants; import org.osgi.framework.ServiceReference; import org.osgi.framework.ServiceRegistration; import org.osgi.service.cm.Configuration; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.util.tracker.ServiceTracker; import org.osgi.util.tracker.ServiceTrackerCustomizer; import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.stereotype.Component; import org.springframework.util.ClassUtils; import org.springframework.web.context.ServletContextAware; import com.atlassian.plugin.osgi.container.OsgiContainerException; import com.atlassian.plugin.osgi.container.PackageScannerConfiguration; import com.atlassian.plugin.osgi.container.impl.DefaultPackageScannerConfiguration; import edu.mayo.cts2.framework.core.config.ConfigInitializer; import edu.mayo.cts2.framework.core.config.ConfigUtils; import edu.mayo.cts2.framework.core.config.Cts2DeploymentConfig; /** * Felix implementation of the OSGi container manager. * * @author <a href="mailto:kevin.peterson@mayo.edu">Kevin Peterson</a> */ @Component public class FelixPluginManager implements InitializingBean, DisposableBean, OsgiPluginManager, ServletContextAware, ApplicationContextAware { public static final String SUPPRESS_OSGI_CONFIG_PROP_NAME = "osgi.suppress"; private static final boolean DEFALUE_SUPPRESS_OSGI_CONFIG_VALUE = false; public static final String OSGI_FRAMEWORK_BUNDLES_ZIP = "osgi-framework-bundles.zip"; public static final int REFRESH_TIMEOUT = 10; public static final String MIN_SERVLET_VERSION = "2.5.0"; private ServletContext servletContext; private ApplicationContext applicationContext; @Resource private ConfigInitializer configInitializer; @Resource private SupplementalPropetiesLoader supplementalPropetiesLoader; @Resource private Cts2DeploymentConfig cts2GeneralConfig; private Set<NonOsgiPluginInitializer> nonOsgiPlugins = new HashSet<NonOsgiPluginInitializer>(); private static final org.slf4j.Logger log = LoggerFactory.getLogger(FelixPluginManager.class); private static final String OSGI_BOOTDELEGATION = "org.osgi.framework.bootdelegation"; private static final String ATLASSIAN_PREFIX = "atlassian."; private Collection<ServiceTracker> trackers = new ArrayList<ServiceTracker>(); private ExportsBuilder exportsBuilder = new ExportsBuilder(); private Felix felix = null; private boolean felixRunning = false; private Logger felixLogger = new Logger(){ }; /* (non-Javadoc) * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() */ @Override public void afterPropertiesSet() throws Exception { this.start(); } /** * Autodeploy bundles. * * @param pluginDirectory the plugin directory * @throws IOException Signals that an I/O exception has occurred. */ protected void autodeployBundles(File pluginDirectory) throws IOException{ PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); for(org.springframework.core.io.Resource resource : resolver.getResources("classpath:/autodeployBundles/*.jar")){ File bundleFile = new File( pluginDirectory.getPath() + File.separator + resource.getFilename()); if(! bundleFile.exists()){ FileUtils.copyInputStreamToFile(resource.getInputStream(), bundleFile); } } } /** * Start. * * @throws OsgiContainerException the osgi container exception */ public void start() throws OsgiContainerException { if (isRunning()) { return; } boolean suppressOsgi = this.cts2GeneralConfig.getBooleanProperty( SUPPRESS_OSGI_CONFIG_PROP_NAME, DEFALUE_SUPPRESS_OSGI_CONFIG_VALUE); // Create a case-insensitive configuration property map. final StringMap configMap = new StringMap(false); if(! suppressOsgi){ try { this.autodeployBundles(this.configInitializer.getPluginsDirectory()); } catch (IOException e) { throw new RuntimeException(e); } PackageScannerConfiguration scannerConfig = new DefaultPackageScannerConfiguration(); scannerConfig.getPackageIncludes().add("edu.mayo.cts2.*"); scannerConfig.getPackageIncludes().add("org.jaxen*"); scannerConfig.getPackageIncludes().add("com.sun*"); scannerConfig.getPackageIncludes().add("org.json*"); scannerConfig.getPackageIncludes().add("org.springframework.oxm*"); scannerConfig.getPackageExcludes().add("com.atlassian.plugins*"); scannerConfig.getPackageExcludes().remove("org.apache.commons.logging*"); scannerConfig.getPackageVersions().put("org.apache.commons.collections*", "3.2.1"); String exports = exportsBuilder.getExports(scannerConfig); if(log.isDebugEnabled()){ log.debug("Exports: " + exports); } // Explicitly add the servlet exports; exports += ",javax.servlet;version=" + MIN_SERVLET_VERSION; exports += ",javax.servlet.http;version=" + MIN_SERVLET_VERSION; // Add the bundle provided service interface package and the core OSGi // packages to be exported from the class path via the system bundle. configMap.put(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA, exports); } // Explicitly specify the directory to use for caching bundles. File felixCache = ConfigUtils.createSubDirectory( this.configInitializer.getContextConfigDirectory(), ".osgi-felix-cache"); configMap.put(FelixConstants.FRAMEWORK_STORAGE, felixCache.getPath()); configMap.put(FelixConstants.LOG_LEVEL_PROP, String.valueOf(felixLogger.getLogLevel())); configMap.put(FelixConstants.LOG_LOGGER_PROP, felixLogger); configMap.put(FelixConstants.FRAGMENT_ATTACHMENT_RESOLVETIME, felixLogger); String bootDelegation = getAtlassianSpecificOsgiSystemProperty(OSGI_BOOTDELEGATION); if ((bootDelegation == null) || (bootDelegation.trim().length() == 0)){ // These exist to work around JAXP problems. Specifically, bundles that use static factories to create JAXP // instances will execute FactoryFinder with the CCL set to the bundle. These delegations ensure the appropriate // implementation is found and loaded. bootDelegation = "weblogic,weblogic.*," + "META-INF.services," + "com.yourkit,com.yourkit.*," + "com.chronon,com.chronon.*," + "com.jprofiler,com.jprofiler.*," + "org.apache.xerces,org.apache.xerces.*," + "org.apache.xalan,org.apache.xalan.*," + "org.apache.xpath.*," + "org.apache.xml.serializer," + "org.springframework.stereotype,"+ "org.springframework.web.bind.annotation," + "org.springframework.web.servlet," + "javax.*," + "org.osgi.*," + "org.apache.felix.*," + "sun.*," + "com.sun.*," + "com.sun.xml.bind.v2," + "com.icl.saxon"; } configMap.put(FelixConstants.FRAMEWORK_BOOTDELEGATION, bootDelegation); configMap.put(FelixConstants.IMPLICIT_BOOT_DELEGATION_PROP, "false"); configMap.put(FelixConstants.FRAMEWORK_BUNDLE_PARENT, FelixConstants.FRAMEWORK_BUNDLE_PARENT_FRAMEWORK); if (log.isDebugEnabled()) { log.debug("Felix configuration: " + configMap); } if(! suppressOsgi){ validateConfiguration(configMap); } try { final List<BundleActivator> hostServices = new ArrayList<BundleActivator>(); for(Entry<String, Object> bean : this.applicationContext.getBeansWithAnnotation(ExportedService.class).entrySet()){ Object service = bean.getValue(); ExportedService annotation = service.getClass().getAnnotation(ExportedService.class); Class<?>[] interfaces = annotation.value(); if(interfaces.length == 1 && interfaces[0] == Void.class){ interfaces = ClassUtils.getAllInterfaces(service); } hostServices.add(new HostActivator(service, interfaces)); } configMap.put(FelixConstants.SYSTEMBUNDLE_ACTIVATORS_PROP, hostServices); // Now create an instance of the framework with // our configuration properties and activator. felix = new Felix(configMap); felixRunning = true; felix.init(); BundleContext context = felix.getBundleContext(); ServiceTracker tracker = new ServiceTracker( felix.getBundleContext(), ConfigurationAdmin.class.getName(), new ServiceTrackerCustomizer(){ @Override public Object addingService(ServiceReference reference) { ConfigurationAdmin cm = (ConfigurationAdmin) felix.getBundleContext().getService(reference); try { for(Entry<String, Properties> entrySet : supplementalPropetiesLoader.getOverriddenProperties().entrySet()){ Configuration config = cm.getConfiguration(entrySet.getKey()); config.update(entrySet.getValue()); } } catch (IOException e) { throw new RuntimeException(e); } return cm; } @Override public void modifiedService(ServiceReference reference, Object service) { // } @Override public void removedService(ServiceReference reference, Object service) { // } }); tracker.open(); this.trackers.add(tracker); if(suppressOsgi){ new ConfigurationManager().start(context); } else { FileFilter fileOnlyFilter = new FileFilter(){ @Override public boolean accept(File file) { return !file.isDirectory(); } }; for(File bundle : this.configInitializer.getPluginsDirectory().listFiles(fileOnlyFilter)){ Bundle installedBundle = felix.getBundleContext().installBundle(bundle.toURI().toString()); try { if(installedBundle.getHeaders().get(Constants.FRAGMENT_HOST) != null){ log.info("Not Auto-starting Fragment bundle: " + installedBundle.getSymbolicName()); } else { installedBundle.start(); log.info("Auto-starting system bundle: " + installedBundle.getSymbolicName()); } } catch (BundleException e) { log.warn("Bundle: " + installedBundle.getSymbolicName() + " failed to start.", e); } } } felix.start(); this.initializeNonOsgiPlugins(); for(String bean : this.applicationContext.getBeanNamesForType(ExtensionPoint.class)){ ExtensionPoint extensionPoint = this.applicationContext.getBean(bean, ExtensionPoint.class); this.registerExtensionPoint(extensionPoint); } servletContext.setAttribute( BundleContext.class.getName(), context); servletContext.setAttribute( PluginManager.class.getName(), this); } catch (final Exception ex) { throw new OsgiContainerException("Unable to start OSGi container", ex); } } protected void initializeNonOsgiPlugins(){ ServiceLoader<NonOsgiPluginInitializer> serviceLoader = ServiceLoader.load(NonOsgiPluginInitializer.class); Iterator<NonOsgiPluginInitializer> itr = serviceLoader.iterator(); while(itr.hasNext()){ NonOsgiPluginInitializer pluginInitializer = itr.next(); pluginInitializer.initialize(this); this.nonOsgiPlugins.add(pluginInitializer); } } /** * The Class HostActivator. * * @author <a href="mailto:kevin.peterson@mayo.edu">Kevin Peterson</a> */ public static class HostActivator implements BundleActivator { private BundleContext m_context = null; private ServiceRegistration m_registration = null; private Object service; private Class<?>[] interfaces; /** * Instantiates a new host activator. * * @param service the service * @param interfaces the interfaces */ public HostActivator(Object service, Class<?>[] interfaces){ this.service = service; this.interfaces = (Class<?>[]) ArrayUtils.clone(interfaces); } /* (non-Javadoc) * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext) */ @SuppressWarnings({ "rawtypes" }) public void start(BundleContext context) { // Save a reference to the bundle context. m_context = context; // Register the property lookup service and save // the service registration. Hashtable prefs = null; if(this.service instanceof ServiceMetadataAware){ prefs = new Hashtable(); prefs = ((ServiceMetadataAware) this.service).getMetadata(); } if(this.service instanceof BundleContextAware){ ((BundleContextAware) this.service).setBundleContext(this.m_context); } m_registration = m_context.registerService( this.classesToStrings( this.interfaces), this.service, prefs); } /* (non-Javadoc) * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext) */ public void stop(BundleContext context) { // Unregister the property lookup service. m_registration.unregister(); m_context = null; } private String[] classesToStrings(Class<?>[] classes){ String[] strings = new String[classes.length]; for(int i=0;i<strings.length;i++){ strings[i] = classes[i].getName(); } return strings; } } /** * * @param configMap The Felix configuration * @throws OsgiContainerException If any validation fails */ private void validateConfiguration(StringMap configMap) throws OsgiContainerException { String systemExports = (String) configMap.get(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA); detectIncorrectOsgiVersion(); detectXercesOverride(systemExports); } /** * Detect when xerces has no version, most likely due to an installation of Tomcat where an old version of xerces * is installed into common/lib/endorsed in order to support Java 1.4. * * @param systemExports The system exports * @throws OsgiContainerException If xerces has no version */ private void detectXercesOverride(String systemExports) throws OsgiContainerException { int pos = systemExports.indexOf("org.apache.xerces.util"); if (pos > -1) { if (pos == 0 || (pos > 0 && systemExports.charAt(pos - 1) == ',')) { pos += "org.apache.xerces.util".length(); // only fail if no xerces found and xerces has no version if (pos >= systemExports.length() || ';' != systemExports.charAt(pos)) { throw new OsgiContainerException( "Detected an incompatible version of Apache Xerces on the classpath. If using Tomcat, you may have " + "an old version of Xerces in $TOMCAT_HOME/common/lib/endorsed that will need to be removed."); } } } } /** * Detects incorrect configuration of WebSphere 6.1 that leaks OSGi 4.0 jars into the application */ private void detectIncorrectOsgiVersion() { try { Bundle.class.getMethod("getBundleContext"); } catch (final NoSuchMethodException e) { throw new OsgiContainerException( "Detected older version (4.0 or earlier) of OSGi. If using WebSphere " + "6.1, please enable application-first (parent-last) classloading and the 'Single classloader for " + "application' WAR classloader policy."); } } /** * Stop. * * @throws OsgiContainerException the osgi container exception */ protected void stop() throws OsgiContainerException { for(NonOsgiPluginInitializer nonOsgiPlugins : this.nonOsgiPlugins){ nonOsgiPlugins.destroy(); } if (felixRunning){ try { for (final ServiceTracker tracker : new ArrayList<ServiceTracker>(this.trackers)){ tracker.close(); } } catch (Exception e) { log.warn("Error closing ServiceTrackers", e); } try { felix.stop(); felix.waitForStop(5000); } catch (InterruptedException e){ log.warn("Interrupting Felix shutdown", e); } catch (BundleException ex){ log.error("An error occurred while stopping the Felix OSGi Container. ", ex); } } felixRunning = false; felix = null; } public ServiceReference[] getRegisteredServices() { return felix.getRegisteredServices(); } /** * Gets the service tracker. * * @param clazz the clazz * @param customizer the customizer * @return the service tracker */ public ServiceTracker getServiceTracker(final String clazz, ServiceTrackerCustomizer customizer) { if (!isRunning()) { throw new IllegalStateException( "Unable to create a tracker when osgi is not running"); } final ServiceTracker tracker = new ServiceTracker( this.felix.getBundleContext(), clazz, customizer) { @Override public void close() { super.close(); trackers.remove(this); } }; tracker.open(); trackers.add(tracker); return tracker; } public boolean isRunning() { return felixRunning; } private String getAtlassianSpecificOsgiSystemProperty(final String originalSystemProperty) { return System.getProperty(ATLASSIAN_PREFIX + originalSystemProperty); } private interface DoWithBundle<T>{ T doWithBundle(Bundle bundle) throws BundleException; } /* (non-Javadoc) * @see edu.mayo.cts2.framework.core.plugin.PluginManager#removePlugin(java.lang.String, java.lang.String) */ @Override public void removePlugin(String pluginName, String pluginVersion) { this.doWithBundle(pluginName, pluginVersion, new DoWithBundle<Void>(){ @Override public Void doWithBundle(Bundle bundle) throws BundleException { bundle.uninstall(); return null; } }); } /** * Find bundle. * * @param name the name * @param version the version * @return the bundle */ protected Bundle findBundle(String name, String version){ for(Bundle bundle : this.felix.getBundleContext().getBundles()){ if(bundle.getSymbolicName().equals(name) && bundle.getVersion().toString().equals(version)){ return bundle; } } return null; } private <T> T doWithBundle(String name, String version, DoWithBundle<T> closure){ Bundle bundle = this.findBundle(name, version); if(bundle == null){ log.warn("Plugin: " + name + "version: " + version + " was not found."); } try { return closure.doWithBundle(bundle); } catch (BundleException e) { throw new RuntimeException(e); } } /* (non-Javadoc) * @see edu.mayo.cts2.framework.core.plugin.PluginManager#getPluginDescription(java.lang.String, java.lang.String) */ @Override public PluginDescription getPluginDescription( String pluginName, String pluginVersion) { return this.doWithBundle(pluginName, pluginVersion, new DoWithBundle<PluginDescription>(){ @Override public PluginDescription doWithBundle(Bundle bundle) throws BundleException { return buildPluginDescription(bundle); } }); } private PluginDescription buildPluginDescription(Bundle bundle){ return new PluginDescription( bundle.getSymbolicName(), bundle.getVersion().toString(), (String)bundle.getHeaders().get(Constants.BUNDLE_DESCRIPTION), bundle.getState() == Bundle.ACTIVE); } @Override public Set<PluginDescription> getPluginDescriptions() { Set<PluginDescription> returnSet = new HashSet<PluginDescription>(); for(Bundle bundle : this.felix.getBundleContext().getBundles()){ returnSet.add(this.buildPluginDescription(bundle)); } return returnSet; } /* (non-Javadoc) * @see edu.mayo.cts2.framework.core.plugin.PluginManager#activatePlugin(java.lang.String, java.lang.String) */ @Override public void activatePlugin(String name, String version) { this.doWithBundle(name, version, new DoWithBundle<Void>(){ @Override public Void doWithBundle(Bundle bundle) throws BundleException { bundle.start(); return null; } }); } /* (non-Javadoc) * @see edu.mayo.cts2.framework.core.plugin.PluginManager#dectivatePlugin(java.lang.String, java.lang.String) */ @Override public void dectivatePlugin(String name, String version) { this.doWithBundle(name, version, new DoWithBundle<Void>(){ @Override public Void doWithBundle(Bundle bundle) throws BundleException { bundle.stop(); return null; } }); } @Override public BundleContext getBundleContext() { return felix.getBundleContext(); } /* (non-Javadoc) * @see edu.mayo.cts2.framework.core.plugin.PluginManager#installPlugin(java.net.URL) */ @Override public void installPlugin(URL source) throws IOException { try { this.felix.getBundleContext().installBundle(source.toString()); } catch (BundleException e) { throw new RuntimeException(e); } } /* (non-Javadoc) * @see edu.mayo.cts2.framework.core.plugin.PluginManager#isPluginActive(edu.mayo.cts2.framework.core.plugin.PluginReference) */ @Override public boolean isPluginActive(PluginReference ref) { return this.doWithBundle( ref.getPluginName(), ref.getPluginVersion(), new DoWithBundle<Boolean>(){ @Override public Boolean doWithBundle(Bundle bundle) throws BundleException { return (bundle.getState() == Bundle.ACTIVE); } }); } /* (non-Javadoc) * @see edu.mayo.cts2.framework.core.plugin.PluginManager#registerExtensionPoint(edu.mayo.cts2.framework.core.plugin.ExtensionPoint) */ @Override public void registerExtensionPoint(ExtensionPoint extensionPoint) { extensionPoint.setServiceTracker( this.getServiceTracker( extensionPoint.getServiceClass().getName(), extensionPoint.addServiceTrackerCustomizer())); } /* (non-Javadoc) * @see org.springframework.beans.factory.DisposableBean#destroy() */ @Override public void destroy() throws Exception { this.stop(); } @Override public void setServletContext(ServletContext servletContext) { this.servletContext = servletContext; } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } }