/*
This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2010 Servoy BV
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation; either version 3 of the License, or (at your option) any
later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along
with this program; if not, see http://www.gnu.org/licenses or write to the Free
Software Foundation,Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
*/
package com.servoy.j2db.plugins;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.CodeSource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
import com.servoy.j2db.IApplication;
import com.servoy.j2db.J2DBGlobals;
import com.servoy.j2db.dataprocessing.IBaseConverter;
import com.servoy.j2db.dataprocessing.IColumnConverter;
import com.servoy.j2db.dataprocessing.IColumnValidator;
import com.servoy.j2db.dataprocessing.IColumnValidatorManager;
import com.servoy.j2db.dataprocessing.IConverterManager;
import com.servoy.j2db.dataprocessing.IUIConverter;
import com.servoy.j2db.persistence.NameComparator;
import com.servoy.j2db.util.Debug;
import com.servoy.j2db.util.ExtendableURLClassLoader;
import com.servoy.j2db.util.JarManager;
import com.servoy.j2db.util.Settings;
import com.servoy.j2db.util.keyword.Ident;
/**
* Manages plugins for the application.
*
* @author jcompagner, jblok
*/
@SuppressWarnings("nls")
public class PluginManager extends JarManager implements IPluginManagerInternal, PropertyChangeListener
{
private static final String PLUGIN_CL_SUFFIX = " plugins"; //$NON-NLS-1$
private final File pluginsDir;
protected static ExtendableURLClassLoader _pluginsClassLoader;
protected final static List<ExtensionResource> supportLibExtensions = new ArrayList<ExtensionResource>();
protected final static List<ExtensionResource> pluginExtensions = new ArrayList<ExtensionResource>();
protected static List<Extension<IClientPlugin>> clientPluginExtensions; //subset from pluginExtensions
// ---instance vars
protected final Object[] initLock = new Object[1];//when filled with an Object the init is completed
// ---plugin instances
protected Map<String, IClientPlugin> loadedClientPlugins; //contains all instances of client plugins, (name -> instance)
protected List<IServerPlugin> loadedServerPlugins;//contains all instances of server plugins
private final ClassLoader parentClassLoader;
/**
* Loads plugins from the plugins directory.
*/
public PluginManager(Object prop_change_source, ClassLoader lafLoader)
{
this(lafLoader);
J2DBGlobals.addPropertyChangeListener(prop_change_source, this);
}
public PluginManager(ClassLoader lafLoader)
{
this(Settings.getInstance().getProperty(J2DBGlobals.SERVOY_APPLICATION_SERVER_DIRECTORY_KEY) + File.separator + "plugins", lafLoader); //$NON-NLS-1$
}
public PluginManager(String pluginDirAsString, ClassLoader lafLoader)
{
super();
this.parentClassLoader = lafLoader;
pluginsDir = new File(pluginDirAsString);
if (pluginExtensions.size() == 0 && this.pluginsDir.isDirectory())
{
readDir(pluginsDir, pluginExtensions, supportLibExtensions, null, false);
}
}
public PluginManager(List<ExtensionResource> pluginUrls, List<ExtensionResource> supportLibUrls, ClassLoader lafLoader)
{
super();
this.parentClassLoader = lafLoader;
pluginsDir = null;
PluginManager.pluginExtensions.addAll(pluginUrls);
PluginManager.supportLibExtensions.addAll(supportLibUrls);
List<URL> allUrls = new ArrayList<URL>(supportLibUrls.size() + pluginUrls.size());
for (ExtensionResource ext : supportLibUrls)
{
allUrls.add(ext.jarUrl);
}
for (ExtensionResource ext : pluginUrls)
{
allUrls.add(ext.jarUrl);
}
URL[] urls = allUrls.toArray(new URL[allUrls.size()]);
PluginManager._pluginsClassLoader = new ExtendableURLClassLoader(urls, lafLoader != null ? lafLoader : getClass().getClassLoader(), PLUGIN_CL_SUFFIX);
}
public File getPluginsDir()
{
return pluginsDir;
}
/**
* Unload all plugins.
*/
public synchronized void flushCachedItems()
{
if (loadedClientPlugins != null)
{
Object[] list = loadedClientPlugins.values().toArray();
for (Object element : list)
{
IClientPlugin plugin = (IClientPlugin)element;
try
{
plugin.unload();
}
catch (Throwable th)
{
Debug.error("Error occured unloading client plugin: " + plugin.getName(), th); //$NON-NLS-1$
}
}
loadedClientPlugins = null;
}
if (loadedServerPlugins != null)
{
for (int j = 0; j < loadedServerPlugins.size(); j++)
{
IPlugin plugin = loadedServerPlugins.get(j);
try
{
plugin.unload();
}
catch (Throwable th)
{
Debug.error("Error ocured unloading server plugin: " + plugin.getClass().getName(), th); //$NON-NLS-1$
}
}
loadedServerPlugins = null;
}
}
/**
* NOTE: To be called only from ApplicationServer.dispose()
*/
public static void dispose()
{
pluginExtensions.clear();
supportLibExtensions.clear();
if (_pluginsClassLoader != null)
{
//present in java 7
try
{
Method closeMethod = URLClassLoader.class.getMethod("close", null);
closeMethod.invoke(_pluginsClassLoader, null);
}
catch (Exception ex)
{
// ignore
}
}
_pluginsClassLoader = null;
clientPluginExtensions = null;
}
public Extension<IClientPlugin>[] loadClientPluginDefs()
{
if (clientPluginExtensions == null)
{
clientPluginExtensions = getExtensionsForClass(IClientPlugin.class);
}
return clientPluginExtensions.toArray(new Extension[clientPluginExtensions.size()]);
}
private <T> List<Extension<T>> getExtensionsForClass(Class<T> searchClass)
{
List<ExtensionResource> notProcessedMap = new ArrayList<ExtensionResource>(pluginExtensions);
List<Extension<T>> extensions = new ArrayList<Extension<T>>();
ServiceLoader<IPlugin> pluginsLoader = ServiceLoader.load(IPlugin.class, getClassLoader());
Iterator<IPlugin> it = pluginsLoader.iterator();
while (it.hasNext())
{
try
{
IPlugin plugin = it.next();
CodeSource codeSource = plugin.getClass().getProtectionDomain().getCodeSource();
if (codeSource != null)
{
URL pluginURL = codeSource.getLocation();
if (pluginURL != null)
{
boolean found = false;
for (ExtensionResource ext : pluginExtensions)
{
if (pluginURL.equals(ext.jarUrl))
{
found = true;
notProcessedMap.remove(ext);
if (searchClass.isAssignableFrom(plugin.getClass()))
{
extensions.add(new Extension(ext, plugin.getClass(), searchClass));
}
break;
}
}
if (!found)
{
Debug.warn("Cannot find the jar URL among plugins: " + pluginURL);
}
}
else
{
Debug.warn("Cannot find the jar URL for loaded plugin: " + plugin.getClass());
}
}
else
{
Debug.warn("Cannot find the jar for loaded plugin: " + plugin.getClass());
}
}
catch (ServiceConfigurationError e) // can be thrown by iterator.next() in case of malformed manifests or other reasons that make instantiating a service fail
{
Debug.error("Cannot use a plugin contributed as a service:", e);
}
}
if (notProcessedMap.size() > 0)
{
Extension<T>[] additionalExtensions = getExtensions((ExtendableURLClassLoader)getClassLoader(), searchClass, notProcessedMap);
if (additionalExtensions != null)
{
extensions.addAll(Arrays.asList(additionalExtensions));
}
}
return extensions;
}
protected void checkIfInitialized()
{
synchronized (initLock)
{
if (initLock[0] == null)
{
try
{
initLock.wait(5 * 1000);//max 5 sec, if not copleted then something else is wrong
}
catch (InterruptedException e)
{
Debug.error(e);
}
}
}
}
private void flagInitialized()
{
initLock[0] = new Object();//done
initLock.notifyAll();
}
/**
* Note: load clients first
*
* @return
*/
private Class<IServerPlugin>[] loadServerPluginDefs()
{
try
{
List<Extension<IServerPlugin>> serverPlugins = getExtensionsForClass(IServerPlugin.class);
List<Class<IServerPlugin>> classes = new ArrayList<Class<IServerPlugin>>();
for (Extension<IServerPlugin> element : serverPlugins)
{
classes.add(element.instanceClass);
}
return classes.toArray(new Class[0]);
}
catch (Throwable th)
{
Debug.error("Error occured retrieving server plugins", th); //$NON-NLS-1$
return null;
}
}
public void init()
{
//ignore
}
//should only be called by app server
public void initServerPlugins(IServerAccess app)
{
synchronized (initLock)
{
if (loadedServerPlugins == null)
{
loadedServerPlugins = new ArrayList<IServerPlugin>();
Class<IServerPlugin>[] classes = loadServerPluginDefs();
for (Class<IServerPlugin> element : classes)
{
try
{
IServerPlugin plugin = loadServerPlugin(element);
if (plugin != null)
{
long now = System.currentTimeMillis();
plugin.initialize(app);
Debug.trace("Plugin " + element.getName() + " initialised in " + (System.currentTimeMillis() - now) + " ms."); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
loadedServerPlugins.add(plugin);
}
}
catch (Throwable th)
{
Debug.error("Error occured loading server plugin class " + element.getName(), th); //$NON-NLS-1$
}
}
flagInitialized();
}
}
}
/**
* Initialize plugins.
*/
public void initClientPlugins(IApplication application, IClientPluginAccess app)
{
synchronized (initLock)
{
loadClientPlugins(application);
initClientPluginsEx(app);
}
}
/**
* Only load client plugins; do not initialize them.
*/
public void loadClientPlugins(IApplication application)
{
synchronized (initLock)
{
loadedClientPlugins = new HashMap<String, IClientPlugin>();
if (application == null || !application.isRunningRemote()) //Servoy developer loading
{
Extension<IClientPlugin>[] exts = loadClientPluginDefs();
if (exts != null)
{
for (Extension<IClientPlugin> element : exts)
{
try
{
loadClientPlugin(element.instanceClass);
}
catch (Throwable th)
{
Debug.error("Error occured loading client plugin class " + element.instanceClass.getName(), th); //$NON-NLS-1$
}
}
}
}
else
//Servoy client loading
{
int count = 0;
Settings settings = Settings.getInstance();
String sCount = settings.getProperty("com.servoy.j2db.plugins.PluginManager.PluginCount"); //$NON-NLS-1$
if (sCount != null)
{
count = new Integer(sCount).intValue();
}
Debug.trace("plugin count " + count); //$NON-NLS-1$
for (int x = 0; x < count; x++)
{
String clazzName = settings.getProperty("com.servoy.j2db.plugins.PluginManager.Plugin." + x + ".className"); //$NON-NLS-1$ //$NON-NLS-2$
if (clazzName != null && clazzName.length() != 0)
{
try
{
loadClientPlugin((Class<IClientPlugin>)Class.forName(clazzName.trim()));
}
catch (Throwable th)
{
Debug.error("Error occured loading client plugin class " + clazzName, th); //$NON-NLS-1$
}
}
}
}
columnConverterManager = new ConverterManager<IColumnConverter>();
uiConverterManager = new ConverterManager<IUIConverter>();
columnValidatorManager = new ColumnValidatorManager();
checkAllPluginsForConvertersAndValidators();
}
}
private ConverterManager<IColumnConverter> columnConverterManager;
private ConverterManager<IUIConverter> uiConverterManager;
private IColumnValidatorManager columnValidatorManager;
public static class ConverterManager<T extends IBaseConverter> implements IConverterManager<T>
{
private final Map<String, T> converters = new HashMap<String, T>();//name -> converter
public T getConverter(String name)
{
return converters.get(name);
}
public void registerConvertor(T converter)
{
Object obj = converters.put(converter.getName(), converter);
if (obj != null) Debug.log("Duplicate converter found: " + converter.getName()); //$NON-NLS-1$
}
public Map<String, T> getConverters()
{
return converters;
}
}
public static class ColumnValidatorManager implements IColumnValidatorManager
{
private final Map<String, IColumnValidator> validators = new HashMap<String, IColumnValidator>();//name -> validator
public void registerValidator(IColumnValidator validator)
{
Object obj = validators.put(validator.getName(), validator);
if (obj != null) Debug.log("Duplicate validator found: " + validator.getName()); //$NON-NLS-1$
}
public Map<String, IColumnValidator> getValidators()
{
return validators;
}
public IColumnValidator getValidator(String name)
{
return validators.get(name);
}
}
public IConverterManager<IColumnConverter> getColumnConverterManager()
{
return columnConverterManager;
}
public IConverterManager<IUIConverter> getUIConverterManager()
{
return uiConverterManager;
}
public IColumnValidatorManager getColumnValidatorManager()
{
return columnValidatorManager;
}
private void initClientPluginsEx(IClientPluginAccess app)
{
synchronized (initLock)
{
// we assume that the plugins are already loaded
Object[] list = loadedClientPlugins.values().toArray();
for (Object element : list)
{
IClientPlugin plugin = (IClientPlugin)element;
try
{
long now = System.currentTimeMillis();
plugin.initialize(app);
Debug.trace("Plugin " + plugin.getName() + " initialised in " + (System.currentTimeMillis() - now) + " ms."); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$
}
catch (Throwable th)
{
Debug.error("Error ocured initializing plugin: " + plugin.getName(), th); //$NON-NLS-1$
}
}
flagInitialized();
}
}
private void checkAllPluginsForConvertersAndValidators()
{
synchronized (initLock)
{
// we assume that the plugins are already loaded
Collection<IClientPlugin> list = loadedClientPlugins.values();
for (IClientPlugin plugin : list)
{
checkPluginForConvertersAndValidators(plugin);
}
}
}
protected void checkPluginForConvertersAndValidators(IClientPlugin plugin)
{
try
{
long now = System.currentTimeMillis();
if (plugin instanceof IColumnConverterProvider && columnConverterManager != null)
{
IColumnConverter[] cons = ((IColumnConverterProvider)plugin).getColumnConverters();
if (cons != null)
{
for (IColumnConverter element2 : cons)
{
if (element2 != null)
{
columnConverterManager.registerConvertor(element2);
}
}
}
}
if (plugin instanceof IUIConverterProvider && uiConverterManager != null)
{
IUIConverter[] cons = ((IUIConverterProvider)plugin).getUIConverters();
if (cons != null)
{
for (IUIConverter element2 : cons)
{
if (element2 != null)
{
uiConverterManager.registerConvertor(element2);
}
}
}
}
if (plugin instanceof IColumnValidatorProvider && columnValidatorManager != null)
{
IColumnValidator[] vals = ((IColumnValidatorProvider)plugin).getColumnValidators();
if (vals != null)
{
for (IColumnValidator element2 : vals)
{
if (element2 != null)
{
columnValidatorManager.registerValidator(element2);
}
}
}
}
Debug.trace("Plugin " + plugin.getName() + " checked for converter/validator in " + (System.currentTimeMillis() - now) + " ms."); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$
}
catch (Throwable th)
{
Debug.error("Error ocured checking plugin: " + plugin.getName(), th); //$NON-NLS-1$
}
}
protected IClientPlugin loadClientPlugin(Class<IClientPlugin> pluginClass)
{
try
{
long now = System.currentTimeMillis();
IClientPlugin plugin = pluginClass.newInstance();
if (validatePlugin(plugin))
{
plugin.load();
String name = plugin.getName();
if (!Ident.checkIfKeyword(name))
{
loadedClientPlugins.put(name, plugin);
Debug.trace("Plugin " + name + " loaded in " + (System.currentTimeMillis() - now) + " ms."); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
return plugin;
}
else
{
Debug.error("Error occured loading client plugin, name is reserved word " + name); //$NON-NLS-1$
}
}
}
catch (Throwable th)
{
Debug.error("Error occured loading client class " + pluginClass.getName() + " from plugin.", th); //$NON-NLS-1$ //$NON-NLS-2$
}
return null;
}
private IServerPlugin loadServerPlugin(Class<IServerPlugin> pluginClass)
{
try
{
long now = System.currentTimeMillis();
IServerPlugin plugin = pluginClass.newInstance();
plugin.load();
Debug.trace("Plugin " + pluginClass.getName() + " loaded in " + (System.currentTimeMillis() - now) + " ms."); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
return plugin;
}
catch (Throwable th)
{
Debug.error("Error occured loading server class " + pluginClass.getName() + " from plugin", th); //$NON-NLS-1$ //$NON-NLS-2$
}
return null;
}
public IClientPlugin getClientPlugin(String name)
{
checkIfInitialized();
synchronized (initLock)
{
return loadedClientPlugins.get(name);
}
}
private List< ? extends IPlugin> getClientPlugins()
{
checkIfInitialized();
synchronized (initLock)
{
IClientPlugin[] list = loadedClientPlugins.values().toArray(new IClientPlugin[loadedClientPlugins.size()]);
Arrays.sort(list, NameComparator.INSTANCE);
return Arrays.asList(list);
}
}
public List<IServerPlugin> getServerPlugins()
{
checkIfInitialized();
synchronized (initLock)
{
return loadedServerPlugins;
}
}
private boolean validatePlugin(IClientPlugin plugin)
{
String name = plugin.getName();
if (name == null || name.trim().length() == 0)
{
Debug.error("Plugin " + plugin.getClass().getName() + " doesn't return a valid getName()"); //$NON-NLS-1$ //$NON-NLS-2$
return false;
}
if (loadedClientPlugins.get(name) != null)
{
Debug.error("A Plugin with the internal name " + name + " has already been loaded"); //$NON-NLS-1$ //$NON-NLS-2$
return false;
}
return true;
}
/*
* (non-Javadoc)
*
* @see java.beans.PropertyChangeListener#propertyChange(java.beans.PropertyChangeEvent)
*/
public void propertyChange(PropertyChangeEvent evt)
{
synchronized (initLock)
{
if (IPlugin.PROPERTY_SOLUTION.equals(evt.getPropertyName()) || IPlugin.PROPERTY_LOCALE.equals(evt.getPropertyName()) ||
IPlugin.PROPERTY_CURRENT_WINDOW.equals(evt.getPropertyName()))
{
if (loadedClientPlugins != null)
{
Object[] list = loadedClientPlugins.values().toArray();
for (Object element : list)
{
IClientPlugin plugin = (IClientPlugin)element;
try
{
plugin.propertyChange(evt);
}
catch (Throwable e)//incase method is missing in old plugin or the plugin designer did something stupid
{
Debug.error("Error occured informing client plugin " + plugin.getName(), e); //$NON-NLS-1$
}
}
}
}
}
}
/**
* Get the classloader (normally system classloader).
*
* @return ClassLoader
*/
public synchronized ClassLoader getClassLoader()
{
if (_pluginsClassLoader == null)
{
try
{
if (pluginExtensions.size() == 0 && pluginsDir.isDirectory())
{
readDir(pluginsDir, pluginExtensions, supportLibExtensions, null, false);
}
List<URL> allUrls = new ArrayList<URL>(supportLibExtensions.size() + pluginExtensions.size());
for (ExtensionResource ext : supportLibExtensions)
{
allUrls.add(ext.jarUrl);
}
for (ExtensionResource ext : pluginExtensions)
{
allUrls.add(ext.jarUrl);
}
URL[] urls = allUrls.toArray(new URL[allUrls.size()]);
_pluginsClassLoader = new ExtendableURLClassLoader(urls, parentClassLoader != null ? parentClassLoader : getClass().getClassLoader(),
PLUGIN_CL_SUFFIX);
}
catch (Throwable th)
{
Debug.error("Error occured retrieving plugins. No plugins have been loaded", th); //$NON-NLS-1$
return null;
}
}
return _pluginsClassLoader;
}
@Override
public PluginManager createEfficientCopy(Object prop_change_source)
{
PluginManager retval = new PluginManager(prop_change_source, parentClassLoader);
return retval;
}
@Override
public <T extends IPlugin> T getPlugin(Class<T> pluginSubType, String name)
{
if (IClientPlugin.class.equals(pluginSubType))
{
return (T)getClientPlugin(name);
}
return null;
}
@Override
public <T extends IPlugin> List<T> getPlugins(Class<T> pluginSubType)
{
if (IClientPlugin.class.equals(pluginSubType))
{
return (List<T>)getClientPlugins();
}
if (IServerPlugin.class.equals(pluginSubType))
{
return (List<T>)getServerPlugins();
}
return null;
}
@Override
public void addClientExtension(String clientPluginClassName, URL extension, URL[] supportLibs) throws PluginException
{
//implemented by subclasses
}
}