001: /*
002: * JBoss, Home of Professional Open Source.
003: * Copyright 2006, Red Hat Middleware LLC, and individual contributors
004: * as indicated by the @author tags. See the copyright.txt file in the
005: * distribution for a full listing of individual contributors.
006: *
007: * This is free software; you can redistribute it and/or modify it
008: * under the terms of the GNU Lesser General Public License as
009: * published by the Free Software Foundation; either version 2.1 of
010: * the License, or (at your option) any later version.
011: *
012: * This software is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: *
017: * You should have received a copy of the GNU Lesser General Public
018: * License along with this software; if not, write to the Free
019: * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020: * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
021: */
022: package org.jboss.deployment.scanner;
023:
024: import java.io.File;
025: import java.io.IOException;
026: import java.net.MalformedURLException;
027: import java.net.URL;
028: import java.net.URLConnection;
029: import java.util.ArrayList;
030: import java.util.Collections;
031: import java.util.Comparator;
032: import java.util.HashSet;
033: import java.util.Iterator;
034: import java.util.LinkedList;
035: import java.util.List;
036: import java.util.Set;
037: import java.util.StringTokenizer;
038:
039: import javax.management.MBeanServer;
040: import javax.management.ObjectName;
041:
042: import org.jboss.deployment.DefaultDeploymentSorter;
043: import org.jboss.deployment.IncompleteDeploymentException;
044: import org.jboss.mx.util.JMXExceptionDecoder;
045: import org.jboss.net.protocol.URLLister;
046: import org.jboss.net.protocol.URLListerFactory;
047: import org.jboss.net.protocol.URLLister.URLFilter;
048: import org.jboss.system.server.ServerConfig;
049: import org.jboss.system.server.ServerConfigLocator;
050: import org.jboss.util.NullArgumentException;
051: import org.jboss.util.StringPropertyReplacer;
052:
053: /**
054: * A URL-based deployment scanner. Supports local directory
055: * scanning for file-based urls.
056: *
057: * @jmx:mbean extends="org.jboss.deployment.scanner.DeploymentScannerMBean"
058: *
059: * @version <tt>$Revision: 57205 $</tt>
060: * @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
061: * @author <a href="mailto:dimitris@jboss.org">Dimitris Andreadis</a>
062: */
063: public class URLDeploymentScanner extends AbstractDeploymentScanner
064: implements DeploymentScanner, URLDeploymentScannerMBean {
065: /** A set of deployment URLs to skip **/
066: protected Set skipSet = Collections.synchronizedSet(new HashSet());
067:
068: /** The list of URLs to scan. */
069: protected List urlList = Collections
070: .synchronizedList(new ArrayList());
071:
072: /** A set of scanned urls which have been deployed. */
073: protected Set deployedSet = Collections
074: .synchronizedSet(new HashSet());
075:
076: /** Helper for listing local/remote directory URLs */
077: protected URLListerFactory listerFactory = new URLListerFactory();
078:
079: /** The server's home directory, for relative paths. */
080: protected File serverHome;
081:
082: protected URL serverHomeURL;
083:
084: /** A sorter urls from a scaned directory to allow for coarse dependency
085: ordering based on file type
086: */
087: protected Comparator sorter;
088:
089: /** Allow a filter for scanned directories */
090: protected URLFilter filter;
091:
092: protected IncompleteDeploymentException lastIncompleteDeploymentException;
093:
094: /** Whether to search inside directories whose names containing no dots */
095: protected boolean doRecursiveSearch = true;
096:
097: /**
098: * @jmx:managed-attribute
099: */
100: public void setRecursiveSearch(boolean recurse) {
101: doRecursiveSearch = recurse;
102: }
103:
104: /**
105: * @jmx:managed-attribute
106: */
107: public boolean getRecursiveSearch() {
108: return doRecursiveSearch;
109: }
110:
111: /**
112: * @jmx:managed-attribute
113: */
114: public void setURLList(final List list) {
115: if (list == null)
116: throw new NullArgumentException("list");
117:
118: // start out with a fresh list
119: urlList.clear();
120:
121: Iterator iter = list.iterator();
122: while (iter.hasNext()) {
123: URL url = (URL) iter.next();
124: if (url == null)
125: throw new NullArgumentException("list element");
126:
127: addURL(url);
128: }
129:
130: log.debug("URL list: " + urlList);
131: }
132:
133: /**
134: * @jmx:managed-attribute
135: *
136: * @param classname The name of a Comparator class.
137: */
138: public void setURLComparator(String classname)
139: throws ClassNotFoundException, IllegalAccessException,
140: InstantiationException {
141: sorter = (Comparator) Thread.currentThread()
142: .getContextClassLoader().loadClass(classname)
143: .newInstance();
144: }
145:
146: /**
147: * @jmx:managed-attribute
148: */
149: public String getURLComparator() {
150: if (sorter == null)
151: return null;
152: return sorter.getClass().getName();
153: }
154:
155: /**
156: * @jmx:managed-attribute
157: *
158: * @param classname The name of a FileFilter class.
159: */
160: public void setFilter(String classname)
161: throws ClassNotFoundException, IllegalAccessException,
162: InstantiationException {
163: Class filterClass = Thread.currentThread()
164: .getContextClassLoader().loadClass(classname);
165: filter = (URLFilter) filterClass.newInstance();
166: }
167:
168: /**
169: * @jmx:managed-attribute
170: */
171: public String getFilter() {
172: if (filter == null)
173: return null;
174: return filter.getClass().getName();
175: }
176:
177: /**
178: * @jmx:managed-attribute
179: *
180: * @param filter The URLFilter instance
181: */
182: public void setFilterInstance(URLFilter filter) {
183: this .filter = filter;
184: }
185:
186: /**
187: * @jmx:managed-attribute
188: */
189: public URLFilter getFilterInstance() {
190: return filter;
191: }
192:
193: /**
194: * @jmx:managed-attribute
195: */
196: public List getURLList() {
197: // too bad, List isn't a cloneable
198: return new ArrayList(urlList);
199: }
200:
201: /**
202: * @jmx:managed-operation
203: */
204: public void addURL(final URL url) {
205: if (url == null)
206: throw new NullArgumentException("url");
207:
208: try {
209: // check if this is a valid url
210: url.openConnection().connect();
211: } catch (IOException e) {
212: // either a bad configuration (non-existent url) or a transient i/o error
213: log.warn("addURL(), caught " + e.getClass().getName()
214: + ": " + e.getMessage());
215: }
216: urlList.add(url);
217:
218: log.debug("Added url: " + url);
219: }
220:
221: /**
222: * @jmx:managed-operation
223: */
224: public void removeURL(final URL url) {
225: if (url == null)
226: throw new NullArgumentException("url");
227:
228: boolean success = urlList.remove(url);
229: if (success) {
230: log.debug("Removed url: " + url);
231: }
232: }
233:
234: /**
235: * @jmx:managed-operation
236: */
237: public boolean hasURL(final URL url) {
238: if (url == null)
239: throw new NullArgumentException("url");
240:
241: return urlList.contains(url);
242: }
243:
244: /**
245: * Temporarily ignore changes (addition, updates, removal) to a particular
246: * deployment, identified by its deployment URL. The deployment URL is different
247: * from the 'base' URLs that are scanned by the scanner (e.g. the full path to
248: * deploy/jmx-console.war vs. deploy/). This can be used to avoid an attempt
249: * by the scanner to deploy/redeploy/undeploy a URL that is being modified.
250: *
251: * To re-enable scanning of changes for a URL, use resumeDeployment(URL, boolean).
252: *
253: * @jmx:managed-operation
254: */
255: public void suspendDeployment(URL url) {
256: if (url == null)
257: throw new NullArgumentException("url");
258:
259: if (skipSet.add(url))
260: log.debug("Deployment URL added to skipSet: " + url);
261: else
262: throw new IllegalStateException(
263: "Deployment URL already suspended: " + url);
264: }
265:
266: /**
267: * Re-enables scanning of a particular deployment URL, previously suspended
268: * using suspendDeployment(URL). If the markUpToDate flag is true then the
269: * deployment module will be considered up-to-date during the next scan.
270: * If the flag is false, at the next scan the scanner will check the
271: * modification date to decide if the module needs deploy/redeploy/undeploy.
272: *
273: * @jmx:managed-operation
274: */
275: public void resumeDeployment(URL url, boolean markUpToDate) {
276: if (url == null)
277: throw new NullArgumentException("url");
278:
279: if (skipSet.contains(url)) {
280: if (markUpToDate) {
281: // look for the deployment and mark it as uptodate
282: for (Iterator i = deployedSet.iterator(); i.hasNext();) {
283: DeployedURL deployedURL = (DeployedURL) i.next();
284: if (deployedURL.url.equals(url)) {
285: // the module could have been removed..
286: log.debug("Marking up-to-date: " + url);
287: deployedURL.deployed();
288: break;
289: }
290: }
291: }
292: // don't skip this url anymore
293: skipSet.remove(url);
294: log.debug("Deployment URL removed from skipSet: " + url);
295: } else {
296: throw new IllegalStateException(
297: "Deployment URL not suspended: " + url);
298: }
299: }
300:
301: /**
302: * Lists all urls deployed by the scanner, each URL on a new line.
303: *
304: * @jmx:managed-operation
305: */
306: public String listDeployedURLs() {
307: StringBuffer sbuf = new StringBuffer();
308: for (Iterator i = deployedSet.iterator(); i.hasNext();) {
309: URL url = ((DeployedURL) i.next()).url;
310: if (sbuf.length() > 0) {
311: sbuf.append("\n").append(url);
312: } else {
313: sbuf.append(url);
314: }
315: }
316: return sbuf.toString();
317: }
318:
319: /////////////////////////////////////////////////////////////////////////
320: // Management/Configuration Helpers //
321: /////////////////////////////////////////////////////////////////////////
322:
323: /**
324: * @jmx:managed-attribute
325: */
326: public void setURLs(final String listspec)
327: throws MalformedURLException {
328: if (listspec == null)
329: throw new NullArgumentException("listspec");
330:
331: List list = new LinkedList();
332:
333: StringTokenizer stok = new StringTokenizer(listspec, ",");
334: while (stok.hasMoreTokens()) {
335: String urlspec = stok.nextToken().trim();
336: log.debug("Adding URL from spec: " + urlspec);
337:
338: URL url = makeURL(urlspec);
339: log.debug("URL: " + url);
340:
341: list.add(url);
342: }
343:
344: setURLList(list);
345: }
346:
347: /**
348: * A helper to make a URL from a full url, or a filespec.
349: */
350: protected URL makeURL(String urlspec) throws MalformedURLException {
351: // First replace URL with appropriate properties
352: //
353: urlspec = StringPropertyReplacer.replaceProperties(urlspec);
354: return new URL(serverHomeURL, urlspec);
355: }
356:
357: /**
358: * @jmx:managed-operation
359: */
360: public void addURL(final String urlspec)
361: throws MalformedURLException {
362: addURL(makeURL(urlspec));
363: }
364:
365: /**
366: * @jmx:managed-operation
367: */
368: public void removeURL(final String urlspec)
369: throws MalformedURLException {
370: removeURL(makeURL(urlspec));
371: }
372:
373: /**
374: * @jmx:managed-operation
375: */
376: public boolean hasURL(final String urlspec)
377: throws MalformedURLException {
378: return hasURL(makeURL(urlspec));
379: }
380:
381: /**
382: * A helper to deploy the given URL with the deployer.
383: */
384: protected void deploy(final DeployedURL du) {
385: // If the deployer is null simply ignore the request
386: if (deployer == null)
387: return;
388:
389: try {
390: if (log.isTraceEnabled())
391: log.trace("Deploying: " + du);
392:
393: deployer.deploy(du.url);
394: } catch (IncompleteDeploymentException e) {
395: lastIncompleteDeploymentException = e;
396: } catch (Exception e) {
397: log.debug("Failed to deploy: " + du, e);
398: }
399:
400: du.deployed();
401:
402: if (!deployedSet.contains(du)) {
403: deployedSet.add(du);
404: }
405: }
406:
407: /**
408: * A helper to undeploy the given URL from the deployer.
409: */
410: protected void undeploy(final DeployedURL du) {
411: try {
412: if (log.isTraceEnabled())
413: log.trace("Undeploying: " + du);
414:
415: deployer.undeploy(du.url);
416: deployedSet.remove(du);
417: } catch (Exception e) {
418: log.error("Failed to undeploy: " + du, e);
419: }
420: }
421:
422: /**
423: * Checks if the url is in the deployed set.
424: */
425: protected boolean isDeployed(final URL url) {
426: DeployedURL du = new DeployedURL(url);
427: return deployedSet.contains(du);
428: }
429:
430: public synchronized void scan() throws Exception {
431: lastIncompleteDeploymentException = null;
432: if (urlList == null)
433: throw new IllegalStateException("not initialized");
434:
435: updateSorter();
436:
437: boolean trace = log.isTraceEnabled();
438: List urlsToDeploy = new LinkedList();
439:
440: // Scan for deployments
441: if (trace) {
442: log.trace("Scanning for new deployments");
443: }
444: synchronized (urlList) {
445: for (Iterator i = urlList.iterator(); i.hasNext();) {
446: URL url = (URL) i.next();
447: try {
448: if (url.toString().endsWith("/")) {
449: // treat URL as a collection
450: URLLister lister = listerFactory
451: .createURLLister(url);
452:
453: // listMembers() will throw an IOException if collection url does not exist
454: urlsToDeploy.addAll(lister.listMembers(url,
455: filter, doRecursiveSearch));
456: } else {
457: // treat URL as a deployable unit
458:
459: // throws IOException if this URL does not exist
460: url.openConnection().connect();
461: urlsToDeploy.add(url);
462: }
463: } catch (IOException e) {
464: // Either one of the configured URLs is bad, i.e. points to a non-existent
465: // location, or it ends with a '/' but it is not a directory (so it
466: // is really user's fault), OR some other hopefully transient I/O error
467: // happened (e.g. out of file descriptors?) so log a warning.
468: log.warn("Scan URL, caught "
469: + e.getClass().getName() + ": "
470: + e.getMessage());
471:
472: // We need to return because at least one of the listed URLs will
473: // return no results, and so all deployments starting from that point
474: // (e.g. deploy/) will get undeployed, see JBAS-3107.
475: // On the other hand, in case of a bad configuration nothing will get
476: // deployed. If really want independence of e.g. 2 deploy urls, more
477: // than one URLDeploymentScanners can be setup.
478: return;
479: }
480: }
481: }
482:
483: if (trace) {
484: log.trace("Updating existing deployments");
485: }
486: LinkedList urlsToRemove = new LinkedList();
487: LinkedList urlsToCheckForUpdate = new LinkedList();
488: synchronized (deployedSet) {
489: // remove previously deployed URLs no longer needed
490: for (Iterator i = deployedSet.iterator(); i.hasNext();) {
491: DeployedURL deployedURL = (DeployedURL) i.next();
492:
493: if (skipSet.contains(deployedURL.url)) {
494: if (trace)
495: log.trace("Skipping update/removal check for: "
496: + deployedURL.url);
497: } else {
498: if (urlsToDeploy.contains(deployedURL.url)) {
499: urlsToCheckForUpdate.add(deployedURL);
500: } else {
501: urlsToRemove.add(deployedURL);
502: }
503: }
504: }
505: }
506:
507: // ********
508: // Undeploy
509: // ********
510:
511: for (Iterator i = urlsToRemove.iterator(); i.hasNext();) {
512: DeployedURL deployedURL = (DeployedURL) i.next();
513: if (trace) {
514: log.trace("Removing " + deployedURL.url);
515: }
516: undeploy(deployedURL);
517: }
518:
519: // ********
520: // Redeploy
521: // ********
522:
523: // compute the DeployedURL list to update
524: ArrayList urlsToUpdate = new ArrayList(urlsToCheckForUpdate
525: .size());
526: for (Iterator i = urlsToCheckForUpdate.iterator(); i.hasNext();) {
527: DeployedURL deployedURL = (DeployedURL) i.next();
528: if (deployedURL.isModified()) {
529: if (trace) {
530: log.trace("Re-deploying " + deployedURL.url);
531: }
532: urlsToUpdate.add(deployedURL);
533: }
534: }
535:
536: // sort to update list
537: Collections.sort(urlsToUpdate, new Comparator() {
538: public int compare(Object o1, Object o2) {
539: return sorter.compare(((DeployedURL) o1).url,
540: ((DeployedURL) o2).url);
541: }
542: });
543:
544: // Undeploy in order
545: for (int i = urlsToUpdate.size() - 1; i >= 0; i--) {
546: undeploy((DeployedURL) urlsToUpdate.get(i));
547: }
548:
549: // Deploy in order
550: for (int i = 0; i < urlsToUpdate.size(); i++) {
551: deploy((DeployedURL) urlsToUpdate.get(i));
552: }
553:
554: // ******
555: // Deploy
556: // ******
557:
558: Collections.sort(urlsToDeploy, sorter);
559: for (Iterator i = urlsToDeploy.iterator(); i.hasNext();) {
560: URL url = (URL) i.next();
561: DeployedURL deployedURL = new DeployedURL(url);
562: if (deployedSet.contains(deployedURL) == false) {
563: if (skipSet.contains(url)) {
564: if (trace)
565: log.trace("Skipping deployment of: " + url);
566: } else {
567: if (trace)
568: log.trace("Deploying " + deployedURL.url);
569:
570: deploy(deployedURL);
571: }
572: }
573: i.remove();
574: // Check to see if mainDeployer suffix list has changed.
575: // if so, then resort
576: if (i.hasNext() && updateSorter()) {
577: Collections.sort(urlsToDeploy, sorter);
578: i = urlsToDeploy.iterator();
579: }
580: }
581:
582: // Validate that there are still incomplete deployments
583: if (lastIncompleteDeploymentException != null) {
584: try {
585: Object[] args = {};
586: String[] sig = {};
587: getServer().invoke(getDeployer(),
588: "checkIncompleteDeployments", args, sig);
589: } catch (Exception e) {
590: Throwable t = JMXExceptionDecoder.decode(e);
591: log.error(t);
592: }
593: }
594: }
595:
596: protected boolean updateSorter() {
597: // Check to see if mainDeployer suffix list has changed.
598: if (sorter instanceof DefaultDeploymentSorter) {
599: DefaultDeploymentSorter defaultSorter = (DefaultDeploymentSorter) sorter;
600: if (defaultSorter.getSuffixOrder() != mainDeployer
601: .getSuffixOrder()) {
602: defaultSorter.setSuffixOrder(mainDeployer
603: .getSuffixOrder());
604: return true;
605: }
606: }
607: return false;
608: }
609:
610: /////////////////////////////////////////////////////////////////////////
611: // Service/ServiceMBeanSupport //
612: /////////////////////////////////////////////////////////////////////////
613:
614: public ObjectName preRegister(MBeanServer server, ObjectName name)
615: throws Exception {
616: // get server's home for relative paths, need this for setting
617: // attribute final values, so we need to do it here
618: ServerConfig serverConfig = ServerConfigLocator.locate();
619: serverHome = serverConfig.getServerHomeDir();
620: serverHomeURL = serverConfig.getServerHomeURL();
621:
622: return super .preRegister(server, name);
623: }
624:
625: protected void createService() throws Exception {
626: // Perform a couple of sanity checks
627: if (this .filter == null) {
628: throw new IllegalStateException(
629: "'FilterInstance' attribute not configured");
630: }
631: if (this .sorter == null) {
632: throw new IllegalStateException(
633: "'URLComparator' attribute not configured");
634: }
635: // ok, proceed with normal createService()
636: super .createService();
637: }
638:
639: /////////////////////////////////////////////////////////////////////////
640: // DeployedURL //
641: /////////////////////////////////////////////////////////////////////////
642:
643: /**
644: * A container and help class for a deployed URL.
645: * should be static at this point, with the explicit scanner ref, but I'm (David) lazy.
646: */
647: protected class DeployedURL {
648: public URL url;
649: /** The url to check to decide if we need to redeploy */
650: public URL watchUrl;
651:
652: public long deployedLastModified;
653:
654: public DeployedURL(final URL url) {
655: this .url = url;
656: }
657:
658: public void deployed() {
659: deployedLastModified = getLastModified();
660: }
661:
662: public boolean isFile() {
663: return url.getProtocol().equals("file");
664: }
665:
666: public File getFile() {
667: return new File(url.getFile());
668: }
669:
670: public boolean isRemoved() {
671: if (isFile()) {
672: File file = getFile();
673: return !file.exists();
674: }
675: return false;
676: }
677:
678: public long getLastModified() {
679: if (watchUrl == null) {
680: try {
681: Object o = getServer().invoke(getDeployer(),
682: "getWatchUrl", new Object[] { url },
683: new String[] { URL.class.getName() });
684: watchUrl = o == null ? url : (URL) o;
685: getLog()
686: .debug(
687: "Watch URL for: " + url + " -> "
688: + watchUrl);
689: } catch (Exception e) {
690: watchUrl = url;
691: getLog().debug(
692: "Unable to obtain watchUrl from deployer. Use url: "
693: + url, e);
694: }
695: }
696:
697: try {
698: URLConnection connection;
699: if (watchUrl != null) {
700: connection = watchUrl.openConnection();
701: } else {
702: connection = url.openConnection();
703: }
704: // no need to do special checks for files...
705: // org.jboss.net.protocol.file.FileURLConnection correctly
706: // implements the getLastModified method.
707: long lastModified = connection.getLastModified();
708:
709: return lastModified;
710: } catch (java.io.IOException e) {
711: log.warn(
712: "Failed to check modification of deployed url: "
713: + url, e);
714: }
715: return -1;
716: }
717:
718: public boolean isModified() {
719: long lastModified = getLastModified();
720: if (lastModified == -1) {
721: // ignore errors fetching the timestamp - see bug 598335
722: return false;
723: }
724: return deployedLastModified != lastModified;
725: }
726:
727: public int hashCode() {
728: return url.hashCode();
729: }
730:
731: public boolean equals(final Object other) {
732: if (other instanceof DeployedURL) {
733: return ((DeployedURL) other).url.equals(this .url);
734: }
735: return false;
736: }
737:
738: public String toString() {
739: return super .toString() + "{ url=" + url
740: + ", deployedLastModified=" + deployedLastModified
741: + " }";
742: }
743: }
744: }
|