/*
This file is part of leafdigital leafChat.
leafChat is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
leafChat 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with leafChat. If not, see <http://www.gnu.org/licenses/>.
Copyright 2012 Samuel Marshall.
*/
package leafchat.core;
import java.io.*;
import java.util.*;
import java.util.jar.*;
import util.*;
import util.xml.XMLException;
import leafchat.core.api.*;
import leafchat.startup.*;
/** Class handles all the plugins */
public class PluginManager implements APIClassLocator,MsgOwner,PluginList
{
// Handle singleton behaviour (note: as this is not accessible to plugins,
// it doesn't use the SingletonManager features)
/** Single instance */
private static PluginManager pm=new PluginManager();
/** @return Singleton instance */
public static PluginManager get() { return pm; }
/** Private constructor to prevent separate construction */
private PluginManager()
{
MessageManager.get().registerOwner(this);
}
// Actual implementation
/** Map of name -> PluginClassLoader for all API classes */
private Map<String, PluginClassLoader> apiClasses =
new HashMap<String, PluginClassLoader>();
/** Set of PluginClassLoader */
private Set<PluginClassLoader> loadedJars =
new TreeSet<PluginClassLoader>(); // Sorted for nice display in lists
/** List of PluginContextProvider */
private List<PluginContextProvider> pluginList =
new LinkedList<PluginContextProvider>();
/** List of PluginClassLoader being loaded (cleared after init) */
private List<PluginClassLoader> loadingJar =
new LinkedList<PluginClassLoader>();
private LinkedList<PluginInfo> fakePluginInfos=null;
/**
* @param sName Classname
* @return The class
* @throws ClassNotFoundException If it isn't an existing API class
*/
@Override
public Class<?> findAPIClass(String sName) throws ClassNotFoundException
{
PluginClassLoader pcl;
synchronized(apiClasses)
{
pcl=apiClasses.get(sName);
if(pcl==null)
throw new ClassNotFoundException(sName);
}
return pcl.getAPIClass(sName);
}
/**
* Called from PluginClassLoader to register each API class
* @param pcl Classloader
* @param sClassName Provided classname
* @throws GeneralException If class of that name already exists
*/
void addAPIClass(PluginClassLoader pcl, String sClassName)
throws GeneralException
{
synchronized(apiClasses)
{
if(apiClasses.containsKey(sClassName))
throw new GeneralException("Defined class "+sClassName+" from "+pcl+
" already exists in "+apiClasses.get(sClassName));
apiClasses.put(sClassName,pcl);
}
}
/**
* Loads a new jar.
* @param f File to load
* @param sandbox True for sandbox (non-system) mode; at present this is not
* implemented in a security sense, but it still marks the plugin as not a
* system one
* @return New class loader
* @throws GeneralException
*/
private PluginClassLoader loadJar(File f, boolean sandbox) throws GeneralException
{
synchronized(loadedJars)
{
// Constructing the PCL is going to end up calling addAPIClass
try
{
PluginClassLoader pcl=new PluginClassLoader(this, f, sandbox);
loadedJars.add(pcl);
loadingJar.add(pcl);
return pcl;
}
catch(IOException e)
{
throw new GeneralException("Error loading plugin " + f.getAbsolutePath()
+ ": " + e.getMessage(), e);
}
}
}
@Override
public void unloadPluginFile(PluginInfo plugin) throws GeneralException
{
// Remove from list of loaded jars
PluginClassLoader found = null;
synchronized(loadedJars)
{
for(PluginClassLoader pcl : loadedJars)
{
if(pcl.getInfo()==plugin)
{
found = pcl;
break;
}
}
}
if(found == null)
{
throw new BugException("Couldn't find plugin for unload");
}
unloadPluginFile(found);
}
/**
* Unloads an existing jar.
* @param pcl Loader to unload
* @throws GeneralException Any error
*/
public void unloadPluginFile(PluginClassLoader pcl) throws GeneralException
{
// Remove from list of loaded jars
synchronized(loadedJars)
{
loadedJars.remove(pcl);
}
// Remove API classes
synchronized(apiClasses)
{
for(Iterator<Map.Entry<String, PluginClassLoader>> i =
apiClasses.entrySet().iterator(); i.hasNext();)
{
Map.Entry<String, PluginClassLoader> me = i.next();
if(me.getValue()==pcl)
{
i.remove();
}
}
}
synchronized(pluginList)
{
for(Iterator<PluginContextProvider> i=pluginList.iterator(); i.hasNext();)
{
PluginContextProvider pcp = i.next();
if(pcp.getPluginClassLoader()==pcl)
{
pcp.close();
i.remove();
}
}
}
MessageManager.get().clearCachedItems(pcl);
}
/**
* Load all .leafChatPlugin files from a folder
* @param fFolder Folder
* @param bSecure True if security restrictions apply
* @param plr Methods of this reporter will be called to inform progress
*/
private void loadFolder(File fFolder,boolean bSecure,PluginLoadReporter plr)
{
if(!fFolder.isDirectory()) return;
File[] af=fFolder.listFiles();
if(af==null) return;
for (int i= 0; i < af.length; i++)
{
if(!af[i].getName().endsWith(".jar")) continue;
try
{
plr.reportLoading(af[i]);
loadJar(af[i],bSecure);
}
catch(GeneralException ge)
{
plr.reportFailure(af[i],ge);
}
}
}
/**
* Call to initialise system by loading all plugins and creating them in
* appropriate dependency order.
* @param plr Methods of this reporter will be called to inform progress
*/
public void init(PluginLoadReporter plr)
{
// Load all jars
loadFolder(new File("./core"),false,plr);
loadFolder(new File("./plugins"),true,plr);
File userPlugins=new File(PlatformUtils.getUserFolder()+"/plugins");
userPlugins.mkdirs();
loadFolder(userPlugins,true,plr);
// Init plugins
initPlugins(null,plr);
}
/**
* Instantiate all loaded PluginInfos (in lLoadingPluginInfo), which
* afterwards will be cleared
* @param supportedApis Pre-existing supported APIs, or null
* @param plr Load reporter, may be null, if it is then a GeneralException
* will be returned in the event of error.
* @return GeneralException if error occurred and plr==null, otherwise null
*/
private GeneralException initPlugins(Set<String> supportedApis,
PluginLoadReporter plr)
{
if(supportedApis==null)
{
supportedApis=new HashSet<String>();
}
// Keep looping around until there are none left
while(true)
{
int iBefore=loadingJar.size();
for(Iterator<PluginClassLoader> i=loadingJar.iterator();i.hasNext();)
{
PluginClassLoader pcl = i.next();
APIDetails[] aapiDependencies=pcl.getInfo().getDependencies();
boolean bLater=false;
for (int iDependency= 0; iDependency < aapiDependencies.length; iDependency++)
{
if(!supportedApis.contains(aapiDependencies[iDependency].getRequiredString()))
{
bLater=true;
break;
}
}
if(!bLater)
{
try
{
// Remove pcl from list
i.remove();
// Create plugin
if(plr!=null) plr.reportInstantiating(pcl);
Plugin[] ap=pcl.createPlugins();
for(int iPlugin=0;iPlugin<ap.length;iPlugin++)
{
PluginContextProvider pcp=new PluginContextProvider(this,ap[iPlugin]);
synchronized(pluginList)
{
pluginList.add(pcp);
}
ap[iPlugin].init(pcp,plr);
}
// Track the APIs that are now supported
APIDetails[] aapiProvided=pcl.getInfo().getExports();
for (int iProvision= 0; iProvision < aapiProvided.length; iProvision++)
{
aapiProvided[iProvision].addSupportStrings(supportedApis);
}
}
catch(GeneralException ge)
{
if(plr!=null)
plr.reportFailure(pcl,ge);
else
return ge;
}
}
}
// If we loaded everything, stop
int iAfter=loadingJar.size();
if(iAfter==0) break;
// If there are files we can't load (dependency failures)
if(iAfter==iBefore)
{
for(PluginClassLoader pcl : loadingJar)
{
List<String> failedDependencies = new LinkedList<String>();
APIDetails[] aapiDependencies=pcl.getInfo().getDependencies();
for (int iDependency= 0; iDependency < aapiDependencies.length; iDependency++)
{
if(!supportedApis.contains(aapiDependencies[iDependency].getRequiredString()))
{
failedDependencies.add(aapiDependencies[iDependency].getRequiredString());
}
}
if(plr!=null)
{
plr.reportFailure(pcl,
failedDependencies.toArray(new String[0]));
}
else
{
StringBuffer sb=new StringBuffer("Plugin could not be instantiated " +
"because the following dependencies are not present:");
for(String dependency : failedDependencies)
{
sb.append(" "+ dependency);
}
return new GeneralException(sb.toString());
}
}
loadingJar.clear();
break;
}
}
return null;
}
/**
* Hack plugin load when actually they're all in system classpath to
* begin with (for IDEStartupHandler)
* @param p Plugin
* @param plr Load reporter
* @throws GeneralException
*/
public void fakePluginInit(Plugin p, PluginLoadReporter plr)
throws GeneralException
{
PluginContextProvider pcp=new PluginContextProvider(this,p);
synchronized(pluginList)
{
pluginList.add(pcp);
if(fakePluginInfos==null)
{
fakePluginInfos = new LinkedList<PluginInfo>();
}
try
{
fakePluginInfos.add(new PluginXMLDetails(
p.getClass().getResourceAsStream("plugininfo.xml"),
new File(StartupClassLoader.getIdeStartupTemplateApp().getPath() + "/core/"+
p.getClass().getPackage().getName().replaceAll("^.*\\.","")+".jar"),true));
}
catch(XMLException e)
{
throw new GeneralException(e);
}
}
p.init(pcp, plr);
}
/**
* Same as other method but with secure defaulting to false.
* @see #loadPluginFile(File, boolean)
* @param f Jar file
* @return Loaded plugin
* @throws GeneralException Any problems
*/
@Override
public PluginInfo loadPluginFile(File f) throws GeneralException
{
return loadPluginFile(f,true).getInfo();
}
/**
* Load a plugin that has been added since init
* @param f Jar file
* @param secure True if secure
* @return Loaded plugin
* @throws GeneralException Any problems
*/
public PluginClassLoader loadPluginFile(File f,boolean secure) throws GeneralException
{
PluginClassLoader pclThis=loadJar(f,secure);
// Get list of existing supported APIs
Set<String> supportedAPIs = new HashSet<String>();
PluginInfo[] info=getPluginList();
for(int i=0;i<info.length;i++)
{
PluginExport[] exports=info[i].getPluginExports();
for(int j=0;j<exports.length;j++)
{
((APIDetails)exports[j]).addSupportStrings(supportedAPIs);
}
}
GeneralException ge=initPlugins(supportedAPIs,null);
if(ge!=null) throw ge;
return pclThis;
}
/** Close all plugins in preparation for shutdown */
public void close()
{
PluginClassLoader[] apcl;
synchronized(loadedJars)
{
apcl=loadedJars.toArray(new PluginClassLoader[0]);
}
for(int i=apcl.length-1;i>=0;i--)
{
try
{
unloadPluginFile(apcl[i]);
}
catch (GeneralException ge)
{
try
{
(SingletonManager.get().get(SystemLog.class)).log(
this,"Unload failed for "+apcl[i],ge);
}
catch (BugException e)
{
}
}
}
// Are there still plugins? (This is caused by the fake plugin init.)
synchronized(pluginList)
{
PluginContextProvider[] plugins=pluginList.toArray(new PluginContextProvider[pluginList.size()]);
for(int i=plugins.length-1;i>=0;i--)
{
try
{
plugins[i].getPlugin().close();
}
catch(GeneralException ge)
{
try
{
(SingletonManager.get().get(SystemLog.class)).log(
this,"Close failed for "+plugins[i],ge);
}
catch (BugException e)
{
}
}
}
}
}
// Message dispatcher for plugin unload messages
void firePluginUnload(Plugin p)
{
mdp.dispatchMessage(new PluginUnloadMsg(p),true);
}
@Override
public boolean allowExternalDispatch(Msg m)
{
return false;
}
@Override
public String getFriendlyName()
{
return "Plugin unload notification";
}
@Override
public Class<? extends Msg> getMessageClass()
{
return PluginUnloadMsg.class;
}
private MessageDispatch mdp;
@Override
public void init(MessageDispatch mdp)
{
this.mdp=mdp;
}
@Override
public void manualDispatch(Msg m)
{
}
@Override
public boolean registerTarget(Object oTarget, Class<? extends Msg> cMessage,
MessageFilter mf,int iRequestID,int iPriority)
{
return true;
}
@Override
public void unregisterTarget(Object oTarget,int iRequestID)
{
}
@Override
public PluginInfo[] getPluginList()
{
List<PluginInfo> result = new LinkedList<PluginInfo>();
// Add real plugins
synchronized(loadedJars)
{
for(PluginClassLoader pcl : loadedJars)
{
result.add(pcl.getInfo());
}
}
// If we have any fake ones, add them too
if(fakePluginInfos!=null)
{
result.addAll(fakePluginInfos);
}
return result.toArray(new PluginInfo[result.size()]);
}
private PluginClassLoader findExportedPackage(String packageName)
{
for(PluginClassLoader pcl : loadedJars)
{
PluginExport[] exports=pcl.getInfo().getPluginExports();
for(int j=0;j<exports.length;j++)
{
if(exports[j].getPackage().equals(packageName))
return pcl;
}
}
return null;
}
@Override
public void saveAPIJar(String[] packages,File target) throws GeneralException
{
try
{
// If the packages list is null, build it from every plugin
if(packages==null)
{
LinkedList<String> l = new LinkedList<String>();
for(PluginClassLoader pcl : loadedJars)
{
PluginInfo pi = pcl.getInfo();
PluginExport[] exports=pi.getPluginExports();
for(int j=0;j<exports.length;j++)
{
l.add(exports[j].getPackage());
}
}
packages=l.toArray(new String[l.size()]);
}
// Open target file
JarOutputStream jos=new JarOutputStream(new FileOutputStream(target));
// Find system jar file (used to load this class)
File main = StartupClassLoader.getMainJar();
copyClasses(jos, main, null); // Do main API
File util = StartupClassLoader.getUtilJar();
copyClasses(jos, util, "util"); // Do util.*
// Copy each file into it
for(int i=0;i<packages.length;i++)
{
// Find the package
PluginClassLoader pcl=findExportedPackage(packages[i]);
if(pcl==null) throw new GeneralException("Couldn't find requested package: "+packages[i]);
copyClasses(jos,pcl.getJar(),packages[i].replace('.','/'));
}
// Close jar file
jos.close();
}
catch(IOException e)
{
throw new GeneralException("Error saving API jar",e);
}
}
@Override
public File getCoreJar()
{
throw new UnsupportedOperationException("Cannot use this method");
}
@Override
public File[] getCoreJars()
{
return StartupClassLoader.getMainJars();
}
/**
* Copies data from one jar file into another.
* @param jos Target jar stream
* @param source Source file
* @param path If null, includes all 'api' folders within file. Otherwise
* includes everything below specified path.
* @throws IOException Any disk error
*/
private void copyClasses(JarOutputStream jos,File source,String path)
throws IOException
{
JarFile jf=new JarFile(source);
for(Enumeration<JarEntry> e=jf.entries(); e.hasMoreElements();)
{
JarEntry entry=e.nextElement();
String name=entry.getName();
if( (path==null && name.indexOf("/api/")!=-1) ||
(path!=null && name.startsWith(path+"/")))
{
jos.putNextEntry(entry);
InputStream is=jf.getInputStream(entry);
IOUtils.copy(is,jos,false);
jos.closeEntry();
}
}
}
}