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