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.FileFilter;
026: import java.io.IOException;
027: import java.net.MalformedURLException;
028: import java.net.URL;
029: import java.util.Arrays;
030: import java.util.ArrayList;
031: import java.util.Comparator;
032: import java.util.HashMap;
033: import java.util.Iterator;
034: import javax.management.MBeanServer;
035: import javax.management.ObjectName;
036: import org.w3c.dom.Element;
037: import org.w3c.dom.NamedNodeMap;
038: import org.w3c.dom.Node;
039: import org.w3c.dom.NodeList;
040:
041: import org.jboss.deployment.IncompleteDeploymentException;
042: import org.jboss.deployment.Deployer;
043: import org.jboss.deployment.DeploymentException;
044: import org.jboss.system.server.ServerConfigLocator;
045:
046: /**
047: * This class is similar to the URLDeploymentScanner (
048: * @see org.jboss.deployment.scanner.URLDeploymentScanner). It is designed
049: * to deploy direct URLs and to scan directories. The distinction between
050: * the two is that this class forces the user to specify which entries are
051: * directories to scan, and which are urls to deploy.
052: *
053: * @jmx:mbean extends="org.jboss.deployment.scanner.DeploymentScannerMBean"
054: *
055: * @version
056: * @author <a href="mailto:lsanders@speakeasy.net">Larry Sanderson</a>
057: */
058: public class URLDirectoryScanner extends AbstractDeploymentScanner
059: implements DeploymentScanner, URLDirectoryScannerMBean {
060:
061: /** This is the default file filter for directory scanning */
062: private FileFilter defaultFilter;
063:
064: /** This is the default Comparator for directory deployment ordering */
065: private Comparator defaultComparator;
066:
067: /** This is the list of scanner objects */
068: private ArrayList scanners = new ArrayList();
069:
070: /** This is a URL to scanner map. */
071: private HashMap urlScannerMap = new HashMap();
072:
073: /** this is used for resolution of reletive file paths */
074: private File serverHome;
075:
076: // TS
077: protected IncompleteDeploymentException lastIncompleteDeploymentException;
078:
079: /**
080: * @jmx:managed-attribute
081: */
082: public void addScanURL(URL url) {
083: Scanner scanner = new Scanner(url);
084: addScanner(scanner);
085: }
086:
087: /**
088: * @jmx:managed-attribute
089: */
090: public void addScanURL(String url) throws MalformedURLException {
091: addScanURL(toUrl(url));
092: }
093:
094: /**
095: * @jmx:managed-attribute
096: */
097: public void addScanDir(URL url, Comparator comp, FileFilter filter) {
098: Scanner scanner = new DirScanner(url, comp, filter);
099: addScanner(scanner);
100: }
101:
102: /**
103: * @jmx:managed-attribute
104: */
105: public void addScanDir(String urlSpec, String compClassName,
106: String filterClassName) throws MalformedURLException {
107:
108: URL url = toUrl(urlSpec);
109: // create a new comparator
110: Comparator comp = null;
111: if (compClassName != null) {
112: try {
113: Class compClass = Thread.currentThread()
114: .getContextClassLoader().loadClass(
115: compClassName);
116: comp = (Comparator) compClass.newInstance();
117: } catch (Exception e) {
118: log
119: .warn(
120: "Unable to create instance of Comparator. Ignoring.",
121: e);
122: }
123: }
124: // create a new filter
125: FileFilter filter = null;
126: if (filterClassName != null) {
127: try {
128: Class filterClass = Thread.currentThread()
129: .getContextClassLoader().loadClass(
130: filterClassName);
131: filter = (FileFilter) filterClass.newInstance();
132: } catch (Exception e) {
133: log
134: .warn(
135: "Unable to create instance of Filter. Ignoring.",
136: e);
137: }
138: }
139:
140: addScanDir(url, comp, filter);
141: }
142:
143: private void addScanner(Scanner scanner) {
144: synchronized (scanners) {
145: if (isScanEnabled()) {
146: // Scan is enabled, so apply changes to a local copy
147: // this enables the scan to occur while things are added
148: ArrayList localScanners = new ArrayList(scanners);
149: HashMap localMap = new HashMap(urlScannerMap);
150:
151: localScanners.add(scanner);
152: localMap.put(scanner.url, scanner);
153:
154: scanners = localScanners;
155: urlScannerMap = localMap;
156: } else {
157: // no need for precautions... just add
158: scanners.add(scanner);
159: urlScannerMap.put(scanner.url, scanner);
160: }
161: }
162: }
163:
164: /**
165: * @jmx:managed-attribute
166: */
167: public void removeScanURL(URL url) {
168: synchronized (scanners) {
169: if (isScanEnabled()) {
170: // Scan is enabled, so apply changes to a local copy
171: // this enables the scan to occur while things are added
172: ArrayList localScanners = new ArrayList(scanners);
173: HashMap localMap = new HashMap(urlScannerMap);
174:
175: Scanner scanner = (Scanner) localMap.remove(url);
176: if (scanner != null) {
177: localScanners.remove(scanner);
178: }
179:
180: scanners = localScanners;
181: urlScannerMap = localMap;
182: } else {
183: // no need for precautions... just remove
184: Scanner scanner = (Scanner) urlScannerMap.remove(url);
185: if (scanner != null) {
186: scanners.remove(scanner);
187: }
188: }
189: }
190: }
191:
192: /**
193: * @jmx:managed-attribute
194: */
195: public void setURLs(Element elem) {
196: NodeList list = elem.getChildNodes();
197: synchronized (scanners) {
198: // clear lists
199: scanners.clear();
200: urlScannerMap.clear();
201:
202: // populate from xml....
203: for (int i = 0; i < list.getLength(); i++) {
204: Node node = list.item(i);
205: if (node.getNodeType() == Node.ELEMENT_NODE) {
206: NamedNodeMap nodeMap = node.getAttributes();
207: String name = getNodeValue(nodeMap
208: .getNamedItem("name"));
209: if (name == null) {
210: log
211: .warn("No name specified in URLDirectoryScanner config: "
212: + node + ". Ignoring");
213: continue;
214: }
215:
216: try {
217: if (node.getNodeName().equals("dir")) {
218: // get the filter and comparator
219: String filter = getNodeValue(nodeMap
220: .getNamedItem("filter"));
221: String comp = getNodeValue(nodeMap
222: .getNamedItem("comparator"));
223:
224: addScanDir(name, comp, filter);
225: } else if (node.getNodeName().equals("url")) {
226: addScanURL(name);
227: }
228: } catch (MalformedURLException e) {
229: log
230: .warn(
231: "Invalid url in DeploymentScanner. Ignoring.",
232: e);
233: }
234: }
235: }
236: }
237: }
238:
239: /**
240: * Extract a string from a node. Trim the value (down to null for empties).
241: */
242: private String getNodeValue(Node node) {
243: if (node == null) {
244: return null;
245: }
246: String val = node.getNodeValue();
247: if (val == null) {
248: return null;
249: }
250: if ((val = val.trim()).length() == 0) {
251: return null;
252: }
253: return val;
254: }
255:
256: /**
257: * Convert a string to a node. I really just copied the code from
258: * Jason Dillon's URLDeploymentScanner. Thanks Jason!
259: */
260: private URL toUrl(String value) throws MalformedURLException {
261: try {
262: return new URL(value);
263: } catch (MalformedURLException e) {
264: File file = new File(value);
265: if (!file.isAbsolute()) {
266: file = new File(serverHome, value);
267: }
268:
269: try {
270: file = file.getCanonicalFile();
271: } catch (IOException ioe) {
272: throw new MalformedURLException("Can not obtain file: "
273: + ioe);
274: }
275:
276: return file.toURL();
277: }
278: }
279:
280: /**
281: * @jmx:managed-attribute
282: */
283: public void setURLComparator(String comparatorClassName) {
284: log.debug("Setting Comparator: " + comparatorClassName);
285: try {
286: defaultComparator = (Comparator) Thread.currentThread()
287: .getContextClassLoader().loadClass(
288: comparatorClassName).newInstance();
289: } catch (Exception e) {
290: log.warn("Unable to create URLComparator.", e);
291: }
292: }
293:
294: /**
295: * @jmx:managed-attribute
296: */
297: public String getURLComparator() {
298: if (defaultComparator == null) {
299: return null;
300: }
301: return defaultComparator.getClass().getName();
302: }
303:
304: /**
305: * @jmx:managed-attribute
306: */
307: public void setFilter(String filterClassName) {
308: log.debug("Setting Filter: " + filterClassName);
309: try {
310: defaultFilter = (FileFilter) Thread.currentThread()
311: .getContextClassLoader().loadClass(filterClassName)
312: .newInstance();
313: } catch (Exception e) {
314: log.warn("Unable to create URLComparator.", e);
315: }
316: }
317:
318: /**
319: * @jmx:managed-attribute
320: */
321: public String getFilter() {
322: if (defaultFilter == null) {
323: return null;
324: }
325: return defaultFilter.getClass().getName();
326: }
327:
328: /**
329: * This is a workaround for a bug in Sun's JVM 1.3 on windows (any
330: * others??). Inner classes can not directly access protected members
331: * from the outer-class's super class.
332: */
333: Deployer getDeployerObj() {
334: return deployer;
335: }
336:
337: /**
338: * This class scans a single url for modifications. It supports
339: * missing url's, and will deploy them when they appear.
340: */
341: private class Scanner {
342: /** the original url to scan */
343: protected URL url;
344:
345: /** the url to watch for modification */
346: private URL watchUrl;
347:
348: /** this holds the lastModified time of watchUrl */
349: private long lastModified;
350:
351: /** this is a flag to indicate if this url is deployed */
352: private boolean deployed;
353:
354: /**
355: * Construct with the url to deploy / watch
356: */
357: public Scanner(URL url) {
358: this .url = url;
359: }
360:
361: /**
362: * Check the url for modification, and deploy / redeploy / undeploy
363: * appropriately.
364: */
365: public void scan() {
366: if (getLog().isTraceEnabled()) {
367: getLog().trace("Scanning url: " + url);
368: }
369: // check time stamps
370: if (lastModified != getLastModified()) {
371: if (exists()) {
372: // url needs deploy / redeploy
373: try {
374: getLog().debug(
375: "Deploying Modified (or new) url: "
376: + url);
377: deploy();
378: // TS
379: } catch (IncompleteDeploymentException e) {
380: lastIncompleteDeploymentException = e;
381: } catch (DeploymentException e) {
382: getLog().error(
383: "Failed to (re)deploy url: " + url, e);
384: }
385: } else {
386: // url does not exist... undeploy
387: try {
388: getLog().debug("Undeploying url: " + url);
389: undeploy();
390: } catch (DeploymentException e) {
391: getLog().error(
392: "Failed to undeploy url: " + url, e);
393: }
394: }
395: }
396: }
397:
398: /**
399: * Return true if the url exists, false otherwise.
400: */
401: private boolean exists() {
402: try {
403: if (url.getProtocol().equals("file")) {
404: File file = new File(url.getPath());
405: return file.exists();
406: } else {
407: url.openStream().close();
408: return true;
409: }
410: } catch (IOException e) {
411: return false;
412: }
413: }
414:
415: /**
416: * return the modification date of watchUrl
417: */
418: private long getLastModified() {
419: try {
420: URL lmUrl = watchUrl == null ? url : watchUrl;
421: return lmUrl.openConnection().getLastModified();
422: } catch (IOException e) {
423: return 0L;
424: }
425: }
426:
427: /**
428: * (Re)deploy the url. This will undeploy the url first, if it is
429: * already deployed. It also fetches
430: */
431: private void deploy() throws DeploymentException {
432: if (deployed) {
433: // already deployed... undeploy first
434: getDeployerObj().undeploy(url);
435: }
436: getDeployerObj().deploy(url);
437:
438: // reset the watch url
439: try {
440: Object o = getServer().invoke(getDeployer(),
441: "getWatchUrl", new Object[] { url },
442: new String[] { URL.class.getName() });
443: watchUrl = o == null ? url : (URL) o;
444:
445: getLog().debug(
446: "Watch URL for: " + url + " -> " + watchUrl);
447: } catch (Exception e) {
448: watchUrl = url;
449: getLog().debug(
450: "Unable to obtain watchUrl from deployer. "
451: + "Use url: " + url, e);
452: }
453:
454: // the watchurl may have changed... get a new lastModified
455: lastModified = getLastModified();
456:
457: // set the deployed flag
458: deployed = true;
459: }
460:
461: /**
462: * Undeploy the url (if deployed).
463: */
464: private void undeploy() throws DeploymentException {
465: if (!deployed) {
466: return;
467: }
468: getDeployerObj().undeploy(url);
469: // reset the other fields
470: deployed = false;
471: lastModified = 0L;
472: watchUrl = null;
473: }
474: }
475:
476: /**
477: * This scanner scans a full directory instead of a single file.
478: */
479: private class DirScanner extends Scanner {
480: /** the directory to scan */
481: private File dir;
482:
483: /** the filter to use while scanning */
484: private FileFilter filter;
485:
486: /** The comparator for deployment ordering */
487: private Comparator comp;
488:
489: /** The list of currently deployed Scanners */
490: private ArrayList deployed = new ArrayList();
491:
492: /** Set up the URL, filter, and comparator to use for this scanner */
493: public DirScanner(URL url, Comparator comp, FileFilter filter) {
494: super (url);
495: if (!url.getProtocol().equals("file")) {
496: throw new IllegalArgumentException(
497: "Urls for directory "
498: + "scanning must use the file: protocol.");
499: }
500: dir = new File(url.getPath()).getAbsoluteFile();
501:
502: this .filter = filter == null ? defaultFilter : filter;
503: this .comp = new FileComparator(
504: comp == null ? defaultComparator : comp);
505: }
506:
507: /**
508: * Scan the directory for modifications / additions / removals.
509: */
510: public void scan() {
511: // check the dir for existence and file-type
512: if (!dir.exists()) {
513: getLog().warn(
514: "The director to scan does not exist: " + dir);
515: return;
516: }
517: if (!dir.isDirectory()) {
518: getLog().warn(
519: "The directory to scan is not a directory: "
520: + dir);
521: return;
522: }
523:
524: // get a sorted list of files in the directory
525: File[] files;
526: if (filter == null) {
527: files = dir.listFiles();
528: } else {
529: files = dir.listFiles(filter);
530: }
531: Arrays.sort(files, comp);
532:
533: // step through the two sorted lists: files and deployed
534: int deployedIndex = 0;
535: int fileIndex = 0;
536:
537: while (true) {
538: // get the current scanner and file
539: Scanner scanner = null;
540: if (deployedIndex < deployed.size()) {
541: scanner = (Scanner) deployed.get(deployedIndex);
542: }
543: File file = null;
544: if (fileIndex < files.length) {
545: file = files[fileIndex];
546: }
547:
548: // if both scanner and file are null, we are done
549: if (scanner == null && file == null) {
550: break;
551: }
552:
553: // determine if this is a new / old / or removed file, and
554: // take the appropriate action
555: switch (comp.compare(file, scanner == null ? null
556: : scanner.url)) {
557: case -1: // the file is new. Create the scanner
558: getLog().debug(
559: "Found new deployable application in directory "
560: + dir + ": " + file);
561: try {
562: scanner = new Scanner(file.toURL());
563: deployed.add(deployedIndex, scanner);
564: } catch (MalformedURLException e) {
565: getLog().warn(
566: "Unable to obtain URL for file: "
567: + file, e);
568: fileIndex++;
569: }
570: // do not break! Intentionally fall through to normal scan.
571: case 0: // the file is already deployed. Scan it.
572: scanner.scan();
573: deployedIndex++;
574: fileIndex++;
575: break;
576: case 1: // the file has been removed. A normal scan will remove it
577: getLog().debug(
578: "Deployed application removed from directory "
579: + dir + ": " + file);
580: scanner.scan();
581: if (!scanner.deployed) {
582: // the undeploy succeded... remove from deployed list
583: deployed.remove(deployedIndex);
584: } else {
585: deployedIndex++;
586: }
587: break;
588: }
589: }
590: }
591: }
592:
593: /**
594: * This comparator is used by the dirScanner. It compares two url's
595: * (or Files) using the specified urlComparator. In the case of a tie,
596: * it then uses File's natural ordering. Finally, it normalizes all
597: * compare values so that "less-than" is always -1, "greater-than" is
598: * always 1, and "equals" is always 0.
599: */
600: private static class FileComparator implements Comparator {
601: /** the delegated URL comparator */
602: private Comparator urlComparator;
603:
604: /** Construct with a (possibly null) URL comparator */
605: public FileComparator(Comparator urlComparator) {
606: this .urlComparator = urlComparator;
607: }
608:
609: /**
610: * Compare all non-nulls as less than nulls. Next, compare as URL's
611: * using the delegated comparator. And finally, use File's natural
612: * ordering.
613: */
614: public int compare(Object o1, Object o2) {
615: // catch nulls
616: if (o1 == o2) {
617: return 0;
618: }
619: if (o1 == null) {
620: return 1;
621: }
622: if (o2 == null) {
623: return -1;
624: }
625:
626: // obtain the File and URL objects pertaining to each argument
627: File file1;
628: File file2;
629: URL url1;
630: URL url2;
631:
632: if (o1 instanceof URL) {
633: url1 = (URL) o1;
634: file1 = new File(url1.getPath());
635: } else {
636: file1 = (File) o1;
637: try {
638: url1 = file1.toURL();
639: } catch (MalformedURLException e) {
640: throw new IllegalStateException(
641: "Unable to create file url: " + file1
642: + ": " + e);
643: }
644: }
645: if (o2 instanceof URL) {
646: url2 = (URL) o2;
647: file2 = new File(url2.getPath());
648: } else {
649: file2 = (File) o2;
650: try {
651: url2 = file2.toURL();
652: } catch (MalformedURLException e) {
653: throw new IllegalStateException(
654: "Unable to create file url: " + file2
655: + ": " + e);
656: }
657: }
658:
659: // first, use the delegate URL comparator
660: int comp = 0;
661: if (urlComparator != null) {
662: comp = urlComparator.compare(url1, url2);
663: }
664:
665: // If equal, break ties with File's natural ordering
666: if (comp == 0) {
667: comp = file1.compareTo(file2);
668: }
669:
670: // normalize the comp value to -1, 0, 1
671: return comp < 0 ? -1 : comp > 0 ? 1 : 0;
672: }
673: }
674:
675: /**
676: * Scan all urls.
677: */
678: public void scan() {
679: log.trace("Scanning urls...");
680:
681: // just scan all the scanners
682: for (Iterator iter = scanners.iterator(); iter.hasNext();) {
683: ((Scanner) iter.next()).scan();
684: }
685: // TS
686: // Validate that there are still incomplete deployments
687: if (lastIncompleteDeploymentException != null) {
688: try {
689: Object[] args = {};
690: String[] sig = {};
691: getServer().invoke(getDeployer(),
692: "checkIncompleteDeployments", args, sig);
693: } catch (Exception e) {
694: log.error(e);
695: }
696: }
697: }
698:
699: /**
700: * Obtain the Service values. This was copied from Jason Dillons
701: * URLDeploymentScanner. Thanks Jason!
702: */
703: public ObjectName preRegister(MBeanServer server, ObjectName name)
704: throws Exception {
705:
706: // get server's home for relative paths, need this for setting
707: // attribute final values, so we need todo it here
708: serverHome = ServerConfigLocator.locate().getServerHomeDir();
709:
710: return super.preRegister(server, name);
711: }
712: }
|