1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18
19 package org.apache.catalina.startup;
20
21
22 import java.io.BufferedOutputStream;
23 import java.io.File;
24 import java.io.FileInputStream;
25 import java.io.FileOutputStream;
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.io.OutputStream;
29 import java.util.ArrayList;
30 import java.util.HashMap;
31 import java.util.HashSet;
32 import java.util.LinkedHashMap;
33 import java.util.Set;
34 import java.util.jar.JarEntry;
35 import java.util.jar.JarFile;
36
37 import javax.management.ObjectName;
38
39 import org.apache.catalina.Container;
40 import org.apache.catalina.Context;
41 import org.apache.catalina.Engine;
42 import org.apache.catalina.Host;
43 import org.apache.catalina.Lifecycle;
44 import org.apache.catalina.LifecycleEvent;
45 import org.apache.catalina.LifecycleListener;
46 import org.apache.catalina.core.ContainerBase;
47 import org.apache.catalina.core.StandardHost;
48 import org.apache.catalina.util.IOTools;
49 import org.apache.catalina.util.StringManager;
50 import org.apache.tomcat.util.digester.Digester;
51 import org.apache.tomcat.util.modeler.Registry;
52
53
54 /**
55 * Startup event listener for a <b>Host</b> that configures the properties
56 * of that Host, and the associated defined contexts.
57 *
58 * @author Craig R. McClanahan
59 * @author Remy Maucherat
60 * @version $Revision: 892815 $ $Date: 2009-12-21 14:27:57 +0100 (Mon, 21 Dec 2009) $
61 */
62 public class HostConfig
63 implements LifecycleListener {
64
65 protected static org.apache.juli.logging.Log log=
66 org.apache.juli.logging.LogFactory.getLog( HostConfig.class );
67
68 // ----------------------------------------------------- Instance Variables
69
70
71 /**
72 * App base.
73 */
74 protected File appBase = null;
75
76
77 /**
78 * Config base.
79 */
80 protected File configBase = null;
81
82
83 /**
84 * The Java class name of the Context configuration class we should use.
85 */
86 protected String configClass = "org.apache.catalina.startup.ContextConfig";
87
88
89 /**
90 * The Java class name of the Context implementation we should use.
91 */
92 protected String contextClass = "org.apache.catalina.core.StandardContext";
93
94
95 /**
96 * The Host we are associated with.
97 */
98 protected Host host = null;
99
100
101 /**
102 * The JMX ObjectName of this component.
103 */
104 protected ObjectName oname = null;
105
106
107 /**
108 * The string resources for this package.
109 */
110 protected static final StringManager sm =
111 StringManager.getManager(Constants.Package);
112
113
114 /**
115 * Should we deploy XML Context config files?
116 */
117 protected boolean deployXML = false;
118
119
120 /**
121 * Should we unpack WAR files when auto-deploying applications in the
122 * <code>appBase</code> directory?
123 */
124 protected boolean unpackWARs = false;
125
126
127 /**
128 * Map of deployed applications.
129 */
130 protected HashMap deployed = new HashMap();
131
132
133 /**
134 * List of applications which are being serviced, and shouldn't be
135 * deployed/undeployed/redeployed at the moment.
136 */
137 protected ArrayList serviced = new ArrayList();
138
139
140 /**
141 * Attribute value used to turn on/off XML validation
142 */
143 protected boolean xmlValidation = false;
144
145
146 /**
147 * Attribute value used to turn on/off XML namespace awarenes.
148 */
149 protected boolean xmlNamespaceAware = false;
150
151
152 /**
153 * The <code>Digester</code> instance used to parse context descriptors.
154 */
155 protected static Digester digester = createDigester();
156
157 /**
158 * The list of Wars in the appBase to be ignored because they are invalid
159 * (e.g. contain /../ sequences).
160 */
161 protected Set<String> invalidWars = new HashSet<String>();
162
163 // ------------------------------------------------------------- Properties
164
165
166 /**
167 * Return the Context configuration class name.
168 */
169 public String getConfigClass() {
170
171 return (this.configClass);
172
173 }
174
175
176 /**
177 * Set the Context configuration class name.
178 *
179 * @param configClass The new Context configuration class name.
180 */
181 public void setConfigClass(String configClass) {
182
183 this.configClass = configClass;
184
185 }
186
187
188 /**
189 * Return the Context implementation class name.
190 */
191 public String getContextClass() {
192
193 return (this.contextClass);
194
195 }
196
197
198 /**
199 * Set the Context implementation class name.
200 *
201 * @param contextClass The new Context implementation class name.
202 */
203 public void setContextClass(String contextClass) {
204
205 this.contextClass = contextClass;
206
207 }
208
209
210 /**
211 * Return the deploy XML config file flag for this component.
212 */
213 public boolean isDeployXML() {
214
215 return (this.deployXML);
216
217 }
218
219
220 /**
221 * Set the deploy XML config file flag for this component.
222 *
223 * @param deployXML The new deploy XML flag
224 */
225 public void setDeployXML(boolean deployXML) {
226
227 this.deployXML= deployXML;
228
229 }
230
231
232 /**
233 * Return the unpack WARs flag.
234 */
235 public boolean isUnpackWARs() {
236
237 return (this.unpackWARs);
238
239 }
240
241
242 /**
243 * Set the unpack WARs flag.
244 *
245 * @param unpackWARs The new unpack WARs flag
246 */
247 public void setUnpackWARs(boolean unpackWARs) {
248
249 this.unpackWARs = unpackWARs;
250
251 }
252
253
254 /**
255 * Set the validation feature of the XML parser used when
256 * parsing xml instances.
257 * @param xmlValidation true to enable xml instance validation
258 */
259 public void setXmlValidation(boolean xmlValidation){
260 this.xmlValidation = xmlValidation;
261 }
262
263 /**
264 * Get the server.xml <host> attribute's xmlValidation.
265 * @return true if validation is enabled.
266 *
267 */
268 public boolean getXmlValidation(){
269 return xmlValidation;
270 }
271
272 /**
273 * Get the server.xml <host> attribute's xmlNamespaceAware.
274 * @return true if namespace awarenes is enabled.
275 *
276 */
277 public boolean getXmlNamespaceAware(){
278 return xmlNamespaceAware;
279 }
280
281
282 /**
283 * Set the namespace aware feature of the XML parser used when
284 * parsing xml instances.
285 * @param xmlNamespaceAware true to enable namespace awareness
286 */
287 public void setXmlNamespaceAware(boolean xmlNamespaceAware){
288 this.xmlNamespaceAware=xmlNamespaceAware;
289 }
290
291
292 // --------------------------------------------------------- Public Methods
293
294
295 /**
296 * Process the START event for an associated Host.
297 *
298 * @param event The lifecycle event that has occurred
299 */
300 public void lifecycleEvent(LifecycleEvent event) {
301
302 if (event.getType().equals(Lifecycle.PERIODIC_EVENT))
303 check();
304
305 // Identify the host we are associated with
306 try {
307 host = (Host) event.getLifecycle();
308 if (host instanceof StandardHost) {
309 setDeployXML(((StandardHost) host).isDeployXML());
310 setUnpackWARs(((StandardHost) host).isUnpackWARs());
311 setXmlNamespaceAware(((StandardHost) host).getXmlNamespaceAware());
312 setXmlValidation(((StandardHost) host).getXmlValidation());
313 }
314 } catch (ClassCastException e) {
315 log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e);
316 return;
317 }
318
319 // Process the event that has occurred
320 if (event.getType().equals(Lifecycle.START_EVENT))
321 start();
322 else if (event.getType().equals(Lifecycle.STOP_EVENT))
323 stop();
324
325 }
326
327
328 /**
329 * Add a serviced application to the list.
330 */
331 public synchronized void addServiced(String name) {
332 serviced.add(name);
333 }
334
335
336 /**
337 * Is application serviced ?
338 * @return state of the application
339 */
340 public synchronized boolean isServiced(String name) {
341 return (serviced.contains(name));
342 }
343
344
345 /**
346 * Removed a serviced application from the list.
347 */
348 public synchronized void removeServiced(String name) {
349 serviced.remove(name);
350 }
351
352
353 /**
354 * Get the instant where an application was deployed.
355 * @return 0L if no application with that name is deployed, or the instant
356 * on which the application was deployed
357 */
358 public long getDeploymentTime(String name) {
359 DeployedApplication app = (DeployedApplication) deployed.get(name);
360 if (app == null) {
361 return 0L;
362 } else {
363 return app.timestamp;
364 }
365 }
366
367
368 /**
369 * Has the specified application been deployed? Note applications defined
370 * in server.xml will not have been deployed.
371 * @return <code>true</code> if the application has been deployed and
372 * <code>false</code> if the applciation has not been deployed or does not
373 * exist
374 */
375 public boolean isDeployed(String name) {
376 DeployedApplication app = (DeployedApplication) deployed.get(name);
377 if (app == null) {
378 return false;
379 } else {
380 return true;
381 }
382 }
383
384
385 // ------------------------------------------------------ Protected Methods
386
387
388 /**
389 * Create the digester which will be used to parse context config files.
390 */
391 protected static Digester createDigester() {
392 Digester digester = new Digester();
393 digester.setValidating(false);
394 // Add object creation rule
395 digester.addObjectCreate("Context", "org.apache.catalina.core.StandardContext",
396 "className");
397 // Set the properties on that object (it doesn't matter if extra
398 // properties are set)
399 digester.addSetProperties("Context");
400 return (digester);
401 }
402
403
404 /**
405 * Return a File object representing the "application root" directory
406 * for our associated Host.
407 */
408 protected File appBase() {
409
410 if (appBase != null) {
411 return appBase;
412 }
413
414 File file = new File(host.getAppBase());
415 if (!file.isAbsolute())
416 file = new File(System.getProperty("catalina.base"),
417 host.getAppBase());
418 try {
419 appBase = file.getCanonicalFile();
420 } catch (IOException e) {
421 appBase = file;
422 }
423 return (appBase);
424
425 }
426
427
428 /**
429 * Return a File object representing the "configuration root" directory
430 * for our associated Host.
431 */
432 protected File configBase() {
433
434 if (configBase != null) {
435 return configBase;
436 }
437
438 File file = new File(System.getProperty("catalina.base"), "conf");
439 Container parent = host.getParent();
440 if ((parent != null) && (parent instanceof Engine)) {
441 file = new File(file, parent.getName());
442 }
443 file = new File(file, host.getName());
444 try {
445 configBase = file.getCanonicalFile();
446 } catch (IOException e) {
447 configBase = file;
448 }
449 return (configBase);
450
451 }
452
453 /**
454 * Get the name of the configBase.
455 * For use with JMX management.
456 */
457 public String getConfigBaseName() {
458 return configBase().getAbsolutePath();
459 }
460
461 /**
462 * Given a context path, get the config file name.
463 */
464 protected String getConfigFile(String path) {
465 String basename = null;
466 if (path.equals("")) {
467 basename = "ROOT";
468 } else {
469 basename = path.substring(1).replace('/', '#');
470 }
471 return (basename);
472 }
473
474
475 /**
476 * Given a context path, get the docBase.
477 */
478 protected String getDocBase(String path) {
479 String basename = null;
480 if (path.equals("")) {
481 basename = "ROOT";
482 } else {
483 basename = path.substring(1).replace('/', '#');
484 }
485 return (basename);
486 }
487
488
489 /**
490 * Deploy applications for any directories or WAR files that are found
491 * in our "application root" directory.
492 */
493 protected void deployApps() {
494
495 File appBase = appBase();
496 File configBase = configBase();
497 // Deploy XML descriptors from configBase
498 deployDescriptors(configBase, configBase.list());
499 // Deploy WARs, and loop if additional descriptors are found
500 deployWARs(appBase, appBase.list());
501 // Deploy expanded folders
502 deployDirectories(appBase, appBase.list());
503
504 }
505
506
507 /**
508 * Deploy applications for any directories or WAR files that are found
509 * in our "application root" directory.
510 */
511 protected void deployApps(String name) {
512
513 File appBase = appBase();
514 File configBase = configBase();
515 String baseName = getConfigFile(name);
516 String docBase = getDocBase(name);
517
518 // Deploy XML descriptors from configBase
519 File xml = new File(configBase, baseName + ".xml");
520 if (xml.exists())
521 deployDescriptor(name, xml, baseName + ".xml");
522 // Deploy WARs, and loop if additional descriptors are found
523 File war = new File(appBase, docBase + ".war");
524 if (war.exists())
525 deployWAR(name, war, docBase + ".war");
526 // Deploy expanded folders
527 File dir = new File(appBase, docBase);
528 if (dir.exists())
529 deployDirectory(name, dir, docBase);
530
531 }
532
533
534 /**
535 * Deploy XML context descriptors.
536 */
537 protected void deployDescriptors(File configBase, String[] files) {
538
539 if (files == null)
540 return;
541
542 for (int i = 0; i < files.length; i++) {
543
544 if (files[i].equalsIgnoreCase("META-INF"))
545 continue;
546 if (files[i].equalsIgnoreCase("WEB-INF"))
547 continue;
548 File contextXml = new File(configBase, files[i]);
549 if (files[i].toLowerCase().endsWith(".xml")) {
550
551 // Calculate the context path and make sure it is unique
552 String nameTmp = files[i].substring(0, files[i].length() - 4);
553 String contextPath = "/" + nameTmp.replace('#', '/');
554 if (nameTmp.equals("ROOT")) {
555 contextPath = "";
556 }
557
558 if (isServiced(contextPath))
559 continue;
560
561 String file = files[i];
562
563 deployDescriptor(contextPath, contextXml, file);
564
565 }
566
567 }
568
569 }
570
571
572 /**
573 * @param contextPath
574 * @param contextXml
575 * @param file
576 */
577 protected void deployDescriptor(String contextPath, File contextXml, String file) {
578 if (deploymentExists(contextPath)) {
579 return;
580 }
581
582 DeployedApplication deployedApp = new DeployedApplication(contextPath);
583
584 // Assume this is a configuration descriptor and deploy it
585 if(log.isInfoEnabled()) {
586 log.info(sm.getString("hostConfig.deployDescriptor", file));
587 }
588
589 Context context = null;
590 try {
591 synchronized (digester) {
592 try {
593 context = (Context) digester.parse(contextXml);
594 if (context == null) {
595 log.error(sm.getString("hostConfig.deployDescriptor.error",
596 file));
597 return;
598 }
599 } finally {
600 digester.reset();
601 }
602 }
603 if (context instanceof Lifecycle) {
604 Class clazz = Class.forName(host.getConfigClass());
605 LifecycleListener listener =
606 (LifecycleListener) clazz.newInstance();
607 ((Lifecycle) context).addLifecycleListener(listener);
608 }
609 context.setConfigFile(contextXml.getAbsolutePath());
610 context.setPath(contextPath);
611 // Add the associated docBase to the redeployed list if it's a WAR
612 boolean isExternalWar = false;
613 boolean isExternal = false;
614 if (context.getDocBase() != null) {
615 File docBase = new File(context.getDocBase());
616 if (!docBase.isAbsolute()) {
617 docBase = new File(appBase(), context.getDocBase());
618 }
619 // If external docBase, register .xml as redeploy first
620 if (!docBase.getCanonicalPath().startsWith(
621 appBase().getAbsolutePath() + File.separator)) {
622 isExternal = true;
623 deployedApp.redeployResources.put
624 (contextXml.getAbsolutePath(), new Long(contextXml.lastModified()));
625 deployedApp.redeployResources.put(docBase.getAbsolutePath(),
626 new Long(docBase.lastModified()));
627 if (docBase.getAbsolutePath().toLowerCase().endsWith(".war")) {
628 isExternalWar = true;
629 }
630 } else {
631 log.warn(sm.getString("hostConfig.deployDescriptor.localDocBaseSpecified",
632 docBase));
633 // Ignore specified docBase
634 context.setDocBase(null);
635 }
636 }
637 host.addChild(context);
638 // Get paths for WAR and expanded WAR in appBase
639 String name = null;
640 String path = context.getPath();
641 if (path.equals("")) {
642 name = "ROOT";
643 } else {
644 if (path.startsWith("/")) {
645 name = path.substring(1);
646 } else {
647 name = path;
648 }
649 }
650 File expandedDocBase = new File(appBase(), name);
651 if (context.getDocBase() != null) {
652 // first assume docBase is absolute
653 expandedDocBase = new File(context.getDocBase());
654 if (!expandedDocBase.isAbsolute()) {
655 // if docBase specified and relative, it must be relative to appBase
656 expandedDocBase = new File(appBase(), context.getDocBase());
657 }
658 }
659 // Add the eventual unpacked WAR and all the resources which will be
660 // watched inside it
661 if (isExternalWar && unpackWARs) {
662 deployedApp.redeployResources.put(expandedDocBase.getAbsolutePath(),
663 new Long(expandedDocBase.lastModified()));
664 deployedApp.redeployResources.put
665 (contextXml.getAbsolutePath(), new Long(contextXml.lastModified()));
666 addWatchedResources(deployedApp, expandedDocBase.getAbsolutePath(), context);
667 } else {
668 // Find an existing matching war and expanded folder
669 if (!isExternal) {
670 File warDocBase = new File(expandedDocBase.getAbsolutePath() + ".war");
671 if (warDocBase.exists()) {
672 deployedApp.redeployResources.put(warDocBase.getAbsolutePath(),
673 new Long(warDocBase.lastModified()));
674 }
675 }
676 if (expandedDocBase.exists()) {
677 deployedApp.redeployResources.put(expandedDocBase.getAbsolutePath(),
678 new Long(expandedDocBase.lastModified()));
679 addWatchedResources(deployedApp,
680 expandedDocBase.getAbsolutePath(), context);
681 } else {
682 addWatchedResources(deployedApp, null, context);
683 }
684 // Add the context XML to the list of files which should trigger a redeployment
685 if (!isExternal) {
686 deployedApp.redeployResources.put
687 (contextXml.getAbsolutePath(), new Long(contextXml.lastModified()));
688 }
689 }
690 } catch (Throwable t) {
691 log.error(sm.getString("hostConfig.deployDescriptor.error",
692 file), t);
693 }
694
695 if (context != null && host.findChild(context.getName()) != null) {
696 deployed.put(contextPath, deployedApp);
697 }
698 }
699
700
701 /**
702 * Deploy WAR files.
703 */
704 protected void deployWARs(File appBase, String[] files) {
705
706 if (files == null)
707 return;
708
709 for (int i = 0; i < files.length; i++) {
710
711 if (files[i].equalsIgnoreCase("META-INF"))
712 continue;
713 if (files[i].equalsIgnoreCase("WEB-INF"))
714 continue;
715 File dir = new File(appBase, files[i]);
716 if (files[i].toLowerCase().endsWith(".war") && dir.isFile()
717 && !invalidWars.contains(files[i]) ) {
718
719 // Calculate the context path and make sure it is unique
720 String contextPath = "/" + files[i].replace('#','/');
721 int period = contextPath.lastIndexOf(".");
722 contextPath = contextPath.substring(0, period);
723
724 // Check for WARs with /../ /./ or similar sequences in the name
725 if (!validateContextPath(appBase, contextPath)) {
726 log.error(sm.getString(
727 "hostConfig.illegalWarName", files[i]));
728 invalidWars.add(files[i]);
729 continue;
730 }
731
732 if (contextPath.equals("/ROOT"))
733 contextPath = "";
734
735 if (isServiced(contextPath))
736 continue;
737
738 String file = files[i];
739
740 deployWAR(contextPath, dir, file);
741
742 }
743
744 }
745
746 }
747
748
749 private boolean validateContextPath(File appBase, String contextPath) {
750 // More complicated than the ideal as the canonical path may or may
751 // not end with File.separator for a directory
752
753 StringBuilder docBase;
754 String canonicalDocBase = null;
755
756 try {
757 String canonicalAppBase = appBase.getCanonicalPath();
758 docBase = new StringBuilder(canonicalAppBase);
759 if (canonicalAppBase.endsWith(File.separator)) {
760 docBase.append(contextPath.substring(1).replace(
761 '/', File.separatorChar));
762 } else {
763 docBase.append(contextPath.replace('/', File.separatorChar));
764 }
765 // At this point docBase should be canonical but will not end
766 // with File.separator
767
768 canonicalDocBase =
769 (new File(docBase.toString())).getCanonicalPath();
770
771 // If the canonicalDocBase ends with File.separator, add one to
772 // docBase before they are compared
773 if (canonicalDocBase.endsWith(File.separator)) {
774 docBase.append(File.separator);
775 }
776 } catch (IOException ioe) {
777 return false;
778 }
779
780 // Compare the two. If they are not the same, the contextPath must
781 // have /../ like sequences in it
782 return canonicalDocBase.equals(docBase.toString());
783 }
784
785 /**
786 * @param contextPath
787 * @param war
788 * @param file
789 */
790 protected void deployWAR(String contextPath, File war, String file) {
791
792 if (deploymentExists(contextPath))
793 return;
794
795 // Checking for a nested /META-INF/context.xml
796 JarFile jar = null;
797 JarEntry entry = null;
798 InputStream istream = null;
799 BufferedOutputStream ostream = null;
800 File xml = new File
801 (configBase, file.substring(0, file.lastIndexOf(".")) + ".xml");
802 if (deployXML && !xml.exists()) {
803 try {
804 jar = new JarFile(war);
805 entry = jar.getJarEntry(Constants.ApplicationContextXml);
806 if (entry != null) {
807 istream = jar.getInputStream(entry);
808
809 configBase.mkdirs();
810
811 ostream =
812 new BufferedOutputStream
813 (new FileOutputStream(xml), 1024);
814 byte buffer[] = new byte[1024];
815 while (true) {
816 int n = istream.read(buffer);
817 if (n < 0) {
818 break;
819 }
820 ostream.write(buffer, 0, n);
821 }
822 ostream.flush();
823 ostream.close();
824 ostream = null;
825 istream.close();
826 istream = null;
827 entry = null;
828 jar.close();
829 jar = null;
830 }
831 } catch (Exception e) {
832 // Ignore and continue
833 if (ostream != null) {
834 try {
835 ostream.close();
836 } catch (Throwable t) {
837 ;
838 }
839 ostream = null;
840 }
841 if (istream != null) {
842 try {
843 istream.close();
844 } catch (Throwable t) {
845 ;
846 }
847 istream = null;
848 }
849 } finally {
850 entry = null;
851 if (jar != null) {
852 try {
853 jar.close();
854 } catch (Throwable t) {
855 ;
856 }
857 jar = null;
858 }
859 }
860 }
861
862 DeployedApplication deployedApp = new DeployedApplication(contextPath);
863
864 // Deploy the application in this WAR file
865 if(log.isInfoEnabled())
866 log.info(sm.getString("hostConfig.deployJar", file));
867
868 try {
869 Context context = null;
870 if (deployXML && xml.exists()) {
871 synchronized (digester) {
872 try {
873 context = (Context) digester.parse(xml);
874 if (context == null) {
875 log.error(sm.getString("hostConfig.deployDescriptor.error",
876 file));
877 return;
878 }
879 } finally {
880 digester.reset();
881 }
882 }
883 context.setConfigFile(xml.getAbsolutePath());
884 } else {
885 context = (Context) Class.forName(contextClass).newInstance();
886 }
887
888 // Populate redeploy resources with the WAR file
889 deployedApp.redeployResources.put
890 (war.getAbsolutePath(), new Long(war.lastModified()));
891
892 if (deployXML && xml.exists()) {
893 deployedApp.redeployResources.put
894 (xml.getAbsolutePath(), new Long(xml.lastModified()));
895 }
896
897 if (context instanceof Lifecycle) {
898 Class clazz = Class.forName(host.getConfigClass());
899 LifecycleListener listener =
900 (LifecycleListener) clazz.newInstance();
901 ((Lifecycle) context).addLifecycleListener(listener);
902 }
903 context.setPath(contextPath);
904 context.setDocBase(file);
905 host.addChild(context);
906 // If we're unpacking WARs, the docBase will be mutated after
907 // starting the context
908 if (unpackWARs && (context.getDocBase() != null)) {
909 String name = null;
910 String path = context.getPath();
911 if (path.equals("")) {
912 name = "ROOT";
913 } else {
914 if (path.startsWith("/")) {
915 name = path.substring(1);
916 } else {
917 name = path;
918 }
919 }
920 name = name.replace('/', '#');
921 File docBase = new File(name);
922 if (!docBase.isAbsolute()) {
923 docBase = new File(appBase(), name);
924 }
925 deployedApp.redeployResources.put(docBase.getAbsolutePath(),
926 new Long(docBase.lastModified()));
927 addWatchedResources(deployedApp, docBase.getAbsolutePath(), context);
928 } else {
929 addWatchedResources(deployedApp, null, context);
930 }
931 } catch (Throwable t) {
932 log.error(sm.getString("hostConfig.deployJar.error", file), t);
933 }
934
935 deployed.put(contextPath, deployedApp);
936 }
937
938
939 /**
940 * Deploy directories.
941 */
942 protected void deployDirectories(File appBase, String[] files) {
943
944 if (files == null)
945 return;
946
947 for (int i = 0; i < files.length; i++) {
948
949 if (files[i].equalsIgnoreCase("META-INF"))
950 continue;
951 if (files[i].equalsIgnoreCase("WEB-INF"))
952 continue;
953 File dir = new File(appBase, files[i]);
954 if (dir.isDirectory()) {
955
956 // Calculate the context path and make sure it is unique
957 String contextPath = "/" + files[i].replace('#','/');
958 if (files[i].equals("ROOT"))
959 contextPath = "";
960
961 if (isServiced(contextPath))
962 continue;
963
964 deployDirectory(contextPath, dir, files[i]);
965
966 }
967
968 }
969
970 }
971
972
973 /**
974 * @param contextPath
975 * @param dir
976 * @param file
977 */
978 protected void deployDirectory(String contextPath, File dir, String file) {
979 DeployedApplication deployedApp = new DeployedApplication(contextPath);
980
981 if (deploymentExists(contextPath))
982 return;
983
984 // Deploy the application in this directory
985 if( log.isInfoEnabled() )
986 log.info(sm.getString("hostConfig.deployDir", file));
987 try {
988 Context context = null;
989 File xml = new File(dir, Constants.ApplicationContextXml);
990 File xmlCopy = null;
991 if (deployXML && xml.exists()) {
992 // Will only do this on initial deployment. On subsequent
993 // deployments the copied xml file means we'll use
994 // deployDescriptor() instead
995 synchronized (digester) {
996 try {
997 context = (Context) digester.parse(xml);
998 if (context == null) {
999 log.error(sm.getString("hostConfig.deployDescriptor.error",
1000 xml));
1001 return;
1002 }
1003 } finally {
1004 digester.reset();
1005 }
1006 }
1007 configBase.mkdirs();
1008 xmlCopy = new File(configBase, file + ".xml");
1009 InputStream is = null;
1010 OutputStream os = null;
1011 try {
1012 is = new FileInputStream(xml);
1013 os = new FileOutputStream(xmlCopy);
1014 IOTools.flow(is, os);
1015 // Don't catch IOE - let the outer try/catch handle it
1016 } finally {
1017 try {
1018 if (is != null) is.close();
1019 } catch (IOException e){
1020 // Ignore
1021 }
1022 try {
1023 if (os != null) os.close();
1024 } catch (IOException e){
1025 // Ignore
1026 }
1027 }
1028 context.setConfigFile(xmlCopy.getAbsolutePath());
1029 } else {
1030 context = (Context) Class.forName(contextClass).newInstance();
1031 }
1032
1033 if (context instanceof Lifecycle) {
1034 Class clazz = Class.forName(host.getConfigClass());
1035 LifecycleListener listener =
1036 (LifecycleListener) clazz.newInstance();
1037 ((Lifecycle) context).addLifecycleListener(listener);
1038 }
1039 context.setPath(contextPath);
1040 context.setDocBase(file);
1041 host.addChild(context);
1042 deployedApp.redeployResources.put(dir.getAbsolutePath(),
1043 new Long(dir.lastModified()));
1044 if (xmlCopy != null) {
1045 deployedApp.redeployResources.put
1046 (xmlCopy.getAbsolutePath(), new Long(xmlCopy.lastModified()));
1047 }
1048 addWatchedResources(deployedApp, dir.getAbsolutePath(), context);
1049 } catch (Throwable t) {
1050 log.error(sm.getString("hostConfig.deployDir.error", file), t);
1051 }
1052
1053 deployed.put(contextPath, deployedApp);
1054 }
1055
1056
1057 /**
1058 * Check if a webapp is already deployed in this host.
1059 *
1060 * @param contextPath of the context which will be checked
1061 */
1062 protected boolean deploymentExists(String contextPath) {
1063 return (deployed.containsKey(contextPath) || (host.findChild(contextPath) != null));
1064 }
1065
1066
1067 /**
1068 * Add watched resources to the specified Context.
1069 * @param app HostConfig deployed app
1070 * @param docBase web app docBase
1071 * @param context web application context
1072 */
1073 protected void addWatchedResources(DeployedApplication app, String docBase, Context context) {
1074 // FIXME: Feature idea. Add support for patterns (ex: WEB-INF/*, WEB-INF/*.xml), where
1075 // we would only check if at least one resource is newer than app.timestamp
1076 File docBaseFile = null;
1077 if (docBase != null) {
1078 docBaseFile = new File(docBase);
1079 if (!docBaseFile.isAbsolute()) {
1080 docBaseFile = new File(appBase(), docBase);
1081 }
1082 }
1083 String[] watchedResources = context.findWatchedResources();
1084 for (int i = 0; i < watchedResources.length; i++) {
1085 File resource = new File(watchedResources[i]);
1086 if (!resource.isAbsolute()) {
1087 if (docBase != null) {
1088 resource = new File(docBaseFile, watchedResources[i]);
1089 } else {
1090 if(log.isDebugEnabled())
1091 log.debug("Ignoring non-existent WatchedResource '"
1092 + resource.getAbsolutePath() + "'");
1093 continue;
1094 }
1095 }
1096 if(log.isDebugEnabled())
1097 log.debug("Watching WatchedResource '" + resource.getAbsolutePath() + "'");
1098 app.reloadResources.put(resource.getAbsolutePath(),
1099 new Long(resource.lastModified()));
1100 }
1101 }
1102
1103
1104 /**
1105 * Check resources for redeployment and reloading.
1106 */
1107 protected synchronized void checkResources(DeployedApplication app) {
1108 String[] resources = (String[]) app.redeployResources.keySet().toArray(new String[0]);
1109 for (int i = 0; i < resources.length; i++) {
1110 File resource = new File(resources[i]);
1111 if (log.isDebugEnabled())
1112 log.debug("Checking context[" + app.name + "] redeploy resource " + resource);
1113 if (resource.exists()) {
1114 long lastModified = ((Long) app.redeployResources.get(resources[i])).longValue();
1115 if ((!resource.isDirectory()) && resource.lastModified() > lastModified) {
1116 // Undeploy application
1117 if (log.isInfoEnabled())
1118 log.info(sm.getString("hostConfig.undeploy", app.name));
1119 ContainerBase context = (ContainerBase) host.findChild(app.name);
1120 try {
1121 host.removeChild(context);
1122 } catch (Throwable t) {
1123 log.warn(sm.getString
1124 ("hostConfig.context.remove", app.name), t);
1125 }
1126 try {
1127 context.destroy();
1128 } catch (Throwable t) {
1129 log.warn(sm.getString
1130 ("hostConfig.context.destroy", app.name), t);
1131 }
1132 // Delete other redeploy resources
1133 for (int j = i + 1; j < resources.length; j++) {
1134 try {
1135 File current = new File(resources[j]);
1136 current = current.getCanonicalFile();
1137 if ((current.getAbsolutePath().startsWith(appBase().getAbsolutePath() + File.separator))
1138 || (current.getAbsolutePath().startsWith(configBase().getAbsolutePath()))) {
1139 if (log.isDebugEnabled())
1140 log.debug("Delete " + current);
1141 ExpandWar.delete(current);
1142 }
1143 } catch (IOException e) {
1144 log.warn(sm.getString
1145 ("hostConfig.canonicalizing", app.name), e);
1146 }
1147 }
1148 deployed.remove(app.name);
1149 return;
1150 }
1151 } else {
1152 // There is a chance the the resource was only missing
1153 // temporarily eg renamed during a text editor save
1154 try {
1155 Thread.sleep(500);
1156 } catch (InterruptedException e1) {
1157 // Ignore
1158 }
1159 // Recheck the resource to see if it was really deleted
1160 if (resource.exists()) {
1161 continue;
1162 }
1163 long lastModified =
1164 ((Long) app.redeployResources.get(resources[i])).longValue();
1165
1166 if (lastModified == 0L) {
1167 continue;
1168 }
1169 // Undeploy application
1170 if (log.isInfoEnabled())
1171 log.info(sm.getString("hostConfig.undeploy", app.name));
1172 ContainerBase context = (ContainerBase) host.findChild(app.name);
1173 try {
1174 host.removeChild(context);
1175 } catch (Throwable t) {
1176 log.warn(sm.getString
1177 ("hostConfig.context.remove", app.name), t);
1178 }
1179 try {
1180 context.destroy();
1181 } catch (Throwable t) {
1182 log.warn(sm.getString
1183 ("hostConfig.context.destroy", app.name), t);
1184 }
1185 // Delete all redeploy resources
1186 for (int j = i + 1; j < resources.length; j++) {
1187 try {
1188 File current = new File(resources[j]);
1189 current = current.getCanonicalFile();
1190 if ((current.getAbsolutePath().startsWith(appBase().getAbsolutePath() + File.separator))
1191 || (current.getAbsolutePath().startsWith(configBase().getAbsolutePath()))) {
1192 if (log.isDebugEnabled())
1193 log.debug("Delete " + current);
1194 ExpandWar.delete(current);
1195 }
1196 } catch (IOException e) {
1197 log.warn(sm.getString
1198 ("hostConfig.canonicalizing", app.name), e);
1199 }
1200 }
1201 // Delete reload resources as well (to remove any remaining .xml descriptor)
1202 String[] resources2 = (String[]) app.reloadResources.keySet().toArray(new String[0]);
1203 for (int j = 0; j < resources2.length; j++) {
1204 try {
1205 File current = new File(resources2[j]);
1206 current = current.getCanonicalFile();
1207 if ((current.getAbsolutePath().startsWith(appBase().getAbsolutePath() + File.separator))
1208 || ((current.getAbsolutePath().startsWith(configBase().getAbsolutePath())
1209 && (current.getAbsolutePath().endsWith(".xml"))))) {
1210 if (log.isDebugEnabled())
1211 log.debug("Delete " + current);
1212 ExpandWar.delete(current);
1213 }
1214 } catch (IOException e) {
1215 log.warn(sm.getString
1216 ("hostConfig.canonicalizing", app.name), e);
1217 }
1218 }
1219 deployed.remove(app.name);
1220 return;
1221 }
1222 }
1223 resources = (String[]) app.reloadResources.keySet().toArray(new String[0]);
1224 for (int i = 0; i < resources.length; i++) {
1225 File resource = new File(resources[i]);
1226 if (log.isDebugEnabled())
1227 log.debug("Checking context[" + app.name + "] reload resource " + resource);
1228 long lastModified = ((Long) app.reloadResources.get(resources[i])).longValue();
1229 if ((!resource.exists() && lastModified != 0L)
1230 || (resource.lastModified() != lastModified)) {
1231 // Reload application
1232 if(log.isInfoEnabled())
1233 log.info(sm.getString("hostConfig.reload", app.name));
1234 Container context = host.findChild(app.name);
1235 try {
1236 ((Lifecycle) context).stop();
1237 } catch (Exception e) {
1238 log.warn(sm.getString
1239 ("hostConfig.context.restart", app.name), e);
1240 }
1241 // If the context was not started (for example an error
1242 // in web.xml) we'll still get to try to start
1243 try {
1244 ((Lifecycle) context).start();
1245 } catch (Exception e) {
1246 log.warn(sm.getString
1247 ("hostConfig.context.restart", app.name), e);
1248 }
1249 // Update times
1250 app.reloadResources.put(resources[i], new Long(resource.lastModified()));
1251 app.timestamp = System.currentTimeMillis();
1252 return;
1253 }
1254 }
1255 }
1256
1257
1258 /**
1259 * Process a "start" event for this Host.
1260 */
1261 public void start() {
1262
1263 if (log.isDebugEnabled())
1264 log.debug(sm.getString("hostConfig.start"));
1265
1266 try {
1267 ObjectName hostON = new ObjectName(host.getObjectName());
1268 oname = new ObjectName
1269 (hostON.getDomain() + ":type=Deployer,host=" + host.getName());
1270 Registry.getRegistry(null, null).registerComponent
1271 (this, oname, this.getClass().getName());
1272 } catch (Exception e) {
1273 log.error(sm.getString("hostConfig.jmx.register", oname), e);
1274 }
1275
1276 if (host.getDeployOnStartup())
1277 deployApps();
1278
1279 }
1280
1281
1282 /**
1283 * Process a "stop" event for this Host.
1284 */
1285 public void stop() {
1286
1287 if (log.isDebugEnabled())
1288 log.debug(sm.getString("hostConfig.stop"));
1289
1290 undeployApps();
1291
1292 if (oname != null) {
1293 try {
1294 Registry.getRegistry(null, null).unregisterComponent(oname);
1295 } catch (Exception e) {
1296 log.error(sm.getString("hostConfig.jmx.unregister", oname), e);
1297 }
1298 }
1299 oname = null;
1300 appBase = null;
1301 configBase = null;
1302
1303 }
1304
1305
1306 /**
1307 * Undeploy all deployed applications.
1308 */
1309 protected void undeployApps() {
1310
1311 if (log.isDebugEnabled())
1312 log.debug(sm.getString("hostConfig.undeploying"));
1313
1314 // Soft undeploy all contexts we have deployed
1315 DeployedApplication[] apps =
1316 (DeployedApplication[]) deployed.values().toArray(new DeployedApplication[0]);
1317 for (int i = 0; i < apps.length; i++) {
1318 try {
1319 host.removeChild(host.findChild(apps[i].name));
1320 } catch (Throwable t) {
1321 log.warn(sm.getString
1322 ("hostConfig.context.remove", apps[i].name), t);
1323 }
1324 }
1325
1326 deployed.clear();
1327
1328 }
1329
1330
1331 /**
1332 * Check status of all webapps.
1333 */
1334 protected void check() {
1335
1336 if (host.getAutoDeploy()) {
1337 // Check for resources modification to trigger redeployment
1338 DeployedApplication[] apps =
1339 (DeployedApplication[]) deployed.values().toArray(new DeployedApplication[0]);
1340 for (int i = 0; i < apps.length; i++) {
1341 if (!isServiced(apps[i].name))
1342 checkResources(apps[i]);
1343 }
1344 // Hotdeploy applications
1345 deployApps();
1346 }
1347
1348 }
1349
1350
1351 /**
1352 * Check status of a specific webapp, for use with stuff like management webapps.
1353 */
1354 public void check(String name) {
1355 DeployedApplication app = (DeployedApplication) deployed.get(name);
1356 if (app != null) {
1357 checkResources(app);
1358 } else {
1359 deployApps(name);
1360 }
1361 }
1362
1363 /**
1364 * Add a new Context to be managed by us.
1365 * Entry point for the admin webapp, and other JMX Context controlers.
1366 */
1367 public void manageApp(Context context) {
1368
1369 String contextPath = context.getPath();
1370
1371 if (deployed.containsKey(contextPath))
1372 return;
1373
1374 DeployedApplication deployedApp = new DeployedApplication(contextPath);
1375
1376 // Add the associated docBase to the redeployed list if it's a WAR
1377 boolean isWar = false;
1378 if (context.getDocBase() != null) {
1379 File docBase = new File(context.getDocBase());
1380 if (!docBase.isAbsolute()) {
1381 docBase = new File(appBase(), context.getDocBase());
1382 }
1383 deployedApp.redeployResources.put(docBase.getAbsolutePath(),
1384 new Long(docBase.lastModified()));
1385 if (docBase.getAbsolutePath().toLowerCase().endsWith(".war")) {
1386 isWar = true;
1387 }
1388 }
1389 host.addChild(context);
1390 // Add the eventual unpacked WAR and all the resources which will be
1391 // watched inside it
1392 if (isWar && unpackWARs) {
1393 String name = null;
1394 String path = context.getPath();
1395 if (path.equals("")) {
1396 name = "ROOT";
1397 } else {
1398 if (path.startsWith("/")) {
1399 name = path.substring(1);
1400 } else {
1401 name = path;
1402 }
1403 }
1404 File docBase = new File(name);
1405 if (!docBase.isAbsolute()) {
1406 docBase = new File(appBase(), name);
1407 }
1408 deployedApp.redeployResources.put(docBase.getAbsolutePath(),
1409 new Long(docBase.lastModified()));
1410 addWatchedResources(deployedApp, docBase.getAbsolutePath(), context);
1411 } else {
1412 addWatchedResources(deployedApp, null, context);
1413 }
1414 deployed.put(contextPath, deployedApp);
1415 }
1416
1417 /**
1418 * Remove a webapp from our control.
1419 * Entry point for the admin webapp, and other JMX Context controlers.
1420 */
1421 public void unmanageApp(String contextPath) {
1422 if(isServiced(contextPath)) {
1423 deployed.remove(contextPath);
1424 host.removeChild(host.findChild(contextPath));
1425 }
1426 }
1427
1428 // ----------------------------------------------------- Instance Variables
1429
1430
1431 /**
1432 * This class represents the state of a deployed application, as well as
1433 * the monitored resources.
1434 */
1435 protected class DeployedApplication {
1436 public DeployedApplication(String name) {
1437 this.name = name;
1438 }
1439
1440 /**
1441 * Application context path. The assertion is that
1442 * (host.getChild(name) != null).
1443 */
1444 public String name;
1445
1446 /**
1447 * Any modification of the specified (static) resources will cause a
1448 * redeployment of the application. If any of the specified resources is
1449 * removed, the application will be undeployed. Typically, this will
1450 * contain resources like the context.xml file, a compressed WAR path.
1451 * The value is the last modification time.
1452 */
1453 public LinkedHashMap redeployResources = new LinkedHashMap();
1454
1455 /**
1456 * Any modification of the specified (static) resources will cause a
1457 * reload of the application. This will typically contain resources
1458 * such as the web.xml of a webapp, but can be configured to contain
1459 * additional descriptors.
1460 * The value is the last modification time.
1461 */
1462 public HashMap reloadResources = new HashMap();
1463
1464 /**
1465 * Instant where the application was last put in service.
1466 */
1467 public long timestamp = System.currentTimeMillis();
1468 }
1469
1470 }