Java tutorial
/** * $RCSfile$ * $Revision: 2608 $ * $Date: 2005-04-12 00:15:21 -0700 (Tue, 12 Apr 2005) $ * * Copyright 2005 Jive Software. * * All rights reserved. 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.jivesoftware.whack.container; import org.dom4j.Document; import org.dom4j.Element; import org.dom4j.io.SAXReader; import org.xmpp.component.Component; import org.xmpp.component.ComponentException; import org.xmpp.component.ComponentManager; import java.io.File; import java.io.FileFilter; import java.io.FileOutputStream; import java.io.InputStream; import java.util.*; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.zip.ZipFile; /** * Loads and manages components. The <tt>components</tt> directory is monitored for any * new components, and they are dynamically loaded.<p> * * @see Component * @see ServerContainer#start() * @author Matt Tucker * @author Gaston Dombiak */ public class ComponentFinder { private File componentDirectory; private Map<String, Component> components; private Map<Component, ComponentClassLoader> classloaders; private Map<Component, File> componentDirs; private Map<Component, String> componentDomains; private boolean setupMode = true; private ComponentManager manager; private ScheduledExecutorService executor = null; /** * Constructs a new component manager. * * @param componentDir the component directory. */ public ComponentFinder(ServerContainer server, File componentDir) { this.componentDirectory = componentDir; components = new HashMap<String, Component>(); componentDirs = new HashMap<Component, File>(); classloaders = new HashMap<Component, ComponentClassLoader>(); componentDomains = new HashMap<Component, String>(); manager = server.getManager(); setupMode = server.isSetupMode(); } /** * Starts the service that looks for components. */ public void start() { executor = new ScheduledThreadPoolExecutor(1); executor.scheduleWithFixedDelay(new ComponentMonitor(), 0, 10, TimeUnit.SECONDS); } /** * Shuts down running components that were found by the service. */ public void shutdown() { // Stop the component monitoring service. if (executor != null) { executor.shutdown(); } // Shutdown found components. for (String subdomain : componentDomains.values()) { try { manager.removeComponent(subdomain); } catch (ComponentException e) { manager.getLog().error("Error shutting down component", e); } } components.clear(); componentDirs.clear(); classloaders.clear(); componentDomains.clear(); } /** * Returns a Collection of all found components. * * @return a Collection of all found components. */ public Collection<Component> getComponents() { return Collections.unmodifiableCollection(components.values()); } /** * Returns a component by name or <tt>null</tt> if a component with that name does not * exist. The name is the name of the directory that the component is in such as * "broadcast". * * @param name the name of the component. * @return the component. */ public Component getComponent(String name) { return components.get(name); } /** * Returns the component's directory. * * @param component the component. * @return the component's directory. */ public File getComponentDirectory(Component component) { return componentDirs.get(component); } /** * Loads a plug-in module into the container. Loading consists of the * following steps:<ul> * * <li>Add all jars in the <tt>lib</tt> dir (if it exists) to the class loader</li> * <li>Add all files in <tt>classes</tt> dir (if it exists) to the class loader</li> * <li>Locate and load <tt>module.xml</tt> into the context</li> * <li>For each jive.module entry, load the given class as a module and start it</li> * * </ul> * * @param componentDir the component directory. */ private void loadComponent(File componentDir) { // Do not load & start any component if in setup mode if (setupMode) { return; } manager.getLog().debug("Loading component: " + componentDir.getName()); Component component = null; try { File componentConfig = new File(componentDir, "component.xml"); if (componentConfig.exists()) { SAXReader saxReader = new SAXReader(); Document componentXML = saxReader.read(componentConfig); ComponentClassLoader classLoader = new ComponentClassLoader(componentDir); String className = componentXML.selectSingleNode("/component/class").getText(); String subdomain = componentXML.selectSingleNode("/component/subdomain").getText(); //component = (Component)classLoader.loadClass(className).newInstance(); Class aClass = classLoader.loadClass(className); component = (Component) aClass.newInstance(); manager.addComponent(subdomain, component); components.put(componentDir.getName(), component); componentDirs.put(component, componentDir); classloaders.put(component, classLoader); componentDomains.put(component, subdomain); // Load any JSP's defined by the component. File webXML = new File(componentDir, "web" + File.separator + "web.xml"); if (webXML.exists()) { ComponentServlet.registerServlets(this, component, webXML); } // If there a <adminconsole> section defined, register it. Element adminElement = (Element) componentXML.selectSingleNode("/component/adminconsole"); if (adminElement != null) { // If global images are specified, override their URL. Element imageEl = (Element) adminElement .selectSingleNode("/component/adminconsole/global/logo-image"); if (imageEl != null) { imageEl.setText("components/" + componentDir.getName() + "/" + imageEl.getText()); } imageEl = (Element) adminElement.selectSingleNode("/component/adminconsole/global/login-image"); if (imageEl != null) { imageEl.setText("components/" + componentDir.getName() + "/" + imageEl.getText()); } // Modify all the URL's in the XML so that they are passed through // the component servlet correctly. /*List urls = adminElement.selectNodes("//@url"); for (int i=0; i<urls.size(); i++) { Attribute attr = (Attribute)urls.get(i); attr.setValue("components/" + componentDir.getName() + "/" + attr.getValue()); } AdminConsole.addModel(componentDir.getName(), adminElement);*/ } } else { manager.getLog() .warn("Component " + componentDir + " could not be loaded: no component.xml file found"); } } catch (Exception e) { manager.getLog().error("Error loading component: " + componentDir.getName(), e); } } /** * Unloads a component. The {@link ComponentManager#removeComponent(String)} method will be * called and then any resources will be released. The name should be the name of the component * directory and not the name as given by the component meta-data. This method only removes * the component but does not delete the component JAR file. Therefore, if the component JAR * still exists after this method is called, the component will be started again the next * time the component monitor process runs. This is useful for "restarting" components.<p> * * This method is called automatically when a component's JAR file is deleted. * * @param componentName the name of the component to unload. */ public void unloadComponent(String componentName) { manager.getLog().debug("Unloading component " + componentName); Component component = components.get(componentName); if (component == null) { return; } File webXML = new File(componentDirectory + File.separator + componentName + File.separator + "web" + File.separator + "web.xml"); if (webXML.exists()) { //AdminConsole.removeModel(componentName); ComponentServlet.unregisterServlets(webXML); } ComponentClassLoader classLoader = classloaders.get(component); try { manager.removeComponent(componentDomains.get(component)); } catch (ComponentException e) { manager.getLog().error("Error shutting down component", e); } classLoader.destroy(); components.remove(componentName); componentDirs.remove(component); classloaders.remove(component); componentDomains.remove(component); } public Class loadClass(String className, Component component) throws ClassNotFoundException, IllegalAccessException, InstantiationException { ComponentClassLoader loader = classloaders.get(component); return loader.loadClass(className); } /** * Returns the name of a component. The value is retrieved from the component.xml file * of the component. If the value could not be found, <tt>null</tt> will be returned. * Note that this value is distinct from the name of the component directory. * * @param component the component. * @return the component's name. */ public String getName(Component component) { String name = getElementValue(component, "/component/name"); if (name != null) { return name; } else { return componentDirs.get(component).getName(); } } /** * Returns the description of a component. The value is retrieved from the component.xml file * of the component. If the value could not be found, <tt>null</tt> will be returned. * * @param component the component. * @return the component's description. */ public String getDescription(Component component) { return getElementValue(component, "/component/description"); } /** * Returns the author of a component. The value is retrieved from the component.xml file * of the component. If the value could not be found, <tt>null</tt> will be returned. * * @param component the component. * @return the component's author. */ public String getAuthor(Component component) { return getElementValue(component, "/component/author"); } /** * Returns the version of a component. The value is retrieved from the component.xml file * of the component. If the value could not be found, <tt>null</tt> will be returned. * * @param component the component. * @return the component's version. */ public String getVersion(Component component) { return getElementValue(component, "/component/version"); } /** * Returns the value of an element selected via an xpath expression from * a component's component.xml file. * * @param component the component. * @param xpath the xpath expression. * @return the value of the element selected by the xpath expression. */ private String getElementValue(Component component, String xpath) { File componentDir = componentDirs.get(component); if (componentDir == null) { return null; } try { File componentConfig = new File(componentDir, "component.xml"); if (componentConfig.exists()) { SAXReader saxReader = new SAXReader(); Document componentXML = saxReader.read(componentConfig); Element element = (Element) componentXML.selectSingleNode(xpath); if (element != null) { return element.getTextTrim(); } } } catch (Exception e) { manager.getLog().error(e); } return null; } /** * A service that monitors the component directory for components. It periodically * checks for new component JAR files and extracts them if they haven't already * been extracted. Then, any new component directories are loaded. */ private class ComponentMonitor implements Runnable { public void run() { try { File[] jars = componentDirectory.listFiles(new FileFilter() { public boolean accept(File pathname) { String fileName = pathname.getName().toLowerCase(); return (fileName.endsWith(".jar") || fileName.endsWith(".war")); } }); for (int i = 0; i < jars.length; i++) { File jarFile = jars[i]; String componentName = jarFile.getName().substring(0, jarFile.getName().length() - 4) .toLowerCase(); // See if the JAR has already been exploded. File dir = new File(componentDirectory, componentName); // If the JAR hasn't been exploded, do so. if (!dir.exists()) { unzipComponent(componentName, jarFile, dir); } // See if the JAR is newer than the directory. If so, the component // needs to be unloaded and then reloaded. else if (jarFile.lastModified() > dir.lastModified()) { unloadComponent(componentName); // Ask the system to clean up references. System.gc(); while (!deleteDir(dir)) { manager.getLog().error("Error unloading component " + componentName + ". " + "Will attempt again momentarily."); Thread.sleep(5000); } // Now unzip the component. unzipComponent(componentName, jarFile, dir); } } File[] dirs = componentDirectory.listFiles(new FileFilter() { public boolean accept(File pathname) { return pathname.isDirectory(); } }); for (int i = 0; i < dirs.length; i++) { File dirFile = dirs[i]; // If the component hasn't already been started, start it. if (!components.containsKey(dirFile.getName())) { loadComponent(dirFile); } } // See if any currently running components need to be unloaded // due to its JAR file being deleted. if (components.size() > jars.length + 1) { // Build a list of components to delete first so that the components // keyset is modified as we're iterating through it. List<String> toDelete = new ArrayList<String>(); for (String componentName : components.keySet()) { File file = new File(componentDirectory, componentName + ".jar"); if (!file.exists()) { toDelete.add(componentName); } } for (String componentName : toDelete) { unloadComponent(componentName); System.gc(); while (!deleteDir(new File(componentDirectory, componentName))) { manager.getLog().error("Error unloading component " + componentName + ". " + "Will attempt again momentarily."); Thread.sleep(5000); } } } } catch (Exception e) { manager.getLog().error(e); } } /** * Unzips a component from a JAR file into a directory. If the JAR file * isn't a component, this method will do nothing. * * @param componentName the name of the component. * @param file the JAR file * @param dir the directory to extract the component to. */ private void unzipComponent(String componentName, File file, File dir) { try { ZipFile zipFile = new JarFile(file); // Ensure that this JAR is a component. if (zipFile.getEntry("component.xml") == null) { return; } dir.mkdir(); manager.getLog().debug("Extracting component: " + componentName); for (Enumeration e = zipFile.entries(); e.hasMoreElements();) { JarEntry entry = (JarEntry) e.nextElement(); File entryFile = new File(dir, entry.getName()); // Ignore any manifest.mf entries. if (entry.getName().toLowerCase().endsWith("manifest.mf")) { continue; } if (!entry.isDirectory()) { entryFile.getParentFile().mkdirs(); FileOutputStream out = new FileOutputStream(entryFile); InputStream zin = zipFile.getInputStream(entry); byte[] b = new byte[512]; int len = 0; while ((len = zin.read(b)) != -1) { out.write(b, 0, len); } out.flush(); out.close(); zin.close(); } } zipFile.close(); zipFile = null; } catch (Exception e) { manager.getLog().error(e); } } /** * Deletes a directory. */ public boolean deleteDir(File dir) { if (dir.isDirectory()) { String[] children = dir.list(); for (int i = 0; i < children.length; i++) { boolean success = deleteDir(new File(dir, children[i])); if (!success) { return false; } } } return dir.delete(); } } }