Java tutorial
/* * Copyright (c) 2007 Pentaho Corporation. All rights reserved. * This software was developed by Pentaho Corporation and is provided under the terms * of the GNU Lesser General Public License, Version 2.1. You may not use * this file except in compliance with the license. If you need a copy of the license, * please go to http://www.gnu.org/licenses/lgpl-2.1.txt. The Original Code is Pentaho * Data Integration. The Initial Developer is Pentaho Corporation. * * Software distributed under the GNU Lesser Public License is distributed on an "AS IS" * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. Please refer to * the license for the specific language governing your rights and limitations. */ package com.panet.imeta.core.plugins; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.lang.annotation.Annotation; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.Set; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.apache.commons.vfs.AllFileSelector; import org.apache.commons.vfs.FileObject; import org.apache.commons.vfs.FileSystemException; import org.apache.commons.vfs.FileSystemManager; import org.apache.commons.vfs.FileType; import org.apache.commons.vfs.VFS; import org.apache.commons.vfs.provider.jar.JarFileObject; import org.springframework.core.io.FileSystemResourceLoader; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.core.io.support.ResourcePatternResolver; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.xml.sax.SAXException; import com.panet.imeta.core.Const; import com.panet.imeta.core.PDIClassLoader; import com.panet.imeta.core.annotations.Job; import com.panet.imeta.core.annotations.Step; import com.panet.imeta.core.config.ConfigManager; import com.panet.imeta.core.config.KettleConfig; import com.panet.imeta.core.exception.KettleConfigException; import com.panet.imeta.core.logging.LogWriter; import com.panet.imeta.core.util.ResolverUtil; import com.panet.imeta.core.vfs.KettleVFS; import com.panet.imeta.core.xml.XMLHandler; import com.panet.imeta.job.JobPlugin; import com.panet.imeta.job.entry.JobEntryInterface; import com.panet.imeta.trans.StepPlugin; /** * This class handles all plugin loading steps for Kettle/PDI. It uses the * ConfigManager class to load <code>PluginConfig</code> objects, which * contain all the location from where the plugin should be loaded. * * Plugins are configured by modifying the kettle-plugins.xml file. * * @see com.panet.imeta.core.plugins.PluginLocation * @see com.panet.imeta.job.JobEntryLoader * * @author Alex Silva * */ @SuppressWarnings("unchecked") public class PluginLoader { private static final String WORK_DIR = "work"; //$NON-NLS-1$ public static final String DEFAULT_LIB = "lib/*.jar"; //$NON-NLS-1$ private static final String JAR = "jar"; //$NON-NLS-1$ // private static final Pattern patt = Pattern.compile("^*..(jar|zip)$");//$NON-NLS-1$ private static final LogWriter log = LogWriter.getInstance(); private final Set<PluginLocation> locs; private final Map<Class<? extends Annotation>, Set<? extends Plugin>> plugins; private final Map<Class<? extends Annotation>, ResolverUtil.Test> tests = new HashMap<Class<? extends Annotation>, ResolverUtil.Test>(); private static PluginLoader loader = new PluginLoader(); private PluginLoader() { tests.put(Job.class, new ResolverUtil.AnnotatedWith(Job.class)); tests.put(Step.class, new ResolverUtil.AnnotatedWith(Step.class)); locs = new HashSet<PluginLocation>(); plugins = new HashMap<Class<? extends Annotation>, Set<? extends Plugin>>();// HashSet<Plugin>(); // initialize map plugins.put(Job.class, new HashSet<JobPlugin>()); plugins.put(Step.class, new HashSet<StepPlugin>()); } public static PluginLoader getInstance() { return loader; } /** * Loads all plugins identified by the string passed. This method can be * called multiple times with different managers. * * @param mgr * The manager id, as defined in kettle-config.xml. * @throws KettleConfigException */ public void load(String mgr) throws KettleConfigException { ConfigManager<?> c = KettleConfig.getInstance().getManager(mgr); locs.addAll((Collection<PluginLocation>) c.loadAs(PluginLocation.class)); doConfig(); } /** * This method does the actual plugin configuration and should be called * after load() * * @return a collection containing the <code>JobPlugin</code> objects * loaded. * @throws KettleConfigException */ private void doConfig() throws KettleConfigException { synchronized (locs) { String sjar = "." + JAR; for (PluginLocation plugin : locs) { // check to see if the resource type is present File base = new File(System.getProperty("user.dir")); try { FileSystemManager mgr = VFS.getManager(); FileObject fobj = mgr.resolveFile(base, plugin.getLocation()); if (fobj.isReadable()) { String name = fobj.getName().getURI(); int jindex = name.indexOf(sjar); int nlen = name.length(); boolean isJar = jindex == nlen - 4 || jindex == nlen - 6; try { if (isJar) build(fobj, true); else { // loop thru folder for (FileObject childFolder : fobj.getChildren()) { boolean isAlsoJar = childFolder.getName().getURI().endsWith(sjar); // ignore anything that is not a folder or a // jar if (!isAlsoJar && childFolder.getType() != FileType.FOLDER) { continue; } // ignore any subversion or CVS directories if (childFolder.getName().getBaseName().equalsIgnoreCase(".svn")) { continue; } else if (childFolder.getName().getBaseName().equalsIgnoreCase(".cvs")) { continue; } try { build(childFolder, isAlsoJar); } catch (KettleConfigException e) { log.logError(Plugin.PLUGIN_LOADER, e.getMessage()); continue; } } } } catch (FileSystemException e) { log.logError(Plugin.PLUGIN_LOADER, e.getMessage()); continue; } catch (KettleConfigException e) { log.logError(Plugin.PLUGIN_LOADER, e.getMessage()); continue; } } else { log.logDebug(Plugin.PLUGIN_LOADER, fobj + " does not exist, ignoring this."); } } catch (Exception e) { throw new KettleConfigException(e); } } } } public <E extends Plugin> Collection<E> getDefinedPlugins(Class<E> pluginType) throws KettleConfigException { Class type = pluginType == JobPlugin.class ? Job.class : Step.class; for (Map.Entry<Class<? extends Annotation>, Set<? extends Plugin>> entry : this.plugins.entrySet()) { if (entry.getKey() == type) { return (Collection<E>) entry.getValue(); } } throw new KettleConfigException("Invalid plugin type: " + type); } private void build(FileObject parent, boolean isJar) throws KettleConfigException { try { FileObject xml = null; if (isJar) { FileObject exploded = explodeJar(parent); // try reading annotations first ... ResolverUtil<Plugin> resPlugins = new ResolverUtil<Plugin>(); // grab all jar files not part of the "lib" File fparent = new File(exploded.getURL().getFile()); File[] files = fparent.listFiles(new JarNameFilter()); URL[] classpath = new URL[files.length]; for (int i = 0; i < files.length; i++) classpath[i] = files[i].toURI().toURL(); ClassLoader cl = new PDIClassLoader(classpath, Thread.currentThread().getContextClassLoader()); resPlugins.setClassLoader(cl); for (FileObject couldBeJar : exploded.getChildren()) { if (couldBeJar.getName().getExtension().equals(JAR)) resPlugins.loadImplementationsInJar(Const.EMPTY_STRING, couldBeJar.getURL(), tests.values().toArray(new ResolverUtil.Test[2])); } for (Class<? extends Plugin> match : resPlugins.getClasses()) { for (Class<? extends Annotation> cannot : tests.keySet()) { Annotation annot = match.getAnnotation(cannot); if (annot != null) fromAnnotation(annot, exploded, match); } } // and we also read from the xml if present xml = exploded.getChild(Plugin.PLUGIN_XML_FILE); if (xml == null || !xml.exists()) return; parent = exploded; } else xml = parent.getChild(Plugin.PLUGIN_XML_FILE); // then read the xml if it is there if (xml != null && xml.isReadable()) fromXML(xml, parent); } catch (Exception e) { throw new KettleConfigException(e); } // throw new KettleConfigException("Unable to read plugin.xml from " + // parent); } private void fromXML(FileObject xml, FileObject parent) throws IOException, ClassNotFoundException, ParserConfigurationException, SAXException { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); Document doc = db.parse(KettleVFS.getInputStream(xml)); Node plugin = XMLHandler.getSubNode(doc, Plugin.PLUGIN); String id = XMLHandler.getTagAttribute(plugin, Plugin.ID); String description = XMLHandler.getTagAttribute(plugin, Plugin.DESCRIPTION); String iconfile = XMLHandler.getTagAttribute(plugin, Plugin.ICONFILE); String tooltip = XMLHandler.getTagAttribute(plugin, Plugin.TOOLTIP); String classname = XMLHandler.getTagAttribute(plugin, Plugin.CLASSNAME); String category = XMLHandler.getTagAttribute(plugin, Plugin.CATEGORY); String errorHelpfile = XMLHandler.getTagAttribute(plugin, Plugin.ERRORHELPFILE); // Localized categories // Node locCatsNode = XMLHandler.getSubNode(plugin, Plugin.LOCALIZED_CATEGORY); int nrLocCats = XMLHandler.countNodes(locCatsNode, Plugin.CATEGORY); Map<String, String> localizedCategories = new Hashtable<String, String>(); for (int j = 0; j < nrLocCats; j++) { Node locCatNode = XMLHandler.getSubNodeByNr(locCatsNode, Plugin.CATEGORY, j); String locale = XMLHandler.getTagAttribute(locCatNode, Plugin.LOCALE); String locCat = XMLHandler.getNodeValue(locCatNode); if (!Const.isEmpty(locale) && !Const.isEmpty(locCat)) { localizedCategories.put(locale.toLowerCase(), locCat); } } // Localized descriptions // Node locDescsNode = XMLHandler.getSubNode(plugin, Plugin.LOCALIZED_DESCRIPTION); int nrLocDescs = XMLHandler.countNodes(locDescsNode, Plugin.DESCRIPTION); Map<String, String> localizedDescriptions = new Hashtable<String, String>(); for (int j = 0; j < nrLocDescs; j++) { Node locDescNode = XMLHandler.getSubNodeByNr(locDescsNode, Plugin.DESCRIPTION, j); String locale = XMLHandler.getTagAttribute(locDescNode, Plugin.LOCALE); String locDesc = XMLHandler.getNodeValue(locDescNode); if (!Const.isEmpty(locale) && !Const.isEmpty(locDesc)) { localizedDescriptions.put(locale.toLowerCase(), locDesc); } } // Localized tooltips // Node locTipsNode = XMLHandler.getSubNode(plugin, Plugin.LOCALIZED_TOOLTIP); int nrLocTips = XMLHandler.countNodes(locTipsNode, Plugin.TOOLTIP); Map<String, String> localizedTooltips = new Hashtable<String, String>(); for (int j = 0; j < nrLocTips; j++) { Node locTipNode = XMLHandler.getSubNodeByNr(locTipsNode, Plugin.TOOLTIP, j); String locale = XMLHandler.getTagAttribute(locTipNode, Plugin.LOCALE); String locTip = XMLHandler.getNodeValue(locTipNode); if (!Const.isEmpty(locale) && !Const.isEmpty(locTip)) { localizedTooltips.put(locale.toLowerCase(), locTip); } } Node libsnode = XMLHandler.getSubNode(plugin, Plugin.LIBRARIES); int nrlibs = XMLHandler.countNodes(libsnode, Plugin.LIBRARY); String jarfiles[] = new String[nrlibs]; for (int j = 0; j < nrlibs; j++) { Node libnode = XMLHandler.getSubNodeByNr(libsnode, Plugin.LIBRARY, j); String jarfile = XMLHandler.getTagAttribute(libnode, Plugin.NAME); jarfiles[j] = parent.resolveFile(jarfile).getURL().getFile(); // System.out.println("jar files=" + jarfiles[j]); } // convert to URL List<URL> classpath = new ArrayList<URL>(); ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(new FileSystemResourceLoader()); for (int i = 0; i < jarfiles.length; i++) { try { Resource[] paths = resolver.getResources(jarfiles[i]); for (Resource path : paths) { classpath.add(path.getURL()); } } catch (IOException e) { e.printStackTrace(); continue; } } URL urls[] = classpath.toArray(new URL[classpath.size()]); URLClassLoader cl = new PDIClassLoader(urls, Thread.currentThread().getContextClassLoader()); String iconFilename = parent.resolveFile(iconfile).getURL().getFile(); Class<?> pluginClass = cl.loadClass(classname); // here we'll have to use some reflection in order to decide // which object we should instantiate! if (JobEntryInterface.class.isAssignableFrom(pluginClass)) { Set<JobPlugin> jps = (Set<JobPlugin>) this.plugins.get(Job.class); JobPlugin plg = new JobPlugin(Plugin.TYPE_PLUGIN, id, description, tooltip, parent.getName().getURI(), jarfiles, iconFilename, classname, category); plg.setClassLoader(cl); // Add localized information too... plg.setLocalizedCategories(localizedCategories); plg.setLocalizedDescriptions(localizedDescriptions); plg.setLocalizedTooltips(localizedTooltips); jps.add(plg); } else { String errorHelpFileFull = errorHelpfile; String path = parent.getName().getURI(); if (!Const.isEmpty(errorHelpfile)) errorHelpFileFull = (path == null) ? errorHelpfile : path + Const.FILE_SEPARATOR + errorHelpfile; StepPlugin sp = new StepPlugin(Plugin.TYPE_PLUGIN, new String[] { id }, description, tooltip, path, jarfiles, iconFilename, classname, category, errorHelpFileFull); // Add localized information too... sp.setLocalizedCategories(localizedCategories); sp.setLocalizedDescriptions(localizedDescriptions); sp.setLocalizedTooltips(localizedTooltips); Set<StepPlugin> sps = (Set<StepPlugin>) this.plugins.get(Step.class); sps.add(sp); } } private void fromAnnotation(Annotation annot, FileObject directory, Class<?> match) throws IOException { Class type = annot.annotationType(); if (type == Job.class) { Job jobAnnot = (Job) annot; String[] libs = getLibs(directory); Set<JobPlugin> jps = (Set<JobPlugin>) this.plugins.get(Job.class); JobPlugin pg = new JobPlugin(Plugin.TYPE_PLUGIN, jobAnnot.id(), jobAnnot.type().getDescription(), jobAnnot.tooltip(), directory.getURL().getFile(), libs, jobAnnot.image(), match.getName(), jobAnnot.categoryDescription()); pg.setClassLoader(match.getClassLoader()); jps.add(pg); } else if (type == Step.class) { Step jobAnnot = (Step) annot; String[] libs = getLibs(directory); Set<StepPlugin> jps = (Set<StepPlugin>) this.plugins.get(Step.class); StepPlugin pg = new StepPlugin(Plugin.TYPE_PLUGIN, jobAnnot.name(), jobAnnot.description(), jobAnnot.tooltip(), directory.getURL().getFile(), libs, jobAnnot.image(), match.getName(), jobAnnot.categoryDescription(), Const.EMPTY_STRING); pg.setClassLoader(match.getClassLoader()); jps.add(pg); } } private String[] getLibs(FileObject pluginLocation) throws IOException { File[] jars = new File(pluginLocation.getURL().getFile()).listFiles(new JarNameFilter()); String[] libs = new String[jars.length]; for (int i = 0; i < jars.length; i++) libs[i] = jars[i].getPath(); Arrays.sort(libs); int idx = Arrays.binarySearch(libs, DEFAULT_LIB); String[] retVal = null; if (idx < 0) // does not contain { String[] completeLib = new String[libs.length + 1]; System.arraycopy(libs, 0, completeLib, 0, libs.length); completeLib[libs.length] = pluginLocation.resolveFile(DEFAULT_LIB).getURL().getFile(); retVal = completeLib; } else retVal = libs; return retVal; } /** * "Deploys" the plugin jar file. * * @param parent * @return * @throws FileSystemException */ private FileObject explodeJar(FileObject parent) throws FileSystemException { // By Alex, 7/13/07 // Since the JVM does not support nested jars and // URLClassLoaders, we have to hack it // see // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4735639 // // We do so by exploding the jar, sort of like deploying it FileObject dest = VFS.getManager().resolveFile(Const.getKettleDirectory() + File.separator + WORK_DIR); dest.createFolder(); FileObject destFile = dest.resolveFile(parent.getName().getBaseName()); if (!destFile.exists()) destFile.createFolder(); else // delete children for (FileObject child : destFile.getChildren()) child.delete(new AllFileSelector()); // force VFS to treat it as a jar file explicitly with children, // etc. and copy destFile.copyFrom(!(parent instanceof JarFileObject) ? VFS.getManager().resolveFile(JAR + ":" + parent.getName().getURI()) : parent, new AllFileSelector()); return destFile; } private static class JarNameFilter implements FilenameFilter { // public boolean accept(File dir, String name) { //return patt.matcher(name).matches(); return name.endsWith(JAR) || name.endsWith(".zip"); } } }