/* * JBoss, Home of Professional Open Source. * Copyright 2008, Red Hat Middleware LLC, and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.deployment.scanner; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.StringTokenizer; import javax.management.MBeanServer; import javax.management.ObjectName; import org.jboss.bootstrap.spi.ServerConfig; import org.jboss.deployment.DefaultDeploymentSorter; import org.jboss.deployment.IncompleteDeploymentException; import org.jboss.mx.util.JMXExceptionDecoder; import org.jboss.net.protocol.URLLister; import org.jboss.net.protocol.URLListerFactory; import org.jboss.net.protocol.URLLister.URLFilter; import org.jboss.system.server.ServerConfigLocator; import org.jboss.util.NullArgumentException; import org.jboss.util.StringPropertyReplacer; /** * A URL-based deployment scanner. Supports local directory * scanning for file-based urls. * * @jmx:mbean extends="org.jboss.deployment.scanner.DeploymentScannerMBean" * * @version <tt>$Revision: 81033 $</tt> * @author <a href="mailto:jason@planet57.com">Jason Dillon</a> * @author <a href="mailto:dimitris@jboss.org">Dimitris Andreadis</a> */ public class URLDeploymentScanner extends AbstractDeploymentScanner implements DeploymentScanner, URLDeploymentScannerMBean { /** A set of deployment URLs to skip **/ protected Set skipSet = Collections.synchronizedSet(new HashSet()); /** The list of URLs to scan. */ protected List urlList = Collections.synchronizedList(new ArrayList()); /** A set of scanned urls which have been deployed. */ protected Set deployedSet = Collections.synchronizedSet(new HashSet()); /** Helper for listing local/remote directory URLs */ protected URLListerFactory listerFactory = new URLListerFactory(); /** The server's home directory, for relative paths. */ protected File serverHome; protected URL serverHomeURL; /** A sorter urls from a scaned directory to allow for coarse dependency ordering based on file type */ protected Comparator sorter; /** Allow a filter for scanned directories */ protected URLFilter filter; protected IncompleteDeploymentException lastIncompleteDeploymentException; /** Whether to search inside directories whose names containing no dots */ protected boolean doRecursiveSearch = true; /** * @jmx:managed-attribute */ public void setRecursiveSearch (boolean recurse) { doRecursiveSearch = recurse; } /** * @jmx:managed-attribute */ public boolean getRecursiveSearch () { return doRecursiveSearch; } /** * @jmx:managed-attribute */ public void setURLList(final List list) { if (list == null) throw new NullArgumentException("list"); // start out with a fresh list urlList.clear(); Iterator iter = list.iterator(); while (iter.hasNext()) { URL url = (URL)iter.next(); if (url == null) throw new NullArgumentException("list element"); addURL(url); } log.debug("URL list: " + urlList); } /** * @jmx:managed-attribute * * @param classname The name of a Comparator class. */ public void setURLComparator(String classname) throws ClassNotFoundException, IllegalAccessException, InstantiationException { sorter = (Comparator)Thread.currentThread().getContextClassLoader().loadClass(classname).newInstance(); } /** * @jmx:managed-attribute */ public String getURLComparator() { if (sorter == null) return null; return sorter.getClass().getName(); } /** * @jmx:managed-attribute * * @param classname The name of a FileFilter class. */ public void setFilter(String classname) throws ClassNotFoundException, IllegalAccessException, InstantiationException { Class filterClass = Thread.currentThread().getContextClassLoader().loadClass(classname); filter = (URLFilter) filterClass.newInstance(); } /** * @jmx:managed-attribute */ public String getFilter() { if (filter == null) return null; return filter.getClass().getName(); } /** * @jmx:managed-attribute * * @param filter The URLFilter instance */ public void setFilterInstance(URLFilter filter) { this.filter = filter; } /** * @jmx:managed-attribute */ public URLFilter getFilterInstance() { return filter; } /** * @jmx:managed-attribute */ public List getURLList() { // too bad, List isn't a cloneable return new ArrayList(urlList); } /** * @jmx:managed-operation */ public void addURL(final URL url) { if (url == null) throw new NullArgumentException("url"); try { // check if this is a valid url url.openConnection().connect(); } catch (IOException e) { // either a bad configuration (non-existent url) or a transient i/o error log.warn("addURL(), caught " + e.getClass().getName() + ": " + e.getMessage()); } urlList.add(url); log.debug("Added url: " + url); } /** * @jmx:managed-operation */ public void removeURL(final URL url) { if (url == null) throw new NullArgumentException("url"); boolean success = urlList.remove(url); if (success) { log.debug("Removed url: " + url); } } /** * @jmx:managed-operation */ public boolean hasURL(final URL url) { if (url == null) throw new NullArgumentException("url"); return urlList.contains(url); } /** * Temporarily ignore changes (addition, updates, removal) to a particular * deployment, identified by its deployment URL. The deployment URL is different * from the 'base' URLs that are scanned by the scanner (e.g. the full path to * deploy/jmx-console.war vs. deploy/). This can be used to avoid an attempt * by the scanner to deploy/redeploy/undeploy a URL that is being modified. * * To re-enable scanning of changes for a URL, use resumeDeployment(URL, boolean). * * @jmx:managed-operation */ public void suspendDeployment(URL url) { if (url == null) throw new NullArgumentException("url"); if (skipSet.add(url)) log.debug("Deployment URL added to skipSet: " + url); else throw new IllegalStateException("Deployment URL already suspended: " + url); } /** * Re-enables scanning of a particular deployment URL, previously suspended * using suspendDeployment(URL). If the markUpToDate flag is true then the * deployment module will be considered up-to-date during the next scan. * If the flag is false, at the next scan the scanner will check the * modification date to decide if the module needs deploy/redeploy/undeploy. * * @jmx:managed-operation */ public void resumeDeployment(URL url, boolean markUpToDate) { if (url == null) throw new NullArgumentException("url"); if (skipSet.contains(url)) { if (markUpToDate) { // look for the deployment and mark it as uptodate for (Iterator i = deployedSet.iterator(); i.hasNext(); ) { DeployedURL deployedURL = (DeployedURL)i.next(); if (deployedURL.url.equals(url)) { // the module could have been removed.. log.debug("Marking up-to-date: " + url); deployedURL.deployed(); break; } } } // don't skip this url anymore skipSet.remove(url); log.debug("Deployment URL removed from skipSet: " + url); } else { throw new IllegalStateException("Deployment URL not suspended: " + url); } } /** * Lists all urls deployed by the scanner, each URL on a new line. * * @jmx:managed-operation */ public String listDeployedURLs() { StringBuffer sbuf = new StringBuffer(); for (Iterator i = deployedSet.iterator(); i.hasNext(); ) { URL url = ((DeployedURL)i.next()).url; if (sbuf.length() > 0) { sbuf.append("\n").append(url); } else { sbuf.append(url); } } return sbuf.toString(); } ///////////////////////////////////////////////////////////////////////// // Management/Configuration Helpers // ///////////////////////////////////////////////////////////////////////// /** * @jmx:managed-attribute */ public void setURLs(final String listspec) throws MalformedURLException { if (listspec == null) throw new NullArgumentException("listspec"); List list = new LinkedList(); StringTokenizer stok = new StringTokenizer(listspec, ","); while (stok.hasMoreTokens()) { String urlspec = stok.nextToken().trim(); log.debug("Adding URL from spec: " + urlspec); URL url = makeURL(urlspec); log.debug("URL: " + url); list.add(url); } setURLList(list); } /** * A helper to make a URL from a full url, or a filespec. */ protected URL makeURL(String urlspec) throws MalformedURLException { // First replace URL with appropriate properties // urlspec = StringPropertyReplacer.replaceProperties (urlspec); return new URL(serverHomeURL, urlspec); } /** * @jmx:managed-operation */ public void addURL(final String urlspec) throws MalformedURLException { addURL(makeURL(urlspec)); } /** * @jmx:managed-operation */ public void removeURL(final String urlspec) throws MalformedURLException { removeURL(makeURL(urlspec)); } /** * @jmx:managed-operation */ public boolean hasURL(final String urlspec) throws MalformedURLException { return hasURL(makeURL(urlspec)); } /** * A helper to deploy the given URL with the deployer. */ protected void deploy(final DeployedURL du) { // If the deployer is null simply ignore the request if (deployer == null) return; try { if (log.isTraceEnabled()) log.trace("Deploying: " + du); deployer.deploy(du.url); } catch (IncompleteDeploymentException e) { lastIncompleteDeploymentException = e; } catch (Exception e) { log.debug("Failed to deploy: " + du, e); } du.deployed(); if (!deployedSet.contains(du)) { deployedSet.add(du); } } /** * A helper to undeploy the given URL from the deployer. */ protected void undeploy(final DeployedURL du) { try { if (log.isTraceEnabled()) log.trace("Undeploying: " + du); deployer.undeploy(du.url); deployedSet.remove(du); } catch (Exception e) { log.error("Failed to undeploy: " + du, e); } } /** * Checks if the url is in the deployed set. */ protected boolean isDeployed(final URL url) { DeployedURL du = new DeployedURL(url); return deployedSet.contains(du); } public synchronized void scan() throws Exception { lastIncompleteDeploymentException = null; if (urlList == null) throw new IllegalStateException("not initialized"); updateSorter(); boolean trace = log.isTraceEnabled(); List urlsToDeploy = new LinkedList(); // Scan for deployments if (trace) { log.trace("Scanning for new deployments"); } synchronized (urlList) { for (Iterator i = urlList.iterator(); i.hasNext();) { URL url = (URL) i.next(); try { if (url.toString().endsWith("/")) { // treat URL as a collection URLLister lister = listerFactory.createURLLister(url); // listMembers() will throw an IOException if collection url does not exist urlsToDeploy.addAll(lister.listMembers(url, filter, doRecursiveSearch)); } else { // treat URL as a deployable unit // throws IOException if this URL does not exist url.openConnection().connect(); urlsToDeploy.add(url); } } catch (IOException e) { // Either one of the configured URLs is bad, i.e. points to a non-existent // location, or it ends with a '/' but it is not a directory (so it // is really user's fault), OR some other hopefully transient I/O error // happened (e.g. out of file descriptors?) so log a warning. log.warn("Scan URL, caught " + e.getClass().getName() + ": " + e.getMessage()); // We need to return because at least one of the listed URLs will // return no results, and so all deployments starting from that point // (e.g. deploy/) will get undeployed, see JBAS-3107. // On the other hand, in case of a bad configuration nothing will get // deployed. If really want independence of e.g. 2 deploy urls, more // than one URLDeploymentScanners can be setup. return; } } } if (trace) { log.trace("Updating existing deployments"); } LinkedList urlsToRemove = new LinkedList(); LinkedList urlsToCheckForUpdate = new LinkedList(); synchronized (deployedSet) { // remove previously deployed URLs no longer needed for (Iterator i = deployedSet.iterator(); i.hasNext();) { DeployedURL deployedURL = (DeployedURL) i.next(); if (skipSet.contains(deployedURL.url)) { if (trace) log.trace("Skipping update/removal check for: " + deployedURL.url); } else { if (urlsToDeploy.contains(deployedURL.url)) { urlsToCheckForUpdate.add(deployedURL); } else { urlsToRemove.add(deployedURL); } } } } // ******** // Undeploy // ******** for (Iterator i = urlsToRemove.iterator(); i.hasNext();) { DeployedURL deployedURL = (DeployedURL) i.next(); if (trace) { log.trace("Removing " + deployedURL.url); } undeploy(deployedURL); } // ******** // Redeploy // ******** // compute the DeployedURL list to update ArrayList urlsToUpdate = new ArrayList(urlsToCheckForUpdate.size()); for (Iterator i = urlsToCheckForUpdate.iterator(); i.hasNext();) { DeployedURL deployedURL = (DeployedURL) i.next(); if (deployedURL.isModified()) { if (trace) { log.trace("Re-deploying " + deployedURL.url); } urlsToUpdate.add(deployedURL); } } // sort to update list Collections.sort(urlsToUpdate, new Comparator() { public int compare(Object o1, Object o2) { return sorter.compare(((DeployedURL) o1).url, ((DeployedURL) o2).url); } }); // Undeploy in order for (int i = urlsToUpdate.size() - 1; i >= 0;i--) { undeploy((DeployedURL) urlsToUpdate.get(i)); } // Deploy in order for (int i = 0; i < urlsToUpdate.size();i++) { deploy((DeployedURL) urlsToUpdate.get(i)); } // ****** // Deploy // ****** Collections.sort(urlsToDeploy, sorter); for (Iterator i = urlsToDeploy.iterator(); i.hasNext();) { URL url = (URL) i.next(); DeployedURL deployedURL = new DeployedURL(url); if (deployedSet.contains(deployedURL) == false) { if (skipSet.contains(url)) { if (trace) log.trace("Skipping deployment of: " + url); } else { if (trace) log.trace("Deploying " + deployedURL.url); deploy(deployedURL); } } i.remove(); // Check to see if mainDeployer suffix list has changed. // if so, then resort if (i.hasNext() && updateSorter()) { Collections.sort(urlsToDeploy, sorter); i = urlsToDeploy.iterator(); } } // Validate that there are still incomplete deployments if (lastIncompleteDeploymentException != null) { try { Object[] args = {}; String[] sig = {}; getServer().invoke(getDeployer(), "checkIncompleteDeployments", args, sig); } catch (Exception e) { Throwable t = JMXExceptionDecoder.decode(e); log.error(t); } } } protected boolean updateSorter() { // Check to see if mainDeployer suffix list has changed. if (sorter instanceof DefaultDeploymentSorter) { DefaultDeploymentSorter defaultSorter = (DefaultDeploymentSorter)sorter; if (defaultSorter.getSuffixOrder() != mainDeployer.getSuffixOrder()) { defaultSorter.setSuffixOrder(mainDeployer.getSuffixOrder()); return true; } } return false; } ///////////////////////////////////////////////////////////////////////// // Service/ServiceMBeanSupport // ///////////////////////////////////////////////////////////////////////// public ObjectName preRegister(MBeanServer server, ObjectName name) throws Exception { // get server's home for relative paths, need this for setting // attribute final values, so we need to do it here ServerConfig serverConfig = ServerConfigLocator.locate(); serverHome = serverConfig.getServerHomeDir(); serverHomeURL = serverConfig.getServerHomeURL(); return super.preRegister(server, name); } protected void createService() throws Exception { // Perform a couple of sanity checks if (this.filter == null) { throw new IllegalStateException("'FilterInstance' attribute not configured"); } if (this.sorter == null) { throw new IllegalStateException("'URLComparator' attribute not configured"); } // ok, proceed with normal createService() super.createService(); } ///////////////////////////////////////////////////////////////////////// // DeployedURL // ///////////////////////////////////////////////////////////////////////// /** * A container and help class for a deployed URL. * should be static at this point, with the explicit scanner ref, but I'm (David) lazy. */ protected class DeployedURL { public URL url; /** The url to check to decide if we need to redeploy */ public URL watchUrl; public long deployedLastModified; public DeployedURL(final URL url) { this.url = url; } public void deployed() { deployedLastModified = getLastModified(); } public boolean isFile() { return url.getProtocol().equals("file"); } public File getFile() { return new File(url.getFile()); } public boolean isRemoved() { if (isFile()) { File file = getFile(); return !file.exists(); } return false; } public long getLastModified() { if (watchUrl == null) { try { Object o = getServer().invoke( getDeployer(), "getWatchUrl", new Object[] { url }, new String[] { URL.class.getName() } ); watchUrl = o == null ? url : (URL)o; getLog().debug("Watch URL for: " + url + " -> " + watchUrl); } catch (Exception e) { watchUrl = url; getLog().debug("Unable to obtain watchUrl from deployer. Use url: " + url, e); } } try { URLConnection connection; if (watchUrl != null) { connection = watchUrl.openConnection(); } else { connection = url.openConnection(); } long lastModified = connection.getLastModified(); return lastModified; } catch (java.io.IOException e) { log.warn("Failed to check modification of deployed url: " + url, e); } return -1; } public boolean isModified() { long lastModified = getLastModified(); if (lastModified == -1) { // ignore errors fetching the timestamp - see bug 598335 return false; } return deployedLastModified != lastModified; } public int hashCode() { return url.hashCode(); } public boolean equals(final Object other) { if (other instanceof DeployedURL) { return ((DeployedURL)other).url.equals(this.url); } return false; } public String toString() { return super.toString() + "{ url=" + url + ", deployedLastModified=" + deployedLastModified + " }"; } } }