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;
23
24 import java.io.File;
25 import java.io.FileOutputStream;
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.net.URL;
29 import java.net.URLClassLoader;
30 import java.util.ArrayList;
31 import java.util.Enumeration;
32 import java.util.HashMap;
33 import java.util.Iterator;
34 import java.util.List;
35 import java.util.ListIterator;
36 import java.util.Map;
37 import java.util.jar.JarEntry;
38 import java.util.jar.JarFile;
39
40 import javax.management.MBeanServer;
41 import javax.management.MalformedObjectNameException;
42 import javax.management.ObjectName;
43 import javax.xml.parsers.DocumentBuilder;
44 import javax.xml.parsers.DocumentBuilderFactory;
45
46 import org.jboss.mx.loading.LoaderRepositoryFactory;
47 import org.jboss.mx.loading.LoaderRepositoryFactory.LoaderRepositoryConfig;
48 import org.jboss.mx.util.MBeanProxyExt;
49 import org.jboss.net.protocol.URLLister;
50 import org.jboss.net.protocol.URLListerFactory;
51 import org.jboss.system.ServiceControllerMBean;
52 import org.jboss.system.server.ServerConfig;
53 import org.jboss.system.server.ServerConfigLocator;
54 import org.jboss.util.StringPropertyReplacer;
55 import org.jboss.util.Strings;
56 import org.jboss.util.stream.Streams;
57 import org.jboss.util.xml.JBossEntityResolver;
58 import org.w3c.dom.Element;
59 import org.w3c.dom.Node;
60 import org.w3c.dom.NodeList;
61 import org.xml.sax.InputSource;
62
63 import EDU.oswego.cs.dl.util.concurrent.ConcurrentReaderHashMap;
64
65 /**
66 * This is the main Service Deployer API.
67 *
68 * @see org.jboss.system.Service
69 *
70 * @author <a href="mailto:marc.fleury@jboss.org">Marc Fleury</a>
71 * @author <a href="mailto:David.Maplesden@orion.co.nz">David Maplesden</a>
72 * @author <a href="mailto:d_jencks@users.sourceforge.net">David Jencks</a>
73 * @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
74 * @author <a href="mailto:sacha.labourey@cogito-info.ch">Sacha Labourey</a>
75 * @author <a href="mailto:scott.stark@jboss.org">Scott Stark</a>
76 * @author <a href="mailto:dimitris@jboss.org">Dimitris Andreadis<a/>
77 * @version $Revision: 57108 $
78 */
79 public class SARDeployer extends SubDeployerSupport
80 implements SARDeployerMBean
81 {
82 /** The suffixes we accept, along with their relative order */
83 private static final String[] DEFAULT_ENHANCED_SUFFIXES = new String[] {
84 "050:.deployer",
85 "050:-deployer.xml",
86 "150:.sar",
87 "150:-service.xml"
88 };
89
90 /** The deployment descriptor to look for */
91 private static final String JBOSS_SERVICE = "META-INF/jboss-service.xml";
92
93 /** A proxy to the ServiceController. */
94 private ServiceControllerMBean serviceController;
95
96 /** The server data directory. */
97 private File dataDir;
98
99 /** The server configuration base URL. For example,
100 file:/<jboss_dist_root>/server/default. Relative service
101 descriptor codebase elements are relative to this URL.
102 */
103 private URL serverHomeURL;
104
105 /** A HashMap<ObjectName, DeploymentInfo> for the deployed services */
106 private HashMap serviceDeploymentMap = new HashMap();
107
108 /**
109 * A Map<String, List<String>> of the suffix to accepted archive META-INF descriptor name
110 * @todo externalize this
111 */
112 private Map suffixToDescriptorMap = new ConcurrentReaderHashMap();
113
114 /** A flag indicating if the parser used for the service descriptor should be configured for namespaces */
115 private boolean useNamespaceAwareParser;
116
117 /**
118 * Default CTOR
119 */
120 public SARDeployer()
121 {
122 setEnhancedSuffixes(DEFAULT_ENHANCED_SUFFIXES);
123 // Add the .har to META-INF/{jboss-service.xml,hibernate-service.xml} mapping
124 ArrayList tmp = new ArrayList();
125 tmp.add(JBOSS_SERVICE);
126 tmp.add("META-INF/hibernate-service.xml");
127 suffixToDescriptorMap.put(".har", tmp);
128 }
129
130 public boolean isUseNamespaceAwareParser()
131 {
132 return useNamespaceAwareParser;
133 }
134
135 public void setUseNamespaceAwareParser(boolean useNamespaceAwareParser)
136 {
137 this.useNamespaceAwareParser = useNamespaceAwareParser;
138 }
139
140 /**
141 * Get the associated service DeploymentInfo if found, null otherwise
142 *
143 * @param serviceName a service object name
144 * @return The associated service DeploymentInfo if found, null otherwise
145 * @jmx.managed-operation
146 */
147 public DeploymentInfo getService(ObjectName serviceName)
148 {
149 DeploymentInfo di = null;
150 synchronized( serviceDeploymentMap )
151 {
152 di = (DeploymentInfo) serviceDeploymentMap.get(serviceName);
153 }
154 return di;
155 }
156
157 /**
158 * Describe <code>init</code> method here.
159 *
160 * @param di a <code>DeploymentInfo</code> value
161 * @exception DeploymentException if an error occurs
162 * @jmx.managed-operation
163 */
164 public void init(DeploymentInfo di)
165 throws DeploymentException
166 {
167 try
168 {
169 if (di.url.getPath().endsWith("/"))
170 {
171 // the URL is a unpacked collection, watch the deployment descriptor
172 di.watch = new URL(di.url, JBOSS_SERVICE);
173 }
174 else
175 {
176 // just watch the original URL
177 di.watch = di.url;
178 }
179
180 // Get the document if not already present
181 parseDocument(di);
182
183 // Check for a custom loader-repository for scoping
184 NodeList loaders = di.document.getElementsByTagName("loader-repository");
185 if( loaders.getLength() > 0 )
186 {
187 Element loader = (Element) loaders.item(0);
188 LoaderRepositoryConfig config = LoaderRepositoryFactory.parseRepositoryConfig(loader);
189 di.setRepositoryInfo(config);
190 }
191
192 // In case there is a dependent classpath defined parse it
193 parseXMLClasspath(di);
194
195 // Copy local directory if local-directory element is present
196 NodeList lds = di.document.getElementsByTagName("local-directory");
197 log.debug("about to copy " + lds.getLength() + " local directories");
198
199 for (int i = 0; i< lds.getLength(); i++)
200 {
201 Element ld = (Element)lds.item(i);
202 String path = ld.getAttribute("path");
203 log.debug("about to copy local directory at " + path);
204
205 // Get the url of the local copy from the classloader.
206 log.debug("copying from " + di.localUrl + path + " -> " + dataDir);
207
208 inflateJar(di.localUrl, dataDir, path);
209 }
210 }
211 catch (DeploymentException de)
212 {
213 throw de;
214 }
215 catch (Exception e)
216 {
217 throw new DeploymentException(e);
218 }
219
220 // invoke super-class initialization
221 super.init(di);
222 }
223
224 /**
225 * Describe <code>create</code> method here.
226 *
227 * @param di a <code>DeploymentInfo</code> value
228 * @exception DeploymentException if an error occurs
229 * @jmx.managed-operation
230 */
231 public void create(DeploymentInfo di)
232 throws DeploymentException
233 {
234 try
235 {
236 // install the MBeans in this descriptor
237 log.debug("Deploying SAR, create step: url " + di.url);
238
239 // Register the SAR UCL as an mbean so we can use it as the service loader
240 ObjectName uclName = di.ucl.getObjectName();
241 if( getServer().isRegistered(uclName) == false )
242 {
243 log.debug("Registering service UCL="+uclName);
244 getServer().registerMBean(di.ucl, uclName);
245 }
246
247 List mbeans = di.mbeans;
248 mbeans.clear();
249 List descriptorMbeans = serviceController.install(di.document.getDocumentElement(), uclName);
250 mbeans.addAll(descriptorMbeans);
251
252 // create the services
253 for (Iterator iter = di.mbeans.iterator(); iter.hasNext(); )
254 {
255 ObjectName service = (ObjectName)iter.next();
256
257 // The service won't be created until explicitly dependent mbeans are created
258 serviceController.create(service);
259 synchronized( this.serviceDeploymentMap )
260 {
261 serviceDeploymentMap.put(service, di);
262 }
263 }
264
265 // Generate a JMX notification for the create stage
266 super.create(di);
267 }
268 catch(DeploymentException e)
269 {
270 log.debug("create operation failed for package "+ di.url, e);
271 destroy(di);
272 throw e;
273 }
274 catch (Exception e)
275 {
276 log.debug("create operation failed for package "+ di.url, e);
277 destroy(di);
278 throw new DeploymentException("create operation failed for package "
279 + di.url, e);
280 }
281 }
282
283 /**
284 * The <code>start</code> method starts all the mbeans in this DeploymentInfo..
285 *
286 * @param di a <code>DeploymentInfo</code> value
287 * @exception DeploymentException if an error occurs
288 * @jmx.managed-operation
289 */
290 public void start(DeploymentInfo di) throws DeploymentException
291 {
292 log.debug("Deploying SAR, start step: url " + di.url);
293 try
294 {
295 // start the services
296
297 for (Iterator iter = di.mbeans.iterator(); iter.hasNext(); )
298 {
299 ObjectName service = (ObjectName)iter.next();
300
301 // The service won't be started until explicitely dependent mbeans are started
302 serviceController.start(service);
303 }
304 // Generate a JMX notification for the start stage
305 super.start(di);
306 }
307 catch (Exception e)
308 {
309 stop(di);
310 destroy(di);
311 throw new DeploymentException("start operation failed on package "
312 + di.url, e);
313 }
314 }
315
316 /** The stop method invokes stop on the mbeans associatedw ith the deployment
317 * in reverse order relative to create.
318 *
319 * @param di the <code>DeploymentInfo</code> value to stop.
320 * @jmx.managed-operation
321 */
322 public void stop(DeploymentInfo di)
323 {
324 log.debug("undeploying document " + di.url);
325
326 List services = di.mbeans;
327 int lastService = services.size();
328
329 // stop services in reverse order.
330 for (ListIterator i = services.listIterator(lastService); i.hasPrevious();)
331 {
332 ObjectName name = (ObjectName)i.previous();
333 log.debug("stopping mbean " + name);
334 try
335 {
336 serviceController.stop(name);
337 }
338 catch (Exception e)
339 {
340 log.error("Could not stop mbean: " + name, e);
341 } // end of try-catch
342 }
343
344 // Generate a JMX notification for the stop stage
345 try
346 {
347 super.stop(di);
348 }
349 catch(Exception ignore)
350 {
351 }
352 }
353
354 /** The destroy method invokes destroy on the mbeans associated with
355 * the deployment in reverse order relative to create.
356 *
357 * @param di a <code>DeploymentInfo</code> value
358 * @jmx.managed-operation
359 */
360 public void destroy(DeploymentInfo di)
361 {
362 List services = di.mbeans;
363 int lastService = services.size();
364
365 for (ListIterator i = services.listIterator(lastService); i.hasPrevious();)
366 {
367 ObjectName name = (ObjectName)i.previous();
368 log.debug("destroying mbean " + name);
369 synchronized( serviceDeploymentMap )
370 {
371 serviceDeploymentMap.remove(name);
372 }
373
374 try
375 {
376 serviceController.destroy(name);
377 }
378 catch (Exception e)
379 {
380 log.error("Could not destroy mbean: " + name, e);
381 } // end of try-catch
382 }
383
384 for (ListIterator i = services.listIterator(lastService); i.hasPrevious();)
385 {
386 ObjectName name = (ObjectName)i.previous();
387 log.debug("removing mbean " + name);
388 try
389 {
390 serviceController.remove(name);
391 }
392 catch (Exception e)
393 {
394 log.error("Could not remove mbean: " + name, e);
395 } // end of try-catch
396 }
397
398 // Unregister the SAR UCL
399 try
400 {
401 ObjectName uclName = di.ucl.getObjectName();
402 if( getServer().isRegistered(uclName) == true )
403 {
404 log.debug("Unregistering service UCL="+uclName);
405 getServer().unregisterMBean(uclName);
406 }
407 }
408 catch(Exception ignore)
409 {
410 }
411
412 // Generate a JMX notification for the destroy stage
413 try
414 {
415 super.destroy(di);
416 }
417 catch(Exception ignore)
418 {
419 }
420 }
421
422 // ServiceMBeanSupport overrides --------------------------------
423
424 /**
425 * The startService method gets the mbeanProxies for MainDeployer
426 * and ServiceController, used elsewhere.
427 *
428 * @exception Exception if an error occurs
429 */
430 protected void startService() throws Exception
431 {
432 super.startService();
433
434 // get the controller proxy
435 serviceController = (ServiceControllerMBean)
436 MBeanProxyExt.create(ServiceControllerMBean.class,
437 ServiceControllerMBean.OBJECT_NAME, server);
438
439 // Get the data directory, install url & library url
440 ServerConfig config = ServerConfigLocator.locate();
441 dataDir = config.getServerDataDir();
442 serverHomeURL = config.getServerHomeURL();
443 }
444
445 /**
446 * This method stops all the applications in this server.
447 */
448 protected void stopService() throws Exception
449 {
450 // deregister with MainDeployer
451 super.stopService();
452
453 // Help GC
454 serviceController = null;
455 serverHomeURL = null;
456 dataDir = null;
457 }
458
459
460 protected ObjectName getObjectName(MBeanServer server, ObjectName name)
461 throws MalformedObjectNameException
462 {
463 return name == null ? OBJECT_NAME : name;
464 }
465
466 // Protected -----------------------------------------------------
467
468 protected File[] listFiles(final String urlspec) throws Exception
469 {
470 URL url = Strings.toURL(urlspec);
471
472 // url is already canonical thanks to Strings.toURL
473 File dir = new File(url.getFile());
474
475 File[] files = dir.listFiles(new java.io.FileFilter()
476 {
477 public boolean accept(File pathname)
478 {
479 String name = pathname.getName().toLowerCase();
480 return (name.endsWith(".jar") || name.endsWith(".zip"));
481 }
482 });
483
484 return files;
485 }
486
487 /**
488 * @param di
489 * @throws Exception
490 */
491 protected void parseXMLClasspath(DeploymentInfo di)
492 throws Exception
493 {
494 ArrayList classpath = new ArrayList();
495 URLListerFactory listerFactory = new URLListerFactory();
496
497 NodeList children = di.document.getDocumentElement().getChildNodes();
498 for (int i = 0; i < children.getLength(); i++)
499 {
500 if (children.item(i).getNodeType() == Node.ELEMENT_NODE)
501 {
502 Element classpathElement = (Element)children.item(i);
503 if (classpathElement.getTagName().equals("classpath"))
504 {
505 log.debug("Found classpath element: " + classpathElement);
506 if (!classpathElement.hasAttribute("codebase"))
507 {
508 throw new DeploymentException
509 ("Invalid classpath element missing codebase: " + classpathElement);
510 }
511 String codebase = classpathElement.getAttribute("codebase").trim();
512 // Replace any system property references like ${x}
513 codebase = StringPropertyReplacer.replaceProperties(codebase);
514
515 String archives = null;
516 if (classpathElement.hasAttribute("archives"))
517 {
518 archives = classpathElement.getAttribute("archives").trim();
519 // Replace any system property references like ${x}
520 archives = StringPropertyReplacer.replaceProperties(archives);
521 if ("".equals(archives))
522 {
523 archives = null;
524 }
525 }
526
527 // Convert codebase to a URL
528 // "." is resolved relative to the deployment
529 // other URLs are resolved relative to SERVER_HOME
530 URL codebaseUrl;
531 if (".".equals(codebase))
532 {
533 codebaseUrl = new URL(di.url, "./");
534 }
535 else
536 {
537 if (archives != null && codebase.endsWith("/") == false)
538 {
539 codebase += "/";
540 }
541 codebaseUrl = new URL(serverHomeURL, codebase);
542 }
543 log.debug("codebase URL is " + codebaseUrl);
544
545 if (archives == null)
546 {
547 // archives not supplied so add the codebase itself
548 classpath.add(codebaseUrl);
549 log.debug("added codebase to classpath");
550 }
551 else
552 {
553 // obtain a URLLister for the codebase and use it to obtain
554 // the list of URLs to add
555 log.debug("listing codebase for archives matching " + archives);
556 URLLister lister = listerFactory.createURLLister(codebaseUrl);
557 log.debug("URLLister class is " + lister.getClass().getName());
558 classpath.addAll(lister.listMembers(codebaseUrl, archives));
559 }
560 } // end of if ()
561
562 } // end of if ()
563 } //end of for
564
565 // Ok, now we've found the list of urls we need... deploy their classes.
566 Iterator jars = classpath.iterator();
567 while (jars.hasNext())
568 {
569 URL neededURL = (URL) jars.next();
570 di.addLibraryJar(neededURL);
571 log.debug("deployed classes for " + neededURL);
572 }
573 }
574
575 /** Parse the META-INF/jboss-service.xml descriptor
576 */
577 protected void parseDocument(DeploymentInfo di)
578 throws Exception
579 {
580 InputStream stream = null;
581 try
582 {
583 if (di.document == null)
584 {
585 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
586 factory.setNamespaceAware(useNamespaceAwareParser);
587 DocumentBuilder parser = factory.newDocumentBuilder();
588 URL docURL = di.localUrl;
589 URLClassLoader localCL = di.localCl;
590 // Load jboss-service.xml from the jar or directory
591 if (di.isXML == false)
592 {
593 // Check the suffix to descriptor mapping
594 String[] descriptors = getDescriptorName(di);
595 for(int n = 0; n < descriptors.length; n ++)
596 {
597 String descriptor = descriptors[n];
598 docURL = localCL.findResource(descriptor);
599 if( docURL != null )
600 {
601 // If this is a unpacked deployment, update the watch url
602 if (di.url.getPath().endsWith("/"))
603 {
604 di.watch = new URL(di.url, descriptor);
605 log.debug("Updated watch URL to: "+di.watch);
606 }
607 break;
608 }
609 }
610 // No descriptors, use the default META-INF/jboss-service.xml
611 if( docURL == null )
612 docURL = localCL.findResource(JBOSS_SERVICE);
613 }
614 // Validate that the descriptor was found
615 if (docURL == null)
616 throw new DeploymentException("Failed to find META-INF/jboss-service.xml for archive " + di.shortName);
617
618 stream = docURL.openStream();
619 InputSource is = new InputSource(stream);
620 is.setSystemId(docURL.toString());
621 parser.setEntityResolver(new JBossEntityResolver());
622 di.document = parser.parse(is);
623 }
624 else
625 {
626 log.debug("Using existing deployment.document");
627 }
628 }
629 finally
630 {
631 // Close the stream to get around "Too many open files"-errors
632 try
633 {
634 stream.close();
635 }
636 catch (Exception ignore)
637 {
638 }
639 }
640 }
641
642 /**
643 * The <code>inflateJar</code> copies the jar entries
644 * from the jar url jarUrl to the directory destDir.
645 * It can be used on the whole jar, a directory, or
646 * a specific file in the jar.
647 *
648 * @param url the <code>URL</code> if the directory or entry to copy.
649 * @param destDir the <code>File</code> value of the directory in which to
650 * place the inflated copies.
651 *
652 * @exception DeploymentException if an error occurs
653 * @exception IOException if an error occurs
654 */
655 protected void inflateJar(URL url, File destDir, String path)
656 throws DeploymentException, IOException
657 {
658 String filename = url.getFile();
659 JarFile jarFile = new JarFile(filename);
660 try
661 {
662 for (Enumeration e = jarFile.entries(); e.hasMoreElements(); )
663 {
664 JarEntry entry = (JarEntry)e.nextElement();
665 String name = entry.getName();
666
667 if (path == null || name.startsWith(path))
668 {
669 File outFile = new File(destDir, name);
670 if (!outFile.exists())
671 {
672 if (entry.isDirectory())
673 {
674 outFile.mkdirs();
675 }
676 else
677 {
678 Streams.copyb(jarFile.getInputStream(entry),
679 new FileOutputStream(outFile));
680 }
681 } // end of if (outFile.exists())
682 } // end of if (matches path)
683 }
684 }
685 finally
686 {
687 jarFile.close();
688 }
689 }
690
691 // Private -------------------------------------------------------
692
693 /**
694 * Parse the deployment url for its suffix and return a list of the accepted service
695 * descriptor names to look for.
696 *
697 * @param sdi - the sar deployment info
698 * @return the array of sar archive/directory relative names of the service descriptor. If
699 * there is no suffix to descriptor mapping, the default of {JBOSS_SERVICE} will be
700 * returned.
701 */
702 private String[] getDescriptorName(DeploymentInfo sdi)
703 {
704 String[] descriptorNames = {JBOSS_SERVICE};
705 String shortName = sdi.shortName;
706 int dot = shortName.lastIndexOf('.');
707 if( dot >= 0 )
708 {
709 String suffix = shortName.substring(dot);
710 List descriptors = (List) suffixToDescriptorMap.get(suffix);
711 if( descriptors != null )
712 {
713 descriptorNames = new String[descriptors.size()];
714 descriptors.toArray(descriptorNames);
715 }
716 }
717 return descriptorNames;
718 }
719 }