/*! ****************************************************************************** * * Pentaho Data Integration * * Copyright (C) 2002-2017 by Pentaho : http://www.pentaho.com * ******************************************************************************* * * 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 org.pentaho.di.core.plugins; import java.io.File; import java.io.UnsupportedEncodingException; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.net.URLDecoder; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.pentaho.di.core.Const; import org.pentaho.di.core.exception.KettlePluginClassMapException; import org.pentaho.di.core.util.Utils; import org.pentaho.di.core.exception.KettlePluginException; import org.pentaho.di.core.logging.KettleLogStore; import org.pentaho.di.core.logging.LogChannel; import org.pentaho.di.core.logging.LogChannelInterface; import org.pentaho.di.core.logging.Metrics; import org.pentaho.di.core.row.RowBuffer; import org.pentaho.di.core.row.RowMeta; import org.pentaho.di.core.row.RowMetaInterface; import org.pentaho.di.core.row.value.ValueMetaString; import org.pentaho.di.core.util.EnvUtil; import org.pentaho.di.i18n.BaseMessages; /** * This singleton provides access to all the plugins in the Kettle universe.<br> It allows you to register types and * plugins, query plugin lists per category, list plugins per type, etc.<br> * * @author matt */ public class PluginRegistry { private static final Class<?> PKG = PluginRegistry.class; // for i18n purposes, needed by Translator2!! private static final PluginRegistry pluginRegistry = new PluginRegistry(); private static final List<PluginTypeInterface> pluginTypes = new ArrayList<PluginTypeInterface>(); private static final List<PluginRegistryExtension> extensions = new ArrayList<PluginRegistryExtension>(); public static final LogChannelInterface log = new LogChannel( "PluginRegistry", true ); public static final String SUPPLEMENTALS_SUFFIX = "-supplementals"; private final Map<Class<? extends PluginTypeInterface>, List<PluginInterface>> pluginMap; private final Map<String, URLClassLoader> folderBasedClassLoaderMap = new HashMap<String, URLClassLoader>(); private final Map<Class<? extends PluginTypeInterface>, Map<PluginInterface, URLClassLoader>> classLoaderMap; private final Map<String, URLClassLoader> classLoaderGroupsMap; private final Map<Class<? extends PluginTypeInterface>, List<String>> categoryMap; private final Map<PluginInterface, String[]> parentClassloaderPatternMap = new HashMap<PluginInterface, String[]>(); private final Map<Class<? extends PluginTypeInterface>, List<PluginTypeListener>> listeners = new HashMap<Class<? extends PluginTypeInterface>, List<PluginTypeListener>>(); private final ReentrantReadWriteLock lock; /** * Initialize the registry, keep private to keep this a singleton */ private PluginRegistry() { pluginMap = new HashMap<Class<? extends PluginTypeInterface>, List<PluginInterface>>(); classLoaderMap = new HashMap<Class<? extends PluginTypeInterface>, Map<PluginInterface, URLClassLoader>>(); categoryMap = new HashMap<Class<? extends PluginTypeInterface>, List<String>>(); classLoaderGroupsMap = new HashMap<String, URLClassLoader>(); lock = new ReentrantReadWriteLock(); } /** * @return The one and only PluginRegistry instance */ public static PluginRegistry getInstance() { return pluginRegistry; } public void registerPluginType( Class<? extends PluginTypeInterface> pluginType ) { lock.writeLock().lock(); try { if ( pluginMap.get( pluginType ) == null ) { pluginMap.put( pluginType, new ArrayList<PluginInterface>() ); } // Keep track of the categories separately for performance reasons... // if ( categoryMap.get( pluginType ) == null ) { categoryMap.put( pluginType, new ArrayList<String>() ); } } finally { lock.writeLock().unlock(); } } public void removePlugin( Class<? extends PluginTypeInterface> pluginType, PluginInterface plugin ) { lock.writeLock().lock(); try { List<PluginInterface> list = pluginMap.get( pluginType ); if ( list != null ) { list.remove( plugin ); } Map<PluginInterface, URLClassLoader> classLoaders = classLoaderMap.get( plugin.getPluginType() ); if ( classLoaders != null ) { classLoaders.remove( plugin ); } if ( !Utils.isEmpty( plugin.getClassLoaderGroup() ) ) { // Straight away remove the class loader for the whole group... // classLoaderGroupsMap.remove( plugin.getClassLoaderGroup() ); } } finally { lock.writeLock().unlock(); List<PluginTypeListener> listeners = this.listeners.get( pluginType ); if ( listeners != null ) { for ( PluginTypeListener listener : listeners ) { listener.pluginRemoved( plugin ); } } synchronized ( this ) { notifyAll(); } } } public void addParentClassLoaderPatterns( PluginInterface plugin, String[] patterns ) { lock.writeLock().lock(); try { parentClassloaderPatternMap.put( plugin, patterns ); } finally { lock.writeLock().unlock(); } } public void registerPlugin( Class<? extends PluginTypeInterface> pluginType, PluginInterface plugin ) throws KettlePluginException { boolean changed = false; // Is this an add or an update? lock.writeLock().lock(); try { if ( plugin.getIds()[0] == null ) { throw new KettlePluginException( "Not a valid id specified in plugin :" + plugin ); } List<PluginInterface> list = pluginMap.get( pluginType ); if ( list == null ) { list = new ArrayList<PluginInterface>(); pluginMap.put( pluginType, list ); } int index = list.indexOf( plugin ); if ( index < 0 ) { list.add( plugin ); } else { list.set( index, plugin ); // replace with the new one changed = true; } // Keep the list of plugins sorted by name... // Collections.sort( list, new Comparator<PluginInterface>() { @Override public int compare( PluginInterface p1, PluginInterface p2 ) { return p1.getName().compareToIgnoreCase( p2.getName() ); } } ); if ( !Utils.isEmpty( plugin.getCategory() ) ) { List<String> categories = categoryMap.get( pluginType ); if ( categories == null ) { categories = new ArrayList<String>(); categoryMap.put( pluginType, categories ); } if ( !categories.contains( plugin.getCategory() ) ) { categories.add( plugin.getCategory() ); // Keep it sorted in the natural order here too! // // Sort the categories in the correct order. // String[] naturalOrder = null; PluginTypeCategoriesOrder naturalOrderAnnotation = pluginType.getAnnotation( PluginTypeCategoriesOrder.class ); if ( naturalOrderAnnotation != null ) { String[] naturalOrderKeys = naturalOrderAnnotation.getNaturalCategoriesOrder(); Class<?> i18nClass = naturalOrderAnnotation.i18nPackageClass(); naturalOrder = new String[naturalOrderKeys.length]; for ( int i = 0; i < naturalOrderKeys.length; i++ ) { naturalOrder[i] = BaseMessages.getString( i18nClass, naturalOrderKeys[i] ); } } if ( naturalOrder != null ) { final String[] fNaturalOrder = naturalOrder; Collections.sort( categories, new Comparator<String>() { @Override public int compare( String one, String two ) { int idx1 = Const.indexOfString( one, fNaturalOrder ); int idx2 = Const.indexOfString( two, fNaturalOrder ); return idx1 - idx2; } } ); } } } } finally { lock.writeLock().unlock(); List<PluginTypeListener> listeners = this.listeners.get( pluginType ); if ( listeners != null ) { for ( PluginTypeListener listener : listeners ) { // Changed or added? if ( changed ) { listener.pluginChanged( plugin ); } else { listener.pluginAdded( plugin ); } } } synchronized ( this ) { notifyAll(); } } } /** * @return An unmodifiable list of plugin types */ public List<Class<? extends PluginTypeInterface>> getPluginTypes() { lock.readLock().lock(); try { return Collections .unmodifiableList( new ArrayList<Class<? extends PluginTypeInterface>>( pluginMap.keySet() ) ); } finally { lock.readLock().unlock(); } } /** * @param type The plugin type to query * @return The list of plugins */ @SuppressWarnings( "unchecked" ) public <T extends PluginInterface, K extends PluginTypeInterface> List<T> getPlugins( Class<K> type ) { Set<T> set = new HashSet<T>(); lock.readLock().lock(); try { for ( Class<? extends PluginTypeInterface> pi : pluginMap.keySet() ) { if ( Const.classIsOrExtends( pi, type ) ) { List<PluginInterface> mapList = pluginMap.get( pi ); if ( mapList != null ) { for ( PluginInterface p : mapList ) { T t = (T) p; set.add( t ); } } } } } finally { lock.readLock().unlock(); } return new ArrayList<T>( set ); } /** * Get a plugin from the registry * * @param stepplugintype The type of plugin to look for * @param id The ID to scan for * @return the plugin or null if nothing was found. */ public PluginInterface getPlugin( Class<? extends PluginTypeInterface> pluginType, String id ) { if ( Utils.isEmpty( id ) ) { return null; } // getPlugins() never returns null, see his method above for ( PluginInterface plugin : getPlugins( pluginType ) ) { if ( plugin.matches( id ) ) { return plugin; } } return null; } /** * Retrieve a list of plugins per category. * * @param pluginType The type of plugins to search * @param pluginCategory The category to look in * @return An unmodifiable list of plugins that belong to the specified type and category. */ public <T extends PluginTypeInterface> List<PluginInterface> getPluginsByCategory( Class<T> pluginType, String pluginCategory ) { List<PluginInterface> plugins = new ArrayList<PluginInterface>(); for ( PluginInterface verify : getPlugins( pluginType ) ) { if ( verify.getCategory() != null && verify.getCategory().equals( pluginCategory ) ) { plugins.add( verify ); } } // Also sort return Collections.unmodifiableList( plugins ); } /** * Retrieve a list of all categories for a certain plugin type. * * @param pluginType The plugin type to search categories for. * @return The list of categories for this plugin type. The list can be modified (sorted etc) but will not impact the * registry in any way. */ public List<String> getCategories( Class<? extends PluginTypeInterface> pluginType ) { lock.readLock().lock(); try { return categoryMap.get( pluginType ); } finally { lock.readLock().unlock(); } } /** * Load and instantiate the main class of the plugin specified. * * @param plugin The plugin to load the main class for. * @return The instantiated class * @throws KettlePluginException In case there was a loading problem. */ public Object loadClass( PluginInterface plugin ) throws KettlePluginException { return loadClass( plugin, plugin.getMainType() ); } /** * Load the class of the type specified for the plugin that owns the class of the specified object. * * @param pluginType the type of plugin * @param object The object for which we want to search the class to find the plugin * @param classType The type of class to load * @return the instantiated class. * @throws KettlePluginException */ public <T> T loadClass( Class<? extends PluginTypeInterface> pluginType, Object object, Class<T> classType ) throws KettlePluginException { PluginInterface plugin = getPlugin( pluginType, object ); if ( plugin == null ) { return null; } return loadClass( plugin, classType ); } /** * Load the class of the type specified for the plugin with the ID specified. * * @param pluginType the type of plugin * @param plugiId The plugin id to use * @param classType The type of class to load * @return the instantiated class. * @throws KettlePluginException */ public <T> T loadClass( Class<? extends PluginTypeInterface> pluginType, String pluginId, Class<T> classType ) throws KettlePluginException { PluginInterface plugin = getPlugin( pluginType, pluginId ); if ( plugin == null ) { return null; } return loadClass( plugin, classType ); } private KettleURLClassLoader createClassLoader( PluginInterface plugin ) throws MalformedURLException, UnsupportedEncodingException { List<String> jarfiles = plugin.getLibraries(); URL[] urls = new URL[jarfiles.size()]; for ( int i = 0; i < jarfiles.size(); i++ ) { File jarfile = new File( jarfiles.get( i ) ); urls[i] = new URL( URLDecoder.decode( jarfile.toURI().toURL().toString(), "UTF-8" ) ); } ClassLoader classLoader = getClass().getClassLoader(); String[] patterns = parentClassloaderPatternMap.get( plugin ); if ( patterns != null ) { return new KettleSelectiveParentFirstClassLoader( urls, classLoader, plugin.getDescription(), patterns ); } else { return new KettleURLClassLoader( urls, classLoader, plugin.getDescription() ); } } /** * Add a Class Mapping + factory for a plugin. This allows extra classes to be added to existing plugins. * * @param pluginType Type of plugin * @param tClass Class to factory * @param id ID of the plugin to extend * @param callable Factory Callable * @param <T> Type of the object factoried * @throws KettlePluginException */ public <T> void addClassFactory( Class<? extends PluginTypeInterface> pluginType, Class<T> tClass, String id, Callable<T> callable ) throws KettlePluginException { String key = createSupplemantalKey( pluginType.getName(), id ); SupplementalPlugin supplementalPlugin = (SupplementalPlugin) getPlugin( pluginType, key ); if ( supplementalPlugin == null ) { supplementalPlugin = new SupplementalPlugin( pluginType, key ); registerPlugin( pluginType, supplementalPlugin ); } supplementalPlugin.addFactory( tClass, callable ); } private String createSupplemantalKey( String pluginName, String id ) { return pluginName + "-" + id + SUPPLEMENTALS_SUFFIX; } /** * Load and instantiate the plugin class specified * * @param plugin the plugin to load * @param pluginClass the class to be loaded * @return The instantiated class * @throws KettlePluginException In case there was a class loading problem somehow */ @SuppressWarnings( "unchecked" ) public <T> T loadClass( PluginInterface plugin, Class<T> pluginClass ) throws KettlePluginException { if ( plugin == null ) { throw new KettlePluginException( BaseMessages.getString( PKG, "PluginRegistry.RuntimeError.NoValidStepOrPlugin.PLUGINREGISTRY001" ) ); } if ( plugin instanceof ClassLoadingPluginInterface ) { return ( (ClassLoadingPluginInterface) plugin ).loadClass( pluginClass ); } else { String className = plugin.getClassMap().get( pluginClass ); if ( className == null ) { // Look for supplemental plugin supplying extra classes for ( String id : plugin.getIds() ) { try { T aClass = loadClass( plugin.getPluginType(), createSupplemantalKey( plugin.getPluginType().getName(), id ), pluginClass ); if ( aClass != null ) { return aClass; } } catch ( KettlePluginException exception ) { // ignore. we'll fall through to the other exception if this loop doesn't produce a return } } throw new KettlePluginClassMapException( BaseMessages.getString( PKG, "PluginRegistry.RuntimeError.NoValidClassRequested.PLUGINREGISTRY002", pluginClass.getName() ) ); } try { Class<? extends T> cl; if ( plugin.isNativePlugin() ) { cl = (Class<? extends T>) Class.forName( className ); } else { URLClassLoader ucl = null; // If the plugin needs to have a separate class loader for each instance of the plugin. // This is not the default. By default we cache the class loader for each plugin ID. // lock.writeLock().lock(); try { if ( plugin.isSeparateClassLoaderNeeded() ) { // Create a new one each time ucl = createClassLoader( plugin ); } else { // See if we can find a class loader to re-use. Map<PluginInterface, URLClassLoader> classLoaders = classLoaderMap.get( plugin.getPluginType() ); if ( classLoaders == null ) { classLoaders = new HashMap<PluginInterface, URLClassLoader>(); classLoaderMap.put( plugin.getPluginType(), classLoaders ); } else { ucl = classLoaders.get( plugin ); } if ( ucl == null ) { if ( plugin.getPluginDirectory() != null ) { ucl = folderBasedClassLoaderMap.get( plugin.getPluginDirectory().toString() ); if ( ucl == null ) { ucl = createClassLoader( plugin ); classLoaders.put( plugin, ucl ); // save for later use... folderBasedClassLoaderMap.put( plugin.getPluginDirectory().toString(), ucl ); } } else { ucl = classLoaders.get( plugin ); if ( ucl == null ) { ucl = createClassLoader( plugin ); classLoaders.put( plugin, ucl ); // save for later use... } } } } } finally { lock.writeLock().unlock(); } // Load the class. cl = (Class<? extends T>) ucl.loadClass( className ); } return cl.newInstance(); } catch ( ClassNotFoundException e ) { throw new KettlePluginException( BaseMessages.getString( PKG, "PluginRegistry.RuntimeError.ClassNotFound.PLUGINREGISTRY003" ), e ); } catch ( InstantiationException e ) { throw new KettlePluginException( BaseMessages.getString( PKG, "PluginRegistry.RuntimeError.UnableToInstantiateClass.PLUGINREGISTRY004" ), e ); } catch ( IllegalAccessException e ) { throw new KettlePluginException( BaseMessages.getString( PKG, "PluginRegistry.RuntimeError.IllegalAccessToClass.PLUGINREGISTRY005" ), e ); } catch ( MalformedURLException e ) { throw new KettlePluginException( BaseMessages.getString( PKG, "PluginRegistry.RuntimeError.MalformedURL.PLUGINREGISTRY006" ), e ); } catch ( Throwable e ) { e.printStackTrace(); throw new KettlePluginException( BaseMessages.getString( PKG, "PluginRegistry.RuntimeError.UnExpectedErrorLoadingClass.PLUGINREGISTRY007" ), e ); } } } /** * Add a PluginType to be managed by the registry * * @param type */ public static synchronized void addPluginType( PluginTypeInterface type ) { pluginTypes.add( type ); } /** * Added so we can tell when types have been added (but not necessarily registered) * * @return the list of added plugin types */ public static List<PluginTypeInterface> getAddedPluginTypes() { return Collections.unmodifiableList( pluginTypes ); } public static synchronized void init() throws KettlePluginException { init( false ); } /** * This method registers plugin types and loads their respective plugins * * @throws KettlePluginException */ public static synchronized void init( boolean keepCache ) throws KettlePluginException { final PluginRegistry registry = getInstance(); log.snap( Metrics.METRIC_PLUGIN_REGISTRY_REGISTER_EXTENSIONS_START ); // Find pluginRegistry extensions try { registry.registerType( PluginRegistryPluginType.getInstance() ); List<PluginInterface> plugins = registry.getPlugins( PluginRegistryPluginType.class ); for ( PluginInterface extensionPlugin : plugins ) { log.snap( Metrics.METRIC_PLUGIN_REGISTRY_REGISTER_EXTENSION_START, extensionPlugin.getName() ); PluginRegistryExtension extension = (PluginRegistryExtension) registry.loadClass( extensionPlugin ); extension.init( registry ); extensions.add( extension ); log.snap( Metrics.METRIC_PLUGIN_REGISTRY_REGISTER_EXTENSIONS_STOP, extensionPlugin.getName() ); } } catch ( KettlePluginException e ) { e.printStackTrace(); } log.snap( Metrics.METRIC_PLUGIN_REGISTRY_REGISTER_EXTENSIONS_STOP ); log.snap( Metrics.METRIC_PLUGIN_REGISTRY_PLUGIN_REGISTRATION_START ); for ( final PluginTypeInterface pluginType : pluginTypes ) { log.snap( Metrics.METRIC_PLUGIN_REGISTRY_PLUGIN_TYPE_REGISTRATION_START, pluginType.getName() ); registry.registerType( pluginType ); log.snap( Metrics.METRIC_PLUGIN_REGISTRY_PLUGIN_TYPE_REGISTRATION_STOP, pluginType.getName() ); } log.snap( Metrics.METRIC_PLUGIN_REGISTRY_PLUGIN_REGISTRATION_STOP ); /* * System.out.println(MetricsUtil.getDuration(log.getLogChannelId(), * Metrics.METRIC_PLUGIN_REGISTRY_REGISTER_EXTENSIONS_START.getDescription()).get(0)); * System.out.println(MetricsUtil.getDuration(log.getLogChannelId(), * Metrics.METRIC_PLUGIN_REGISTRY_PLUGIN_REGISTRATION_START.getDescription()).get(0)); long total=0; for * (MetricsDuration duration : MetricsUtil.getDuration(log.getLogChannelId(), * Metrics.METRIC_PLUGIN_REGISTRY_PLUGIN_TYPE_REGISTRATION_START.getDescription())) { total+=duration.getDuration(); * System.out.println(" - "+duration.toString()+" Total="+total); } */ // Clear the jar file cache so that we don't waste memory... // if ( !keepCache ) { JarFileCache.getInstance().clear(); } } private void registerType( PluginTypeInterface pluginType ) throws KettlePluginException { registerPluginType( pluginType.getClass() ); // Search plugins for this type... // long startScan = System.currentTimeMillis(); pluginType.searchPlugins(); for ( PluginRegistryExtension ext : extensions ) { ext.searchForType( pluginType ); } List<String> pluginClassNames = new ArrayList<String>(); // Scan for plugin classes to facilitate debugging etc. // String pluginClasses = EnvUtil.getSystemProperty( Const.KETTLE_PLUGIN_CLASSES ); if ( !Utils.isEmpty( pluginClasses ) ) { String[] classNames = pluginClasses.split( "," ); for ( String className : classNames ) { if ( !pluginClassNames.contains( className ) ) { pluginClassNames.add( className ); } } } for ( String className : pluginClassNames ) { try { // What annotation does the plugin type have? // PluginAnnotationType annotationType = pluginType.getClass().getAnnotation( PluginAnnotationType.class ); if ( annotationType != null ) { Class<? extends Annotation> annotationClass = annotationType.value(); Class<?> clazz = Class.forName( className ); Annotation annotation = clazz.getAnnotation( annotationClass ); if ( annotation != null ) { // Register this one! // pluginType.handlePluginAnnotation( clazz, annotation, new ArrayList<String>(), true, null ); LogChannel.GENERAL.logBasic( "Plugin class " + className + " registered for plugin type '" + pluginType.getName() + "'" ); } else { if ( KettleLogStore.isInitialized() && LogChannel.GENERAL.isDebug() ) { LogChannel.GENERAL.logDebug( "Plugin class " + className + " doesn't contain annotation for plugin type '" + pluginType.getName() + "'" ); } } } else { if ( KettleLogStore.isInitialized() && LogChannel.GENERAL.isDebug() ) { LogChannel.GENERAL.logDebug( "Plugin class " + className + " doesn't contain valid class for plugin type '" + pluginType.getName() + "'" ); } } } catch ( Exception e ) { if ( KettleLogStore.isInitialized() ) { LogChannel.GENERAL.logError( "Error registring plugin class from KETTLE_PLUGIN_CLASSES: " + className + Const.CR + Const.getStackTracker( e ) ); } } } if ( LogChannel.GENERAL.isDetailed() ) { LogChannel.GENERAL.logDetailed( "Registered " + getPlugins( pluginType.getClass() ).size() + " plugins of type '" + pluginType.getName() + "' in " + ( System.currentTimeMillis() - startScan ) + "ms." ); } } /** * Find the plugin ID based on the class * * @param pluginClass * @return The ID of the plugin to which this class belongs (checks the plugin class maps) */ public String getPluginId( Object pluginClass ) { for ( Class<? extends PluginTypeInterface> pluginType : getPluginTypes() ) { String id = getPluginId( pluginType, pluginClass ); if ( id != null ) { return id; } } return null; } /** * Find the plugin ID based on the class * * @param pluginType the type of plugin * @param pluginClass The class to look for * @return The ID of the plugin to which this class belongs (checks the plugin class maps) or null if nothing was * found. */ public String getPluginId( Class<? extends PluginTypeInterface> pluginType, Object pluginClass ) { String className = pluginClass.getClass().getName(); for ( PluginInterface plugin : getPlugins( pluginType ) ) { for ( String check : plugin.getClassMap().values() ) { if ( check != null && check.equals( className ) ) { return plugin.getIds()[0]; } } } for ( PluginRegistryExtension ext : extensions ) { String id = ext.getPluginId( pluginType, pluginClass ); if ( id != null ) { return id; } } return null; } /** * Retrieve the Plugin for a given class * * @param pluginType The type of plugin to search for * @param pluginClass The class of this object is used to look around * @return the plugin or null if nothing could be found */ public PluginInterface getPlugin( Class<? extends PluginTypeInterface> pluginType, Object pluginClass ) { String pluginId = getPluginId( pluginType, pluginClass ); if ( pluginId == null ) { return null; } return getPlugin( pluginType, pluginId ); } /** * Find the plugin ID based on the name of the plugin * * @param pluginType the type of plugin * @param pluginName The name to look for * @return The plugin with the specified name or null if nothing was found. */ public PluginInterface findPluginWithName( Class<? extends PluginTypeInterface> pluginType, String pluginName ) { for ( PluginInterface plugin : getPlugins( pluginType ) ) { if ( plugin.getName().equals( pluginName ) ) { return plugin; } } return null; } /** * Find the plugin ID based on the description of the plugin * * @param pluginType the type of plugin * @param pluginDescription The description to look for * @return The plugin with the specified description or null if nothing was found. */ public PluginInterface findPluginWithDescription( Class<? extends PluginTypeInterface> pluginType, String pluginDescription ) { for ( PluginInterface plugin : getPlugins( pluginType ) ) { if ( plugin.getDescription().equals( pluginDescription ) ) { return plugin; } } return null; } /** * Find the plugin ID based on the name of the plugin * * @param pluginType the type of plugin * @param pluginName The name to look for * @return The plugin with the specified name or null if nothing was found. */ public PluginInterface findPluginWithId( Class<? extends PluginTypeInterface> pluginType, String pluginId ) { for ( PluginInterface plugin : getPlugins( pluginType ) ) { if ( plugin.matches( pluginId ) ) { return plugin; } } return null; } /** * @return a unique list of all the step plugin package names */ public List<String> getPluginPackages( Class<? extends PluginTypeInterface> pluginType ) { List<String> list = new ArrayList<String>(); for ( PluginInterface plugin : getPlugins( pluginType ) ) { for ( String className : plugin.getClassMap().values() ) { int lastIndex = className.lastIndexOf( "." ); if ( lastIndex > -1 ) { String packageName = className.substring( 0, lastIndex ); if ( !list.contains( packageName ) ) { list.add( packageName ); } } } } Collections.sort( list ); return list; } private RowMetaInterface getPluginInformationRowMeta() { RowMetaInterface row = new RowMeta(); row.addValueMeta( new ValueMetaString( BaseMessages.getString( PKG, "PluginRegistry.Information.Type.Label" ) ) ); row.addValueMeta( new ValueMetaString( BaseMessages.getString( PKG, "PluginRegistry.Information.ID.Label" ) ) ); row.addValueMeta( new ValueMetaString( BaseMessages.getString( PKG, "PluginRegistry.Information.Name.Label" ) ) ); row.addValueMeta( new ValueMetaString( BaseMessages.getString( PKG, "PluginRegistry.Information.Description.Label" ) ) ); row.addValueMeta( new ValueMetaString( BaseMessages.getString( PKG, "PluginRegistry.Information.Libraries.Label" ) ) ); row.addValueMeta( new ValueMetaString( BaseMessages.getString( PKG, "PluginRegistry.Information.ImageFile.Label" ) ) ); row.addValueMeta( new ValueMetaString( BaseMessages.getString( PKG, "PluginRegistry.Information.ClassName.Label" ) ) ); row.addValueMeta( new ValueMetaString( BaseMessages.getString( PKG, "PluginRegistry.Information.Category.Label" ) ) ); return row; } /** * @param the type of plugin to get information for * @return a row buffer containing plugin information for the given plugin type * @throws KettlePluginException */ public RowBuffer getPluginInformation( Class<? extends PluginTypeInterface> pluginType ) throws KettlePluginException { RowBuffer rowBuffer = new RowBuffer( getPluginInformationRowMeta() ); for ( PluginInterface plugin : getPlugins( pluginType ) ) { Object[] row = new Object[getPluginInformationRowMeta().size()]; int rowIndex = 0; row[rowIndex++] = getPluginType( plugin.getPluginType() ).getName(); row[rowIndex++] = plugin.getIds()[0]; row[rowIndex++] = plugin.getName(); row[rowIndex++] = Const.NVL( plugin.getDescription(), "" ); row[rowIndex++] = Utils.isEmpty( plugin.getLibraries() ) ? "" : plugin.getLibraries().toString(); row[rowIndex++] = Const.NVL( plugin.getImageFile(), "" ); row[rowIndex++] = plugin.getClassMap().values().toString(); row[rowIndex++] = Const.NVL( plugin.getCategory(), "" ); rowBuffer.getBuffer().add( row ); } return rowBuffer; } /** * Load the class with a certain name using the class loader of certain plugin. * * @param plugin The plugin for which we want to use the class loader * @param className The name of the class to load * @return the name of the class * @throws KettlePluginException In case there is something wrong */ @SuppressWarnings( "unchecked" ) public <T> T getClass( PluginInterface plugin, String className ) throws KettlePluginException { try { if ( plugin.isNativePlugin() ) { return (T) Class.forName( className ); } else { URLClassLoader ucl = null; lock.writeLock().lock(); try { Map<PluginInterface, URLClassLoader> classLoaders = classLoaderMap.get( plugin.getPluginType() ); if ( classLoaders == null ) { classLoaders = new HashMap<PluginInterface, URLClassLoader>(); classLoaderMap.put( plugin.getPluginType(), classLoaders ); } else { ucl = classLoaders.get( plugin ); } if ( ucl == null ) { if ( plugin.getPluginDirectory() != null ) { ucl = folderBasedClassLoaderMap.get( plugin.getPluginDirectory().toString() ); classLoaders.put( plugin, ucl ); // save for later use... } } } finally { lock.writeLock().unlock(); } if ( ucl == null ) { throw new KettlePluginException( "Unable to find class loader for plugin: " + plugin ); } return (T) ucl.loadClass( className ); } } catch ( Exception e ) { throw new KettlePluginException( "Unexpected error loading class with name: " + className, e ); } } /** * Load the class with a certain name using the class loader of certain plugin. * * @param plugin The plugin for which we want to use the class loader * @param classType The type of class to load * @return the name of the class * @throws KettlePluginException In case there is something wrong */ @SuppressWarnings( "unchecked" ) public <T> T getClass( PluginInterface plugin, T classType ) throws KettlePluginException { String className = plugin.getClassMap().get( classType ); return (T) getClass( plugin, className ); } /** * Create or retrieve the class loader for the specified plugin * * @param plugin the plugin to use * @return The class loader * @throws KettlePluginException In case there was a problem * <p/> * TODO: remove the similar code in the loadClass() method above with a call to * getClassLoader(); */ public ClassLoader getClassLoader( PluginInterface plugin ) throws KettlePluginException { if ( plugin == null ) { throw new KettlePluginException( BaseMessages.getString( PKG, "PluginRegistry.RuntimeError.NoValidStepOrPlugin.PLUGINREGISTRY001" ) ); } try { if ( plugin.isNativePlugin() ) { return this.getClass().getClassLoader(); } else { URLClassLoader ucl = null; lock.writeLock().lock(); try { // If the plugin needs to have a separate class loader for each instance // of the plugin. // This is not the default. By default we cache the class loader for // each plugin ID. // if ( plugin.isSeparateClassLoaderNeeded() ) { // Create a new one each time ucl = createClassLoader( plugin ); } else { // See if we can find a class loader to re-use. Map<PluginInterface, URLClassLoader> classLoaders = classLoaderMap.get( plugin.getPluginType() ); if ( classLoaders == null ) { classLoaders = new HashMap<PluginInterface, URLClassLoader>(); classLoaderMap.put( plugin.getPluginType(), classLoaders ); } else { ucl = classLoaders.get( plugin ); } if ( ucl == null ) { if ( !Utils.isEmpty( plugin.getClassLoaderGroup() ) ) { ucl = classLoaderGroupsMap.get( plugin.getClassLoaderGroup() ); if ( ucl == null ) { ucl = createClassLoader( plugin ); classLoaders.put( plugin, ucl ); classLoaderGroupsMap.put( plugin.getClassLoaderGroup(), ucl ); } } else { if ( plugin.getPluginDirectory() != null ) { ucl = folderBasedClassLoaderMap.get( plugin.getPluginDirectory().toString() ); if ( ucl == null ) { ucl = createClassLoader( plugin ); classLoaders.put( plugin, ucl ); // save for later use... folderBasedClassLoaderMap.put( plugin.getPluginDirectory().toString(), ucl ); } } else { ucl = classLoaders.get( plugin ); if ( ucl == null ) { if ( plugin.getLibraries().size() == 0 ) { if ( plugin instanceof ClassLoadingPluginInterface ) { return ( (ClassLoadingPluginInterface) plugin ).getClassLoader(); } } ucl = createClassLoader( plugin ); classLoaders.put( plugin, ucl ); // save for later use... } } } } } } finally { lock.writeLock().unlock(); } // Load the class. return ucl; } } catch ( MalformedURLException e ) { throw new KettlePluginException( BaseMessages.getString( PKG, "PluginRegistry.RuntimeError.MalformedURL.PLUGINREGISTRY006" ), e ); } catch ( Throwable e ) { e.printStackTrace(); throw new KettlePluginException( BaseMessages.getString( PKG, "PluginRegistry.RuntimeError.UnExpectedCreatingClassLoader.PLUGINREGISTRY008" ), e ); } } /** * Allows the tracking of plugins as they come and go. * * @param typeToTrack extension of PluginTypeInterface to track. * @param listener receives notification when a plugin of the specified type is added/removed/modified * @param <T> extension of PluginTypeInterface */ public <T extends PluginTypeInterface> void addPluginListener( Class<T> typeToTrack, PluginTypeListener listener ) { lock.writeLock().lock(); try { List<PluginTypeListener> list = listeners.get( typeToTrack ); if ( list == null ) { list = new ArrayList<PluginTypeListener>(); listeners.put( typeToTrack, list ); } if ( !list.contains( listener ) ) { list.add( listener ); } } finally { lock.writeLock().unlock(); } } public void addClassLoader( URLClassLoader ucl, PluginInterface plugin ) { lock.writeLock().lock(); try { Map<PluginInterface, URLClassLoader> classLoaders = classLoaderMap.get( plugin.getPluginType() ); if ( classLoaders == null ) { classLoaders = new HashMap<PluginInterface, URLClassLoader>(); classLoaderMap.put( plugin.getPluginType(), classLoaders ); } classLoaders.put( plugin, ucl ); } finally { lock.writeLock().unlock(); } } public PluginTypeInterface getPluginType( Class<? extends PluginTypeInterface> pluginTypeClass ) throws KettlePluginException { try { // All these plugin type interfaces are singletons... // So we should call a static getInstance() method... // Method method = pluginTypeClass.getMethod( "getInstance", new Class<?>[0] ); PluginTypeInterface pluginTypeInterface = (PluginTypeInterface) method.invoke( null, new Object[0] ); return pluginTypeInterface; } catch ( Exception e ) { throw new KettlePluginException( "Unable to get instance of plugin type: " + pluginTypeClass.getName(), e ); } } public List<PluginInterface> findPluginsByFolder( URL folder ) { String path = folder.getPath(); try { path = folder.toURI().normalize().getPath(); } catch ( URISyntaxException e ) { log.logError( e.getLocalizedMessage(), e ); } if ( path.endsWith( "/" ) ) { path = path.substring( 0, path.length() - 1 ); } List<PluginInterface> result = new ArrayList<PluginInterface>(); lock.readLock().lock(); try { for ( List<PluginInterface> typeInterfaces : pluginMap.values() ) { for ( PluginInterface plugin : typeInterfaces ) { URL pluginFolder = plugin.getPluginDirectory(); try { if ( pluginFolder != null && pluginFolder.toURI().normalize().getPath().startsWith( path ) ) { result.add( plugin ); } } catch ( URISyntaxException e ) { log.logError( e.getLocalizedMessage(), e ); } } } } finally { lock.readLock().unlock(); } return result; } }