package org.agnitas.emm.extension.impl; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.ResourceBundle; import java.util.Vector; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.jsp.PageContext; import org.agnitas.emm.extension.EmmFeatureExtension; import org.agnitas.emm.extension.ExtensionManager; import org.agnitas.emm.extension.ExtensionSystem; import org.agnitas.emm.extension.JspExtension; import org.agnitas.emm.extension.PluginContext; import org.agnitas.emm.extension.PluginFilenameFilter; import org.agnitas.emm.extension.PluginInstaller; import org.agnitas.emm.extension.ResourceBundleManager; import org.agnitas.emm.extension.dao.PluginDao; import org.agnitas.emm.extension.data.PluginData; import org.agnitas.emm.extension.data.PluginDetail; import org.agnitas.emm.extension.data.PluginStatusReport; import org.agnitas.emm.extension.data.impl.PluginDataImpl; import org.agnitas.emm.extension.data.impl.PluginDetailImpl; import org.agnitas.emm.extension.data.impl.PluginStatusImpl; import org.agnitas.emm.extension.data.impl.PluginStatusReportImpl; import org.agnitas.emm.extension.exceptions.DatabaseScriptException; import org.agnitas.emm.extension.exceptions.ExtensionException; import org.agnitas.emm.extension.exceptions.JspExtensionException; import org.agnitas.emm.extension.exceptions.MissingPluginManifestException; import org.agnitas.emm.extension.exceptions.PluginInstantiationException; import org.agnitas.emm.extension.exceptions.RemovingSystemPluginNotAllowedException; import org.agnitas.emm.extension.exceptions.UnknownPluginException; import org.agnitas.emm.extension.util.ExtensionConstants; import org.agnitas.emm.extension.util.ExtensionUtils; import org.agnitas.emm.extension.util.I18NFactory; import org.agnitas.emm.extension.util.I18NResourceBundle; import org.apache.log4j.Logger; import org.java.plugin.JpfException; import org.java.plugin.ObjectFactory; import org.java.plugin.PluginClassLoader; import org.java.plugin.PluginLifecycleException; import org.java.plugin.PluginManager; import org.java.plugin.PluginManager.PluginLocation; import org.java.plugin.registry.Extension; import org.java.plugin.registry.ExtensionPoint; import org.java.plugin.registry.Identity; import org.java.plugin.registry.PluginAttribute; import org.java.plugin.registry.PluginDescriptor; import org.java.plugin.registry.PluginRegistry; import org.java.plugin.standard.StandardPluginLocation; import org.springframework.context.ApplicationContext; /** * ExtensionSystem. Encapsulates the whole logic to load, locate and invoke plugins. * * This class is automatically instantiated once at start-up of the web application. * * <b>Do not instantiate this class. To get the instance, use ExtensionUtils.getExtensionSystem(ServletContext).</b> * * @author md * @see ExtensionUtils#getExtensionSystem(javax.servlet.ServletContext) */ public class ExtensionSystemImpl implements ExtensionSystem { /** Logger. */ private static final transient Logger logger = Logger.getLogger( ExtensionSystemImpl.class); private static final String CORE_PLUGIN_ID = "core"; /** JPFs ObjectFactory. */ private final ObjectFactory objectFactory; /** JPFs PluginManager. */ private final PluginManager pluginManager; /** Manager for JspExtensions. */ private final ExtensionManager<JspExtension> jspExtensionManager; /** Manager for EmmFeatureExtensions. */ private final ExtensionManager<EmmFeatureExtension> featureExtensionManager; /** Cache for ResourceBundles */ private final ResourceBundleManager resourceBundleManager; private final LocationTracker locationTracker; private final PluginInstaller pluginInstaller; private final JspRestoreUtil jspRestoreUtil; private final ExtensionSystemConfiguration configuration; private final PluginDao pluginDao; /** * Instantiates the ExtensionSystem. * * Do not instantiate the class yourself. This is done at start-up. * * @param systemPluginDirectory directory containing the system plugins * @param pluginJspBaseDirectory base directory for plugin JSPs * * @see ExtensionUtils#getExtensionSystem(javax.servlet.ServletContext) */ public ExtensionSystemImpl( ExtensionSystemConfiguration configuration, JspRestoreUtil jspRestoreUtil, PluginInstaller pluginInstaller, PluginDao pluginDao) { if( logger.isInfoEnabled()) { logger.info( "creating extension system"); } this.configuration = configuration; this.objectFactory = ObjectFactory.newInstance(); this.pluginManager = objectFactory.createManager(); this.jspExtensionManager = new ExtensionManager<JspExtension>( this.pluginManager); this.featureExtensionManager = new ExtensionManager<EmmFeatureExtension>( this.pluginManager); this.resourceBundleManager = new ResourceBundleManager( this); this.locationTracker = new LocationTracker(); this.jspRestoreUtil = jspRestoreUtil; this.pluginInstaller = pluginInstaller; this.pluginDao = pluginDao; } /** * Starts up the extension system. Loads all plugins and activate them. * * @throws JpfException on errors during start up */ public void startUp() throws JpfException { if( logger.isInfoEnabled()) { logger.info( "startup of extension system"); } initializeExtensionSystem(); restorePluginJsps(); if( logger.isInfoEnabled()) { logger.info( "extension system is UP"); } } private void restorePluginJsps() { Collection<PluginDescriptor> plugins = getPluginRegistry().getPluginDescriptors(); for( PluginDescriptor descriptor : plugins) { if( logger.isInfoEnabled()) { logger.info( "Restoring JSPs for plugin " + descriptor.getId()); } try { this.jspRestoreUtil.createWorkingJspsFromBackupDirectory( descriptor.getId()); } catch( Exception e) { logger.error( "Error restoring plugin JSPs", e); } } } /** * Initializes the ExtensionSystem instance. Loads and activates plugins. * * @throws JpfException */ private void initializeExtensionSystem() throws JpfException { registerAvailablePlugins(); activatePlugins(); } /** * Looks for plugins in the plugin directory and tries to load the plugins. * Loading process is continued with the next plugin in the list, when loading of a plugin fails. * * @throws JpfException on registering plugins */ private void registerAvailablePlugins() throws JpfException { if( logger.isInfoEnabled()) { logger.info( "Registering system plugins"); } registerAvailablePlugins( this.configuration.getSystemPluginsBaseDirectory(), true); if( logger.isInfoEnabled()) { logger.info( "Registering additional plugins"); } registerAvailablePlugins( this.configuration.getPluginsBaseDirectory(), false); } private void registerAvailablePlugins( String path, boolean isSystemPath) throws JpfException { File directory = new File( path); File[] files = directory.listFiles( new PluginFilenameFilter()); Collection<PluginLocation> locations = new Vector<PluginLocation>(); if( files != null) { for( File file : files) { try { if( file.isFile()) { if( logger.isInfoEnabled()) logger.info( "found archive file: " + file.getAbsolutePath()); PluginLocation location = StandardPluginLocation.create( file); if( logger.isDebugEnabled()) logger.debug( "plugin location is " + location); locations.add( location); } else { PluginLocation location = processDirectory( file); if( location != null) { if( logger.isInfoEnabled()) logger.info( "found plugin location: " + file.getAbsolutePath()); locations.add( location); } } } catch (MalformedURLException e) { logger.warn( "wrong plugin file: " + file.getAbsolutePath(), e); } } } publishPlugins( locations, isSystemPath); } private void registerLocations( Collection<PluginLocation> locations, boolean systemPlugins) { for( PluginLocation location : locations) this.locationTracker.registerLocation( location, systemPlugins); } private void publishPlugins( Collection<PluginLocation> locations, boolean systemPlugins) throws JpfException { PluginLocation[] pluginLocationsArray = locations.toArray( new PluginLocation[locations.size()]); if( logger.isInfoEnabled()) { logger.info( "Publishing plugins"); } Map<String, Identity> map = this.pluginManager.publishPlugins( pluginLocationsArray); if( logger.isInfoEnabled()) { logger.info( "Plugins published"); } registerLocations( locations, systemPlugins); } private void publishPlugin( PluginLocation location) throws JpfException { if( logger.isInfoEnabled()) { logger.info( "Publishing single plugin"); } this.pluginManager.publishPlugins( new PluginLocation[] { location }); this.locationTracker.registerLocation( location, false); if( logger.isInfoEnabled()) { logger.info( "Plugin published"); } } /** * Post-order traversal of directory tree to find a plugin not archived in a single file. * For each directory, the methods tries to create a PluginLocation and stops, if the attempt * is successful. Otherwise the subdirectories are processed. * If no plugin was found, null is returned. * * @param directory directory to search in * * @return PluginLocation location of the plugin or null, if no plugin was found. */ private PluginLocation processDirectory( File directory) { PluginLocation pluginLocation = null; try { pluginLocation = StandardPluginLocation.create( directory); // No plugin found? if (pluginLocation == null) { // Get directory content... File[] files = directory.listFiles(); for( File file : files) { // ... and traverse sub-directories. if( file.isDirectory()) { pluginLocation = processDirectory( file); if( pluginLocation != null) break; } } } } catch( MalformedURLException e) { return null; } return pluginLocation; } /** * Tries to active all loaded plugins. */ private void activatePlugins() { Collection<PluginDescriptor> descriptors = getPluginRegistry().getPluginDescriptors(); for( PluginDescriptor descriptor : descriptors) { try { // Check DB if plugin is to be enabled at startup if( checkActivationOnStartup( descriptor.getId())) { if( logger.isInfoEnabled()) { logger.info( "Activating plugin " + descriptor.getId()); } activatePlugin( descriptor); } else { if( logger.isInfoEnabled()) { logger.info( "Do not activate plugin " + descriptor.getId() + " due to startup-configuration"); } } } catch (PluginLifecycleException e) { logger.error( "Error activating plugin: " + descriptor.getPluginClassName(), e); } catch (PluginInstantiationException e) { logger.error( "Error activating plugin: " + descriptor.getPluginClassName(), e); } } } /** * Active a single plugin. * * @param pluginDescriptor plugin to active * * @throws PluginLifecycleException on errors activating the plugin * @throws PluginInstantiationException on errors creating a plugin instance */ private void activatePlugin( PluginDescriptor pluginDescriptor) throws PluginLifecycleException, PluginInstantiationException { if( logger.isInfoEnabled()) logger.info( "Activating plugin: " + pluginDescriptor.getId()); pluginManager.activatePlugin( pluginDescriptor.getId()); } @Override public void invokeJspExtension( String pluginName, String extensionPointName, PageContext pageContext) { Collection<Extension> extensions = getActiveExtensions( pluginName, extensionPointName); JspExtension jspExtension; for( Extension extension : extensions) { try { jspExtension = this.jspExtensionManager.getOrCreateExtensionInstance( extension); jspExtension.invoke( extension, pageContext); } catch (JspExtensionException e) { logger.error( e); } catch (PluginInstantiationException e) { logger.error( e); } } } // TODO: Uses ApplicationContext in parameter list. PoC only! @Override public void invokeFeatureExtension( String pluginId, ApplicationContext context, HttpServletRequest request, HttpServletResponse response) throws PluginInstantiationException, ExtensionException, UnknownPluginException { Collection<Extension> extensions = getActiveExtensions( getCorePluginId(), "featurePlugin"); EmmFeatureExtension featureExtension = null; Extension extension = null; for( Extension extension0 : extensions) { if( extension0.getId().equals( pluginId)) { extension = extension0; break; } } if( extension != null) { featureExtension = this.featureExtensionManager.getOrCreateExtensionInstance( extension); PluginContext pluginContext = new PluginContextImpl( extension.getDeclaringPluginDescriptor().getId(), request, response); featureExtension.invoke( pluginContext, extension, context); } else { logger.warn( "Unknown plugin to invoke: " + pluginId); throw new UnknownPluginException( pluginId); } } // TODO: Uses ApplicationContext in parameter list. PoC only! @Override public void invokeFeatureSetupExtension( String pluginId, ApplicationContext context, HttpServletRequest request, HttpServletResponse response) throws PluginInstantiationException, ExtensionException, UnknownPluginException { Collection<Extension> extensions = getActiveExtensions( getCorePluginId(), "featurePlugin"); EmmFeatureExtension featureExtension = null; Extension extension = null; for( Extension extension0 : extensions) { if( extension0.getId().equals( pluginId)) { extension = extension0; break; } } if( extension != null) { featureExtension = this.featureExtensionManager.getOrCreateExtensionInstance( extension); PluginContext pluginContext = new PluginContextImpl( extension.getDeclaringPluginDescriptor().getId(), request, response); request.setAttribute( "agnPluginId", extension.getDeclaringPluginDescriptor().getId()); request.setAttribute( "agnExtensionId", extension.getId()); featureExtension.setup( pluginContext, extension, context); } else { logger.warn( "Unknown plugin to setup: " + pluginId); throw new UnknownPluginException( pluginId); } } @Override public Collection<Extension> getActiveExtensions( String plugin, String extensionPoint) { ExtensionPoint point = getPluginRegistry().getExtensionPoint( plugin, extensionPoint); Collection<Extension> connectedExtensions = point.getConnectedExtensions(); Collection<Extension> activeExtensions = new Vector<Extension>(); for( Extension extension : connectedExtensions) { if( this.pluginManager.isPluginActivated( extension.getDeclaringPluginDescriptor())) activeExtensions.add( extension); } return activeExtensions; } @Override public InputStream getPluginResource( String plugin, String resource) { PluginDescriptor pluginDescriptor = getPluginRegistry().getPluginDescriptor( plugin); PluginClassLoader classLoader = this.pluginManager.getPluginClassLoader( pluginDescriptor); return classLoader.getResourceAsStream( resource); } @Override public I18NResourceBundle getPluginI18NResourceBundle( String plugin) { PluginDescriptor pluginDescriptor = getPluginRegistry().getPluginDescriptor( plugin); PluginAttribute attribute = pluginDescriptor.getAttribute( "i18n-bundle"); I18NResourceBundle bundle = null; if( attribute != null) { String bundleName = attribute.getValue(); I18NFactory factory = new I18NFactory( pluginManager.getPluginClassLoader( pluginDescriptor), bundleName); bundle = new I18NResourceBundle( factory); } return bundle; } @Override public ResourceBundle getPluginResourceBundle( String plugin, String bundleName) { return this.resourceBundleManager.getResourceBundle( plugin, bundleName); } @Override public Extension getExtension( String plugin, String extension) { return getPluginRegistry().getPluginDescriptor( plugin).getExtension( extension); } public void shutdown() { this.pluginManager.shutdown(); } @Override public PluginStatusReport getPluginStatusReport() { Map<URL, PluginStatusImpl> pluginStatusItemMap = createStatusMapFromPluginLocations( this.locationTracker); Collection<PluginDescriptor> descriptors = getPluginRegistry().getPluginDescriptors(); setPluginStatusInformationsInMap( descriptors, pluginStatusItemMap); return convertMapToPluginStatusReport( pluginStatusItemMap); } @Override public PluginDetail getPluginDetails( String pluginID) throws UnknownPluginException { PluginDescriptor pluginDescriptor = getPluginRegistry().getPluginDescriptor( pluginID); if( pluginDescriptor == null) { logger.warn( "No plugin with ID " + pluginID + " found"); throw new UnknownPluginException( pluginID); } PluginDetailImpl pluginDetail = new PluginDetailImpl(); setPluginStatusInformation( pluginDetail, pluginDescriptor); setPluginDetailInformation( pluginDetail, pluginDescriptor); return pluginDetail; } private Map<URL, PluginStatusImpl> createStatusMapFromPluginLocations( LocationTracker locationTracker) { Collection<PluginLocation> pluginLocations = locationTracker.getPluginLocations(); Map<URL, PluginStatusImpl> map = new HashMap<URL, PluginStatusImpl>(); for( PluginLocation location : pluginLocations) { if( logger.isDebugEnabled()) logger.debug( "Tracked plugin location: " + location); PluginStatusImpl item = new PluginStatusImpl(); item.setUrl( location.getContextLocation()); map.put( location.getManifestLocation(), item); } return map; } private void setPluginStatusInformationsInMap( Collection<PluginDescriptor> descriptors, Map<URL, PluginStatusImpl> pluginStatusItemMap) { for( PluginDescriptor descriptor : descriptors) { PluginStatusImpl item = pluginStatusItemMap.get( descriptor.getLocation()); setPluginStatusInformation( item, descriptor); } } private void setPluginStatusInformation( PluginStatusImpl pluginStatus, PluginDescriptor pluginDescriptor) { if( logger.isDebugEnabled()) { logger.debug( "Descriptor location: " + pluginDescriptor.getLocation()); } PluginAttribute nameAttribute = pluginDescriptor.getAttribute( ExtensionConstants.PLUGIN_NAME_MANIFEST_ATTRIBUTE); PluginAttribute descriptionAttribute = pluginDescriptor.getAttribute( ExtensionConstants.PLUGIN_DESCRIPTION_MANIFEST_ATTRIBUTE); pluginStatus.setPluginId( pluginDescriptor.getId()); pluginStatus.setVersion( pluginDescriptor.getVersion().toString()); pluginStatus.setVendor( pluginDescriptor.getVendor()); pluginStatus.setPluginName( nameAttribute != null ? nameAttribute.getValue() : null); pluginStatus.setDescription( descriptionAttribute != null ? descriptionAttribute.getValue() : null); pluginStatus.setActivated( this.pluginManager.isPluginActivated( pluginDescriptor)); } private void setPluginDetailInformation( PluginDetailImpl pluginDetail, PluginDescriptor pluginDescriptor) { Collection<PluginDescriptor> dependingPlugins = getPluginRegistry().getDependingPlugins( pluginDescriptor); Collection<String> dependingPluginIds = new Vector<String>(); for( PluginDescriptor dependingPlugin : dependingPlugins) { dependingPluginIds.add( dependingPlugin.getId()); } pluginDetail.setDependingPluginIds( dependingPluginIds); pluginDetail.setSystemPlugin( isSystemPlugin( pluginDescriptor)); } private PluginStatusReport convertMapToPluginStatusReport( Map<URL, PluginStatusImpl> map) { return new PluginStatusReportImpl( map.values()); } /** * Utility method to return assigned plugin registry. * * @return PluginRegistry */ private PluginRegistry getPluginRegistry() { return this.pluginManager.getRegistry(); } @Override public void activatePluginForStartup( String pluginId) throws PluginLifecycleException { activatePlugin( pluginId); PluginData pluginData; try { pluginData = this.pluginDao.getPluginData( pluginId); } catch( UnknownPluginException e) { pluginData = createPluginData( pluginId); } pluginData.setActivatedOnStartup( true); pluginDao.savePluginData( pluginData); } private void activatePlugin( String pluginId) throws PluginLifecycleException { this.pluginManager.activatePlugin( pluginId); } @Override public void deactivatePluginForStartup( String pluginId) { deactivatePlugin( pluginId); PluginData pluginData; try { pluginData = this.pluginDao.getPluginData( pluginId); } catch( UnknownPluginException e) { pluginData = createPluginData( pluginId); } pluginData.setActivatedOnStartup( false); pluginDao.savePluginData( pluginData); } private void deactivatePlugin( String pluginId) { PluginDescriptor descriptor = getPluginRegistry().getPluginDescriptor( pluginId); this.pluginManager.deactivatePlugin( pluginId); if( descriptor != null) { this.jspExtensionManager.deregisterExtensions( descriptor.getExtensions()); this.featureExtensionManager.deregisterExtensions( descriptor.getExtensions()); } else { if( logger.isInfoEnabled()) { logger.info( "No descriptor found for plugin '" + pluginId + "'???"); } } } private boolean checkActivationOnStartup( String pluginId) { try { PluginData pluginData = this.pluginDao.getPluginData( pluginId); return pluginData.isActivatedOnStartup(); } catch( UnknownPluginException e) { if( logger.isInfoEnabled()) { logger.info( "No informations on plugin '" + pluginId + "' found. No activation on startup"); } return false; } } private PluginData createPluginData( String pluginId) { PluginData pluginData = new PluginDataImpl(); pluginData.setPluginId( pluginId); return pluginData; } @Override public void installPlugin(String pluginFilename) throws MissingPluginManifestException, IOException, JpfException, DatabaseScriptException { String pluginId = this.pluginInstaller.installPlugin( pluginFilename); File directory = new File( this.configuration.getPluginDirectory( pluginId)); if( logger.isDebugEnabled()) { logger.debug( "Activating installed plugin at " + directory.getAbsolutePath()); } PluginLocation location = StandardPluginLocation.create( directory); try { this.deactivatePlugin( pluginId); // Try to deactive active plugin if( logger.isInfoEnabled()) { logger.info( "plugin '" + pluginId + "' successfully deactivated before re-installing plugin"); } } catch( IllegalArgumentException e) { if( logger.isInfoEnabled()) { logger.info( "error deactivating plugin '" + pluginId + "' - plugin may not be active or installed"); } } this.getPluginRegistry().unregister( new String[] { pluginId } ); // Try to remove previously installed plugin this.publishPlugin( location); } @Override public void uninstallPlugin( String pluginId) throws RemovingSystemPluginNotAllowedException { // TODO: Prevent system plugins from removal PluginDescriptor descriptor = this.getPluginRegistry().getPluginDescriptor( pluginId); if( isSystemPlugin( descriptor)) { logger.warn( "Attempt to uninstall system plugin: " + pluginId); throw new RemovingSystemPluginNotAllowedException( pluginId); } this.locationTracker.unregisterLocation( descriptor); this.getPluginRegistry().unregister( new String[] { pluginId }); this.pluginInstaller.uninstallPlugin( pluginId); try { this.pluginDao.removePluginData( pluginId); } catch( Exception e) { logger.warn( "Error removing plugin data", e); } } protected String getCorePluginId() { return CORE_PLUGIN_ID; } @Override public boolean isSystemPlugin( String pluginId) { PluginDescriptor descriptor = this.getPluginRegistry().getPluginDescriptor( pluginId); return isSystemPlugin( descriptor); } public boolean isSystemPlugin( PluginDescriptor descriptor) { return this.locationTracker.isSystemPlugin( descriptor); } }