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 package org.apache.catalina.startup;
18
19 import java.io.File;
20 import java.io.FileInputStream;
21 import java.io.FileOutputStream;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.OutputStream;
25 import java.net.MalformedURLException;
26 import java.net.URL;
27 import java.nio.file.Files;
28 import java.security.CodeSource;
29 import java.security.Permission;
30 import java.security.PermissionCollection;
31 import java.security.Policy;
32 import java.security.cert.Certificate;
33 import java.util.ArrayList;
34 import java.util.HashMap;
35 import java.util.HashSet;
36 import java.util.Iterator;
37 import java.util.LinkedHashMap;
38 import java.util.List;
39 import java.util.Locale;
40 import java.util.Map;
41 import java.util.Set;
42 import java.util.SortedSet;
43 import java.util.TreeSet;
44 import java.util.concurrent.ConcurrentHashMap;
45 import java.util.concurrent.ExecutorService;
46 import java.util.concurrent.Future;
47 import java.util.jar.JarEntry;
48 import java.util.jar.JarFile;
49 import java.util.regex.Matcher;
50 import java.util.regex.Pattern;
51
52 import javax.management.ObjectName;
53
54 import org.apache.catalina.Container;
55 import org.apache.catalina.Context;
56 import org.apache.catalina.DistributedManager;
57 import org.apache.catalina.Globals;
58 import org.apache.catalina.Host;
59 import org.apache.catalina.Lifecycle;
60 import org.apache.catalina.LifecycleEvent;
61 import org.apache.catalina.LifecycleListener;
62 import org.apache.catalina.Manager;
63 import org.apache.catalina.core.StandardContext;
64 import org.apache.catalina.core.StandardHost;
65 import org.apache.catalina.security.DeployXmlPermission;
66 import org.apache.catalina.util.ContextName;
67 import org.apache.catalina.util.IOTools;
68 import org.apache.juli.logging.Log;
69 import org.apache.juli.logging.LogFactory;
70 import org.apache.tomcat.util.ExceptionUtils;
71 import org.apache.tomcat.util.buf.UriUtil;
72 import org.apache.tomcat.util.digester.Digester;
73 import org.apache.tomcat.util.modeler.Registry;
74 import org.apache.tomcat.util.res.StringManager;
75
76 /**
77  * Startup event listener for a <b>Host</b> that configures the properties
78  * of that Host, and the associated defined contexts.
79  *
80  * @author Craig R. McClanahan
81  * @author Remy Maucherat
82  */

83 public class HostConfig implements LifecycleListener {
84
85     private static final Log log = LogFactory.getLog(HostConfig.class);
86
87     /**
88      * The string resources for this package.
89      */

90     protected static final StringManager sm = StringManager.getManager(HostConfig.class);
91
92     /**
93      * The resolution, in milliseconds, of file modification times.
94      */

95     protected static final long FILE_MODIFICATION_RESOLUTION_MS = 1000;
96
97
98     // ----------------------------------------------------- Instance Variables
99
100     /**
101      * The Java class name of the Context implementation we should use.
102      */

103     protected String contextClass = "org.apache.catalina.core.StandardContext";
104
105
106     /**
107      * The Host we are associated with.
108      */

109     protected Host host = null;
110
111
112     /**
113      * The JMX ObjectName of this component.
114      */

115     protected ObjectName oname = null;
116
117
118     /**
119      * Should we deploy XML Context config files packaged with WAR files and
120      * directories?
121      */

122     protected boolean deployXML = false;
123
124
125     /**
126      * Should XML files be copied to
127      * $CATALINA_BASE/conf/&lt;engine&gt;/&lt;host&gt; by default when
128      * a web application is deployed?
129      */

130     protected boolean copyXML = false;
131
132
133     /**
134      * Should we unpack WAR files when auto-deploying applications in the
135      * <code>appBase</code> directory?
136      */

137     protected boolean unpackWARs = false;
138
139
140     /**
141      * Map of deployed applications.
142      */

143     protected final Map<String, DeployedApplication> deployed =
144             new ConcurrentHashMap<>();
145
146
147     /**
148      * List of applications which are being serviced, and shouldn't be
149      * deployed/undeployed/redeployed at the moment.
150      */

151     protected final ArrayList<String> serviced = new ArrayList<>();
152
153
154     /**
155      * The <code>Digester</code> instance used to parse context descriptors.
156      */

157     protected Digester digester = createDigester(contextClass);
158     private final Object digesterLock = new Object();
159
160     /**
161      * The list of Wars in the appBase to be ignored because they are invalid
162      * (e.g. contain /../ sequences).
163      */

164     protected final Set<String> invalidWars = new HashSet<>();
165
166     // ------------------------------------------------------------- Properties
167
168
169     /**
170      * @return the Context implementation class name.
171      */

172     public String getContextClass() {
173         return this.contextClass;
174     }
175
176
177     /**
178      * Set the Context implementation class name.
179      *
180      * @param contextClass The new Context implementation class name.
181      */

182     public void setContextClass(String contextClass) {
183
184         String oldContextClass = this.contextClass;
185         this.contextClass = contextClass;
186
187         if (!oldContextClass.equals(contextClass)) {
188             synchronized (digesterLock) {
189                 digester = createDigester(getContextClass());
190             }
191         }
192     }
193
194
195     /**
196      * @return the deploy XML config file flag for this component.
197      */

198     public boolean isDeployXML() {
199         return this.deployXML;
200     }
201
202
203     /**
204      * Set the deploy XML config file flag for this component.
205      *
206      * @param deployXML The new deploy XML flag
207      */

208     public void setDeployXML(boolean deployXML) {
209         this.deployXML = deployXML;
210     }
211
212
213     private boolean isDeployThisXML(File docBase, ContextName cn) {
214         boolean deployThisXML = isDeployXML();
215         if (Globals.IS_SECURITY_ENABLED && !deployThisXML) {
216             // When running under a SecurityManager, deployXML may be overridden
217             // on a per Context basis by the granting of a specific permission
218             Policy currentPolicy = Policy.getPolicy();
219             if (currentPolicy != null) {
220                 URL contextRootUrl;
221                 try {
222                     contextRootUrl = docBase.toURI().toURL();
223                     CodeSource cs = new CodeSource(contextRootUrl, (Certificate[]) null);
224                     PermissionCollection pc = currentPolicy.getPermissions(cs);
225                     Permission p = new DeployXmlPermission(cn.getBaseName());
226                     if (pc.implies(p)) {
227                         deployThisXML = true;
228                     }
229                 } catch (MalformedURLException e) {
230                     // Should never happen
231                     log.warn(sm.getString("hostConfig.docBaseUrlInvalid"), e);
232                 }
233             }
234         }
235
236         return deployThisXML;
237     }
238
239
240     /**
241      * @return the copy XML config file flag for this component.
242      */

243     public boolean isCopyXML() {
244         return this.copyXML;
245     }
246
247
248     /**
249      * Set the copy XML config file flag for this component.
250      *
251      * @param copyXML The new copy XML flag
252      */

253     public void setCopyXML(boolean copyXML) {
254
255         this.copyXML= copyXML;
256
257     }
258
259
260     /**
261      * @return the unpack WARs flag.
262      */

263     public boolean isUnpackWARs() {
264         return this.unpackWARs;
265     }
266
267
268     /**
269      * Set the unpack WARs flag.
270      *
271      * @param unpackWARs The new unpack WARs flag
272      */

273     public void setUnpackWARs(boolean unpackWARs) {
274         this.unpackWARs = unpackWARs;
275     }
276
277
278     // --------------------------------------------------------- Public Methods
279
280
281     /**
282      * Process the START event for an associated Host.
283      *
284      * @param event The lifecycle event that has occurred
285      */

286     @Override
287     public void lifecycleEvent(LifecycleEvent event) {
288
289         // Identify the host we are associated with
290         try {
291             host = (Host) event.getLifecycle();
292             if (host instanceof StandardHost) {
293                 setCopyXML(((StandardHost) host).isCopyXML());
294                 setDeployXML(((StandardHost) host).isDeployXML());
295                 setUnpackWARs(((StandardHost) host).isUnpackWARs());
296                 setContextClass(((StandardHost) host).getContextClass());
297             }
298         } catch (ClassCastException e) {
299             log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e);
300             return;
301         }
302
303         // Process the event that has occurred
304         if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) {
305             check();
306         } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
307             beforeStart();
308         } else if (event.getType().equals(Lifecycle.START_EVENT)) {
309             start();
310         } else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
311             stop();
312         }
313     }
314
315
316     /**
317      * Add a serviced application to the list.
318      * @param name the context name
319      */

320     public synchronized void addServiced(String name) {
321         serviced.add(name);
322     }
323
324
325     /**
326      * Is application serviced ?
327      * @param name the context name
328      * @return state of the application
329      */

330     public synchronized boolean isServiced(String name) {
331         return serviced.contains(name);
332     }
333
334
335     /**
336      * Removed a serviced application from the list.
337      * @param name the context name
338      */

339     public synchronized void removeServiced(String name) {
340         serviced.remove(name);
341     }
342
343
344     /**
345      * Get the instant where an application was deployed.
346      * @param name the context name
347      * @return 0L if no application with that name is deployed, or the instant
348      *  on which the application was deployed
349      */

350     public long getDeploymentTime(String name) {
351         DeployedApplication app = deployed.get(name);
352         if (app == null) {
353             return 0L;
354         }
355
356         return app.timestamp;
357     }
358
359
360     /**
361      * Has the specified application been deployed? Note applications defined
362      * in server.xml will not have been deployed.
363      * @param name the context name
364      * @return <code>true</code> if the application has been deployed and
365      *  <code>false</code> if the application has not been deployed or does not
366      *  exist
367      */

368     public boolean isDeployed(String name) {
369         return deployed.containsKey(name);
370     }
371
372
373     // ------------------------------------------------------ Protected Methods
374
375
376     /**
377      * Create the digester which will be used to parse context config files.
378      * @param contextClassName The class which will be used to create the
379      *  context instance
380      * @return the digester
381      */

382     protected static Digester createDigester(String contextClassName) {
383         Digester digester = new Digester();
384         digester.setValidating(false);
385         // Add object creation rule
386         digester.addObjectCreate("Context", contextClassName, "className");
387         // Set the properties on that object (it doesn't matter if extra
388         // properties are set)
389         digester.addSetProperties("Context");
390         return digester;
391     }
392
393     protected File returnCanonicalPath(String path) {
394         File file = new File(path);
395         if (!file.isAbsolute())
396             file = new File(host.getCatalinaBase(), path);
397         try {
398             return file.getCanonicalFile();
399         } catch (IOException e) {
400             return file;
401         }
402     }
403
404
405     /**
406      * Get the name of the configBase.
407      * For use with JMX management.
408      * @return the config base
409      */

410     public String getConfigBaseName() {
411         return host.getConfigBaseFile().getAbsolutePath();
412     }
413
414
415     /**
416      * Deploy applications for any directories or WAR files that are found
417      * in our "application root" directory.
418      */

419     protected void deployApps() {
420
421         File appBase = host.getAppBaseFile();
422         File configBase = host.getConfigBaseFile();
423         String[] filteredAppPaths = filterAppPaths(appBase.list());
424         // Deploy XML descriptors from configBase
425         deployDescriptors(configBase, configBase.list());
426         // Deploy WARs
427         deployWARs(appBase, filteredAppPaths);
428         // Deploy expanded folders
429         deployDirectories(appBase, filteredAppPaths);
430
431     }
432
433
434     /**
435      * Filter the list of application file paths to remove those that match
436      * the regular expression defined by {@link Host#getDeployIgnore()}.
437      *
438      * @param unfilteredAppPaths    The list of application paths to filter
439      *
440      * @return  The filtered list of application paths
441      */

442     protected String[] filterAppPaths(String[] unfilteredAppPaths) {
443         Pattern filter = host.getDeployIgnorePattern();
444         if (filter == null || unfilteredAppPaths == null) {
445             return unfilteredAppPaths;
446         }
447
448         List<String> filteredList = new ArrayList<>();
449         Matcher matcher = null;
450         for (String appPath : unfilteredAppPaths) {
451             if (matcher == null) {
452                 matcher = filter.matcher(appPath);
453             } else {
454                 matcher.reset(appPath);
455             }
456             if (matcher.matches()) {
457                 if (log.isDebugEnabled()) {
458                     log.debug(sm.getString("hostConfig.ignorePath", appPath));
459                 }
460             } else {
461                 filteredList.add(appPath);
462             }
463         }
464         return filteredList.toArray(new String[filteredList.size()]);
465     }
466
467
468     /**
469      * Deploy applications for any directories or WAR files that are found
470      * in our "application root" directory.
471      * @param name The context name which should be deployed
472      */

473     protected void deployApps(String name) {
474
475         File appBase = host.getAppBaseFile();
476         File configBase = host.getConfigBaseFile();
477         ContextName cn = new ContextName(name, false);
478         String baseName = cn.getBaseName();
479
480         if (deploymentExists(cn.getName())) {
481             return;
482         }
483
484         // Deploy XML descriptor from configBase
485         File xml = new File(configBase, baseName + ".xml");
486         if (xml.exists()) {
487             deployDescriptor(cn, xml);
488             return;
489         }
490         // Deploy WAR
491         File war = new File(appBase, baseName + ".war");
492         if (war.exists()) {
493             deployWAR(cn, war);
494             return;
495         }
496         // Deploy expanded folder
497         File dir = new File(appBase, baseName);
498         if (dir.exists())
499             deployDirectory(cn, dir);
500     }
501
502
503     /**
504      * Deploy XML context descriptors.
505      * @param configBase The config base
506      * @param files The XML descriptors which should be deployed
507      */

508     protected void deployDescriptors(File configBase, String[] files) {
509
510         if (files == null)
511             return;
512
513         ExecutorService es = host.getStartStopExecutor();
514         List<Future<?>> results = new ArrayList<>();
515
516         for (int i = 0; i < files.length; i++) {
517             File contextXml = new File(configBase, files[i]);
518
519             if (files[i].toLowerCase(Locale.ENGLISH).endsWith(".xml")) {
520                 ContextName cn = new ContextName(files[i], true);
521
522                 if (isServiced(cn.getName()) || deploymentExists(cn.getName()))
523                     continue;
524
525                 results.add(
526                         es.submit(new DeployDescriptor(this, cn, contextXml)));
527             }
528         }
529
530         for (Future<?> result : results) {
531             try {
532                 result.get();
533             } catch (Exception e) {
534                 log.error(sm.getString(
535                         "hostConfig.deployDescriptor.threaded.error"), e);
536             }
537         }
538     }
539
540
541     /**
542      * Deploy specified context descriptor.
543      * @param cn The context name
544      * @param contextXml The descriptor
545      */

546     @SuppressWarnings("null"// context is not null
547     protected void deployDescriptor(ContextName cn, File contextXml) {
548
549         DeployedApplication deployedApp =
550                 new DeployedApplication(cn.getName(), true);
551
552         long startTime = 0;
553         // Assume this is a configuration descriptor and deploy it
554         if(log.isInfoEnabled()) {
555            startTime = System.currentTimeMillis();
556            log.info(sm.getString("hostConfig.deployDescriptor",
557                     contextXml.getAbsolutePath()));
558         }
559
560         Context context = null;
561         boolean isExternalWar = false;
562         boolean isExternal = false;
563         File expandedDocBase = null;
564
565         try (FileInputStream fis = new FileInputStream(contextXml)) {
566             synchronized (digesterLock) {
567                 try {
568                     context = (Context) digester.parse(fis);
569                 } catch (Exception e) {
570                     log.error(sm.getString(
571                             "hostConfig.deployDescriptor.error",
572                             contextXml.getAbsolutePath()), e);
573                 } finally {
574                     digester.reset();
575                     if (context == null) {
576                         context = new FailedContext();
577                     }
578                 }
579             }
580
581             if (context.getPath() != null) {
582                 log.warn(sm.getString("hostConfig.deployDescriptor.path", context.getPath(),
583                         contextXml.getAbsolutePath()));
584             }
585
586             Class<?> clazz = Class.forName(host.getConfigClass());
587             LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
588             context.addLifecycleListener(listener);
589
590             context.setConfigFile(contextXml.toURI().toURL());
591             context.setName(cn.getName());
592             context.setPath(cn.getPath());
593             context.setWebappVersion(cn.getVersion());
594             // Add the associated docBase to the redeployed list if it's a WAR
595             if (context.getDocBase() != null) {
596                 File docBase = new File(context.getDocBase());
597                 if (!docBase.isAbsolute()) {
598                     docBase = new File(host.getAppBaseFile(), context.getDocBase());
599                 }
600                 // If external docBase, register .xml as redeploy first
601                 if (!docBase.getCanonicalPath().startsWith(
602                         host.getAppBaseFile().getAbsolutePath() + File.separator)) {
603                     isExternal = true;
604                     deployedApp.redeployResources.put(
605                             contextXml.getAbsolutePath(),
606                             Long.valueOf(contextXml.lastModified()));
607                     deployedApp.redeployResources.put(docBase.getAbsolutePath(),
608                             Long.valueOf(docBase.lastModified()));
609                     if (docBase.getAbsolutePath().toLowerCase(Locale.ENGLISH).endsWith(".war")) {
610                         isExternalWar = true;
611                     }
612                     // Check that a WAR or DIR in the appBase is not 'hidden'
613                     File war = new File(host.getAppBaseFile(), cn.getBaseName() + ".war");
614                     if (war.exists()) {
615                         log.warn(sm.getString("hostConfig.deployDescriptor.hiddenWar",
616                                 contextXml.getAbsolutePath(), war.getAbsolutePath()));
617                     }
618                     File dir = new File(host.getAppBaseFile(), cn.getBaseName());
619                     if (dir.exists()) {
620                         log.warn(sm.getString("hostConfig.deployDescriptor.hiddenDir",
621                                 contextXml.getAbsolutePath(), dir.getAbsolutePath()));
622                     }
623                 } else {
624                     log.warn(sm.getString("hostConfig.deployDescriptor.localDocBaseSpecified",
625                              docBase));
626                     // Ignore specified docBase
627                     context.setDocBase(null);
628                 }
629             }
630
631             host.addChild(context);
632         } catch (Throwable t) {
633             ExceptionUtils.handleThrowable(t);
634             log.error(sm.getString("hostConfig.deployDescriptor.error",
635                                    contextXml.getAbsolutePath()), t);
636         } finally {
637             // Get paths for WAR and expanded WAR in appBase
638
639             // default to appBase dir + name
640             expandedDocBase = new File(host.getAppBaseFile(), cn.getBaseName());
641             if (context.getDocBase() != null
642                     && !context.getDocBase().toLowerCase(Locale.ENGLISH).endsWith(".war")) {
643                 // first assume docBase is absolute
644                 expandedDocBase = new File(context.getDocBase());
645                 if (!expandedDocBase.isAbsolute()) {
646                     // if docBase specified and relative, it must be relative to appBase
647                     expandedDocBase = new File(host.getAppBaseFile(), context.getDocBase());
648                 }
649             }
650
651             boolean unpackWAR = unpackWARs;
652             if (unpackWAR && context instanceof StandardContext) {
653                 unpackWAR = ((StandardContext) context).getUnpackWAR();
654             }
655
656             // Add the eventual unpacked WAR and all the resources which will be
657             // watched inside it
658             if (isExternalWar) {
659                 if (unpackWAR) {
660                     deployedApp.redeployResources.put(expandedDocBase.getAbsolutePath(),
661                             Long.valueOf(expandedDocBase.lastModified()));
662                     addWatchedResources(deployedApp, expandedDocBase.getAbsolutePath(), context);
663                 } else {
664                     addWatchedResources(deployedApp, null, context);
665                 }
666             } else {
667                 // Find an existing matching war and expanded folder
668                 if (!isExternal) {
669                     File warDocBase = new File(expandedDocBase.getAbsolutePath() + ".war");
670                     if (warDocBase.exists()) {
671                         deployedApp.redeployResources.put(warDocBase.getAbsolutePath(),
672                                 Long.valueOf(warDocBase.lastModified()));
673                     } else {
674                         // Trigger a redeploy if a WAR is added
675                         deployedApp.redeployResources.put(
676                                 warDocBase.getAbsolutePath(),
677                                 Long.valueOf(0));
678                     }
679                 }
680                 if (unpackWAR) {
681                     deployedApp.redeployResources.put(expandedDocBase.getAbsolutePath(),
682                             Long.valueOf(expandedDocBase.lastModified()));
683                     addWatchedResources(deployedApp,
684                             expandedDocBase.getAbsolutePath(), context);
685                 } else {
686                     addWatchedResources(deployedApp, null, context);
687                 }
688                 if (!isExternal) {
689                     // For external docBases, the context.xml will have been
690                     // added above.
691                     deployedApp.redeployResources.put(
692                             contextXml.getAbsolutePath(),
693                             Long.valueOf(contextXml.lastModified()));
694                 }
695             }
696             // Add the global redeploy resources (which are never deleted) at
697             // the end so they don't interfere with the deletion process
698             addGlobalRedeployResources(deployedApp);
699         }
700
701         if (host.findChild(context.getName()) != null) {
702             deployed.put(context.getName(), deployedApp);
703         }
704
705         if (log.isInfoEnabled()) {
706             log.info(sm.getString("hostConfig.deployDescriptor.finished",
707                 contextXml.getAbsolutePath(), Long.valueOf(System.currentTimeMillis() - startTime)));
708         }
709     }
710
711
712     /**
713      * Deploy WAR files.
714      * @param appBase The base path for applications
715      * @param files The WARs to deploy
716      */

717     protected void deployWARs(File appBase, String[] files) {
718
719         if (files == null)
720             return;
721
722         ExecutorService es = host.getStartStopExecutor();
723         List<Future<?>> results = new ArrayList<>();
724
725         for (int i = 0; i < files.length; i++) {
726
727             if (files[i].equalsIgnoreCase("META-INF"))
728                 continue;
729             if (files[i].equalsIgnoreCase("WEB-INF"))
730                 continue;
731             File war = new File(appBase, files[i]);
732             if (files[i].toLowerCase(Locale.ENGLISH).endsWith(".war") &&
733                     war.isFile() && !invalidWars.contains(files[i]) ) {
734
735                 ContextName cn = new ContextName(files[i], true);
736
737                 if (isServiced(cn.getName())) {
738                     continue;
739                 }
740                 if (deploymentExists(cn.getName())) {
741                     DeployedApplication app = deployed.get(cn.getName());
742                     boolean unpackWAR = unpackWARs;
743                     if (unpackWAR && host.findChild(cn.getName()) instanceof StandardContext) {
744                         unpackWAR = ((StandardContext) host.findChild(cn.getName())).getUnpackWAR();
745                     }
746                     if (!unpackWAR && app != null) {
747                         // Need to check for a directory that should not be
748                         // there
749                         File dir = new File(appBase, cn.getBaseName());
750                         if (dir.exists()) {
751                             if (!app.loggedDirWarning) {
752                                 log.warn(sm.getString(
753                                         "hostConfig.deployWar.hiddenDir",
754                                         dir.getAbsoluteFile(),
755                                         war.getAbsoluteFile()));
756                                 app.loggedDirWarning = true;
757                             }
758                         } else {
759                             app.loggedDirWarning = false;
760                         }
761                     }
762                     continue;
763                 }
764
765                 // Check for WARs with /../ /./ or similar sequences in the name
766                 if (!validateContextPath(appBase, cn.getBaseName())) {
767                     log.error(sm.getString(
768                             "hostConfig.illegalWarName", files[i]));
769                     invalidWars.add(files[i]);
770                     continue;
771                 }
772
773                 results.add(es.submit(new DeployWar(this, cn, war)));
774             }
775         }
776
777         for (Future<?> result : results) {
778             try {
779                 result.get();
780             } catch (Exception e) {
781                 log.error(sm.getString(
782                         "hostConfig.deployWar.threaded.error"), e);
783             }
784         }
785     }
786
787
788     private boolean validateContextPath(File appBase, String contextPath) {
789         // More complicated than the ideal as the canonical path may or may
790         // not end with File.separator for a directory
791
792         StringBuilder docBase;
793         String canonicalDocBase = null;
794
795         try {
796             String canonicalAppBase = appBase.getCanonicalPath();
797             docBase = new StringBuilder(canonicalAppBase);
798             if (canonicalAppBase.endsWith(File.separator)) {
799                 docBase.append(contextPath.substring(1).replace(
800                         '/', File.separatorChar));
801             } else {
802                 docBase.append(contextPath.replace('/', File.separatorChar));
803             }
804             // At this point docBase should be canonical but will not end
805             // with File.separator
806
807             canonicalDocBase =
808                 (new File(docBase.toString())).getCanonicalPath();
809
810             // If the canonicalDocBase ends with File.separator, add one to
811             // docBase before they are compared
812             if (canonicalDocBase.endsWith(File.separator)) {
813                 docBase.append(File.separator);
814             }
815         } catch (IOException ioe) {
816             return false;
817         }
818
819         // Compare the two. If they are not the same, the contextPath must
820         // have /../ like sequences in it
821         return canonicalDocBase.equals(docBase.toString());
822     }
823
824     /**
825      * Deploy packed WAR.
826      * @param cn The context name
827      * @param war The WAR file
828      */

829     protected void deployWAR(ContextName cn, File war) {
830
831         File xml = new File(host.getAppBaseFile(),
832                 cn.getBaseName() + "/" + Constants.ApplicationContextXml);
833
834         File warTracker = new File(host.getAppBaseFile(), cn.getBaseName() + Constants.WarTracker);
835
836         boolean xmlInWar = false;
837         try (JarFile jar = new JarFile(war)) {
838             JarEntry entry = jar.getJarEntry(Constants.ApplicationContextXml);
839             if (entry != null) {
840                 xmlInWar = true;
841             }
842         } catch (IOException e) {
843             /* Ignore */
844         }
845
846         // If there is an expanded directory then any xml in that directory
847         // should only be used if the directory is not out of date and
848         // unpackWARs is true. Note the code below may apply further limits
849         boolean useXml = false;
850         // If the xml file exists then expandedDir must exists so no need to
851         // test that here
852         if (xml.exists() && unpackWARs &&
853                 (!warTracker.exists() || warTracker.lastModified() == war.lastModified())) {
854             useXml = true;
855         }
856
857         Context context = null;
858         boolean deployThisXML = isDeployThisXML(war, cn);
859
860         try {
861             if (deployThisXML && useXml && !copyXML) {
862                 synchronized (digesterLock) {
863                     try {
864                         context = (Context) digester.parse(xml);
865                     } catch (Exception e) {
866                         log.error(sm.getString(
867                                 "hostConfig.deployDescriptor.error",
868                                 war.getAbsolutePath()), e);
869                     } finally {
870                         digester.reset();
871                         if (context == null) {
872                             context = new FailedContext();
873                         }
874                     }
875                 }
876                 context.setConfigFile(xml.toURI().toURL());
877             } else if (deployThisXML && xmlInWar) {
878                 synchronized (digesterLock) {
879                     try (JarFile jar = new JarFile(war)) {
880                         JarEntry entry = jar.getJarEntry(Constants.ApplicationContextXml);
881                         try (InputStream istream = jar.getInputStream(entry)) {
882                             context = (Context) digester.parse(istream);
883                         }
884                     } catch (Exception e) {
885                         log.error(sm.getString(
886                                 "hostConfig.deployDescriptor.error",
887                                 war.getAbsolutePath()), e);
888                     } finally {
889                         digester.reset();
890                         if (context == null) {
891                             context = new FailedContext();
892                         }
893                         context.setConfigFile(
894                                 UriUtil.buildJarUrl(war, Constants.ApplicationContextXml));
895                     }
896                 }
897             } else if (!deployThisXML && xmlInWar) {
898                 // Block deployment as META-INF/context.xml may contain security
899                 // configuration necessary for a secure deployment.
900                 log.error(sm.getString("hostConfig.deployDescriptor.blocked",
901                         cn.getPath(), Constants.ApplicationContextXml,
902                         new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml")));
903             } else {
904                 context = (Context) Class.forName(contextClass).getConstructor().newInstance();
905             }
906         } catch (Throwable t) {
907             ExceptionUtils.handleThrowable(t);
908             log.error(sm.getString("hostConfig.deployWar.error",
909                     war.getAbsolutePath()), t);
910         } finally {
911             if (context == null) {
912                 context = new FailedContext();
913             }
914         }
915
916         boolean copyThisXml = false;
917         if (deployThisXML) {
918             if (host instanceof StandardHost) {
919                 copyThisXml = ((StandardHost) host).isCopyXML();
920             }
921
922             // If Host is using default value Context can override it.
923             if (!copyThisXml && context instanceof StandardContext) {
924                 copyThisXml = ((StandardContext) context).getCopyXML();
925             }
926
927             if (xmlInWar && copyThisXml) {
928                 // Change location of XML file to config base
929                 xml = new File(host.getConfigBaseFile(),
930                         cn.getBaseName() + ".xml");
931                 try (JarFile jar = new JarFile(war)) {
932                     JarEntry entry = jar.getJarEntry(Constants.ApplicationContextXml);
933                     try (InputStream istream = jar.getInputStream(entry);
934                             OutputStream ostream = new FileOutputStream(xml)) {
935                         IOTools.flow(istream, ostream);
936                     }
937                 } catch (IOException e) {
938                     /* Ignore */
939                 }
940             }
941         }
942
943         DeployedApplication deployedApp = new DeployedApplication(cn.getName(),
944                 xml.exists() && deployThisXML && copyThisXml);
945
946         long startTime = 0;
947         // Deploy the application in this WAR file
948         if(log.isInfoEnabled()) {
949             startTime = System.currentTimeMillis();
950             log.info(sm.getString("hostConfig.deployWar",
951                     war.getAbsolutePath()));
952         }
953
954         try {
955             // Populate redeploy resources with the WAR file
956             deployedApp.redeployResources.put
957                 (war.getAbsolutePath(), Long.valueOf(war.lastModified()));
958
959             if (deployThisXML && xml.exists() && copyThisXml) {
960                 deployedApp.redeployResources.put(xml.getAbsolutePath(),
961                         Long.valueOf(xml.lastModified()));
962             } else {
963                 // In case an XML file is added to the config base later
964                 deployedApp.redeployResources.put(
965                         (new File(host.getConfigBaseFile(),
966                                 cn.getBaseName() + ".xml")).getAbsolutePath(),
967                         Long.valueOf(0));
968             }
969
970             Class<?> clazz = Class.forName(host.getConfigClass());
971             LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
972             context.addLifecycleListener(listener);
973
974             context.setName(cn.getName());
975             context.setPath(cn.getPath());
976             context.setWebappVersion(cn.getVersion());
977             context.setDocBase(cn.getBaseName() + ".war");
978             host.addChild(context);
979         } catch (Throwable t) {
980             ExceptionUtils.handleThrowable(t);
981             log.error(sm.getString("hostConfig.deployWar.error",
982                     war.getAbsolutePath()), t);
983         } finally {
984             // If we're unpacking WARs, the docBase will be mutated after
985             // starting the context
986             boolean unpackWAR = unpackWARs;
987             if (unpackWAR && context instanceof StandardContext) {
988                 unpackWAR = ((StandardContext) context).getUnpackWAR();
989             }
990             if (unpackWAR && context.getDocBase() != null) {
991                 File docBase = new File(host.getAppBaseFile(), cn.getBaseName());
992                 deployedApp.redeployResources.put(docBase.getAbsolutePath(),
993                         Long.valueOf(docBase.lastModified()));
994                 addWatchedResources(deployedApp, docBase.getAbsolutePath(),
995                         context);
996                 if (deployThisXML && !copyThisXml && (xmlInWar || xml.exists())) {
997                     deployedApp.redeployResources.put(xml.getAbsolutePath(),
998                             Long.valueOf(xml.lastModified()));
999                 }
1000             } else {
1001                 // Passing null for docBase means that no resources will be
1002                 // watched. This will be logged at debug level.
1003                 addWatchedResources(deployedApp, null, context);
1004             }
1005             // Add the global redeploy resources (which are never deleted) at
1006             // the end so they don't interfere with the deletion process
1007             addGlobalRedeployResources(deployedApp);
1008         }
1009
1010         deployed.put(cn.getName(), deployedApp);
1011
1012         if (log.isInfoEnabled()) {
1013             log.info(sm.getString("hostConfig.deployWar.finished",
1014                 war.getAbsolutePath(), Long.valueOf(System.currentTimeMillis() - startTime)));
1015         }
1016     }
1017
1018
1019     /**
1020      * Deploy exploded webapps.
1021      * @param appBase The base path for applications
1022      * @param files The exploded webapps that should be deployed
1023      */

1024     protected void deployDirectories(File appBase, String[] files) {
1025
1026         if (files == null)
1027             return;
1028
1029         ExecutorService es = host.getStartStopExecutor();
1030         List<Future<?>> results = new ArrayList<>();
1031
1032         for (int i = 0; i < files.length; i++) {
1033
1034             if (files[i].equalsIgnoreCase("META-INF"))
1035                 continue;
1036             if (files[i].equalsIgnoreCase("WEB-INF"))
1037                 continue;
1038             File dir = new File(appBase, files[i]);
1039             if (dir.isDirectory()) {
1040                 ContextName cn = new ContextName(files[i], false);
1041
1042                 if (isServiced(cn.getName()) || deploymentExists(cn.getName()))
1043                     continue;
1044
1045                 results.add(es.submit(new DeployDirectory(this, cn, dir)));
1046             }
1047         }
1048
1049         for (Future<?> result : results) {
1050             try {
1051                 result.get();
1052             } catch (Exception e) {
1053                 log.error(sm.getString(
1054                         "hostConfig.deployDir.threaded.error"), e);
1055             }
1056         }
1057     }
1058
1059
1060     /**
1061      * Deploy exploded webapp.
1062      * @param cn The context name
1063      * @param dir The path to the root folder of the weapp
1064      */

1065     protected void deployDirectory(ContextName cn, File dir) {
1066
1067
1068         long startTime = 0;
1069         // Deploy the application in this directory
1070         if( log.isInfoEnabled() ) {
1071             startTime = System.currentTimeMillis();
1072             log.info(sm.getString("hostConfig.deployDir",
1073                     dir.getAbsolutePath()));
1074         }
1075
1076         Context context = null;
1077         File xml = new File(dir, Constants.ApplicationContextXml);
1078         File xmlCopy =
1079                 new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml");
1080
1081
1082         DeployedApplication deployedApp;
1083         boolean copyThisXml = isCopyXML();
1084         boolean deployThisXML = isDeployThisXML(dir, cn);
1085
1086         try {
1087             if (deployThisXML && xml.exists()) {
1088                 synchronized (digesterLock) {
1089                     try {
1090                         context = (Context) digester.parse(xml);
1091                     } catch (Exception e) {
1092                         log.error(sm.getString(
1093                                 "hostConfig.deployDescriptor.error",
1094                                 xml), e);
1095                         context = new FailedContext();
1096                     } finally {
1097                         digester.reset();
1098                         if (context == null) {
1099                             context = new FailedContext();
1100                         }
1101                     }
1102                 }
1103
1104                 if (copyThisXml == false && context instanceof StandardContext) {
1105                     // Host is using default value. Context may override it.
1106                     copyThisXml = ((StandardContext) context).getCopyXML();
1107                 }
1108
1109                 if (copyThisXml) {
1110                     Files.copy(xml.toPath(), xmlCopy.toPath());
1111                     context.setConfigFile(xmlCopy.toURI().toURL());
1112                 } else {
1113                     context.setConfigFile(xml.toURI().toURL());
1114                 }
1115             } else if (!deployThisXML && xml.exists()) {
1116                 // Block deployment as META-INF/context.xml may contain security
1117                 // configuration necessary for a secure deployment.
1118                 log.error(sm.getString("hostConfig.deployDescriptor.blocked",
1119                         cn.getPath(), xml, xmlCopy));
1120                 context = new FailedContext();
1121             } else {
1122                 context = (Context) Class.forName(contextClass).getConstructor().newInstance();
1123             }
1124
1125             Class<?> clazz = Class.forName(host.getConfigClass());
1126             LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
1127             context.addLifecycleListener(listener);
1128
1129             context.setName(cn.getName());
1130             context.setPath(cn.getPath());
1131             context.setWebappVersion(cn.getVersion());
1132             context.setDocBase(cn.getBaseName());
1133             host.addChild(context);
1134         } catch (Throwable t) {
1135             ExceptionUtils.handleThrowable(t);
1136             log.error(sm.getString("hostConfig.deployDir.error",
1137                     dir.getAbsolutePath()), t);
1138         } finally {
1139             deployedApp = new DeployedApplication(cn.getName(),
1140                     xml.exists() && deployThisXML && copyThisXml);
1141
1142             // Fake re-deploy resource to detect if a WAR is added at a later
1143             // point
1144             deployedApp.redeployResources.put(dir.getAbsolutePath() + ".war",
1145                     Long.valueOf(0));
1146             deployedApp.redeployResources.put(dir.getAbsolutePath(),
1147                     Long.valueOf(dir.lastModified()));
1148             if (deployThisXML && xml.exists()) {
1149                 if (copyThisXml) {
1150                     deployedApp.redeployResources.put(
1151                             xmlCopy.getAbsolutePath(),
1152                             Long.valueOf(xmlCopy.lastModified()));
1153                 } else {
1154                     deployedApp.redeployResources.put(
1155                             xml.getAbsolutePath(),
1156                             Long.valueOf(xml.lastModified()));
1157                     // Fake re-deploy resource to detect if a context.xml file is
1158                     // added at a later point
1159                     deployedApp.redeployResources.put(
1160                             xmlCopy.getAbsolutePath(),
1161                             Long.valueOf(0));
1162                 }
1163             } else {
1164                 // Fake re-deploy resource to detect if a context.xml file is
1165                 // added at a later point
1166                 deployedApp.redeployResources.put(
1167                         xmlCopy.getAbsolutePath(),
1168                         Long.valueOf(0));
1169                 if (!xml.exists()) {
1170                     deployedApp.redeployResources.put(
1171                             xml.getAbsolutePath(),
1172                             Long.valueOf(0));
1173                 }
1174             }
1175             addWatchedResources(deployedApp, dir.getAbsolutePath(), context);
1176             // Add the global redeploy resources (which are never deleted) at
1177             // the end so they don't interfere with the deletion process
1178             addGlobalRedeployResources(deployedApp);
1179         }
1180
1181         deployed.put(cn.getName(), deployedApp);
1182
1183         if( log.isInfoEnabled() ) {
1184             log.info(sm.getString("hostConfig.deployDir.finished",
1185                     dir.getAbsolutePath(), Long.valueOf(System.currentTimeMillis() - startTime)));
1186         }
1187     }
1188
1189
1190     /**
1191      * Check if a webapp is already deployed in this host.
1192      *
1193      * @param contextName of the context which will be checked
1194      * @return <code>true</code> if the specified deployment exists
1195      */

1196     protected boolean deploymentExists(String contextName) {
1197         return (deployed.containsKey(contextName) ||
1198                 (host.findChild(contextName) != null));
1199     }
1200
1201
1202     /**
1203      * Add watched resources to the specified Context.
1204      * @param app HostConfig deployed app
1205      * @param docBase web app docBase
1206      * @param context web application context
1207      */

1208     protected void addWatchedResources(DeployedApplication app, String docBase,
1209             Context context) {
1210         // FIXME: Feature idea. Add support for patterns (ex: WEB-INF/*,
1211         //        WEB-INF/*.xml), where we would only check if at least one
1212         //        resource is newer than app.timestamp
1213         File docBaseFile = null;
1214         if (docBase != null) {
1215             docBaseFile = new File(docBase);
1216             if (!docBaseFile.isAbsolute()) {
1217                 docBaseFile = new File(host.getAppBaseFile(), docBase);
1218             }
1219         }
1220         String[] watchedResources = context.findWatchedResources();
1221         for (int i = 0; i < watchedResources.length; i++) {
1222             File resource = new File(watchedResources[i]);
1223             if (!resource.isAbsolute()) {
1224                 if (docBase != null) {
1225                     resource = new File(docBaseFile, watchedResources[i]);
1226                 } else {
1227                     if(log.isDebugEnabled())
1228                         log.debug("Ignoring non-existent WatchedResource '" +
1229                                 resource.getAbsolutePath() + "'");
1230                     continue;
1231                 }
1232             }
1233             if(log.isDebugEnabled())
1234                 log.debug("Watching WatchedResource '" +
1235                         resource.getAbsolutePath() + "'");
1236             app.reloadResources.put(resource.getAbsolutePath(),
1237                     Long.valueOf(resource.lastModified()));
1238         }
1239     }
1240
1241
1242     protected void addGlobalRedeployResources(DeployedApplication app) {
1243         // Redeploy resources processing is hard-coded to never delete this file
1244         File hostContextXml =
1245                 new File(getConfigBaseName(), Constants.HostContextXml);
1246         if (hostContextXml.isFile()) {
1247             app.redeployResources.put(hostContextXml.getAbsolutePath(),
1248                     Long.valueOf(hostContextXml.lastModified()));
1249         }
1250
1251         // Redeploy resources in CATALINA_BASE/conf are never deleted
1252         File globalContextXml =
1253                 returnCanonicalPath(Constants.DefaultContextXml);
1254         if (globalContextXml.isFile()) {
1255             app.redeployResources.put(globalContextXml.getAbsolutePath(),
1256                     Long.valueOf(globalContextXml.lastModified()));
1257         }
1258     }
1259
1260
1261     /**
1262      * Check resources for redeployment and reloading.
1263      *
1264      * @param app   The web application to check
1265      * @param skipFileModificationResolutionCheck
1266      *              When checking files for modification should the check that
1267      *              requires that any file modification must have occurred at
1268      *              least as long ago as the resolution of the file time stamp
1269      *              be skipped
1270      */

1271     protected synchronized void checkResources(DeployedApplication app,
1272             boolean skipFileModificationResolutionCheck) {
1273         String[] resources =
1274             app.redeployResources.keySet().toArray(new String[0]);
1275         // Offset the current time by the resolution of File.lastModified()
1276         long currentTimeWithResolutionOffset =
1277                 System.currentTimeMillis() - FILE_MODIFICATION_RESOLUTION_MS;
1278         for (int i = 0; i < resources.length; i++) {
1279             File resource = new File(resources[i]);
1280             if (log.isDebugEnabled())
1281                 log.debug("Checking context[" + app.name +
1282                         "] redeploy resource " + resource);
1283             long lastModified =
1284                     app.redeployResources.get(resources[i]).longValue();
1285             if (resource.exists() || lastModified == 0) {
1286                 // File.lastModified() has a resolution of 1s (1000ms). The last
1287                 // modified time has to be more than 1000ms ago to ensure that
1288                 // modifications that take place in the same second are not
1289                 // missed. See Bug 57765.
1290                 if (resource.lastModified() != lastModified && (!host.getAutoDeploy() ||
1291                         resource.lastModified() < currentTimeWithResolutionOffset ||
1292                         skipFileModificationResolutionCheck)) {
1293                     if (resource.isDirectory()) {
1294                         // No action required for modified directory
1295                         app.redeployResources.put(resources[i],
1296                                 Long.valueOf(resource.lastModified()));
1297                     } else if (app.hasDescriptor &&
1298                             resource.getName().toLowerCase(
1299                                     Locale.ENGLISH).endsWith(".war")) {
1300                         // Modified WAR triggers a reload if there is an XML
1301                         // file present
1302                         // The only resource that should be deleted is the
1303                         // expanded WAR (if any)
1304                         Context context = (Context) host.findChild(app.name);
1305                         String docBase = context.getDocBase();
1306                         if (!docBase.toLowerCase(Locale.ENGLISH).endsWith(".war")) {
1307                             // This is an expanded directory
1308                             File docBaseFile = new File(docBase);
1309                             if (!docBaseFile.isAbsolute()) {
1310                                 docBaseFile = new File(host.getAppBaseFile(),
1311                                         docBase);
1312                             }
1313                             reload(app, docBaseFile, resource.getAbsolutePath());
1314                         } else {
1315                             reload(app, nullnull);
1316                         }
1317                         // Update times
1318                         app.redeployResources.put(resources[i],
1319                                 Long.valueOf(resource.lastModified()));
1320                         app.timestamp = System.currentTimeMillis();
1321                         boolean unpackWAR = unpackWARs;
1322                         if (unpackWAR && context instanceof StandardContext) {
1323                             unpackWAR = ((StandardContext) context).getUnpackWAR();
1324                         }
1325                         if (unpackWAR) {
1326                             addWatchedResources(app, context.getDocBase(), context);
1327                         } else {
1328                             addWatchedResources(app, null, context);
1329                         }
1330                         return;
1331                     } else {
1332                         // Everything else triggers a redeploy
1333                         // (just need to undeploy here, deploy will follow)
1334                         undeploy(app);
1335                         deleteRedeployResources(app, resources, i, false);
1336                         return;
1337                     }
1338                 }
1339             } else {
1340                 // There is a chance the the resource was only missing
1341                 // temporarily eg renamed during a text editor save
1342                 try {
1343                     Thread.sleep(500);
1344                 } catch (InterruptedException e1) {
1345                     // Ignore
1346                 }
1347                 // Recheck the resource to see if it was really deleted
1348                 if (resource.exists()) {
1349                     continue;
1350                 }
1351                 // Undeploy application
1352                 undeploy(app);
1353                 deleteRedeployResources(app, resources, i, true);
1354                 return;
1355             }
1356         }
1357         resources = app.reloadResources.keySet().toArray(new String[0]);
1358         boolean update = false;
1359         for (int i = 0; i < resources.length; i++) {
1360             File resource = new File(resources[i]);
1361             if (log.isDebugEnabled()) {
1362                 log.debug("Checking context[" + app.name + "] reload resource " + resource);
1363             }
1364             long lastModified = app.reloadResources.get(resources[i]).longValue();
1365             // File.lastModified() has a resolution of 1s (1000ms). The last
1366             // modified time has to be more than 1000ms ago to ensure that
1367             // modifications that take place in the same second are not
1368             // missed. See Bug 57765.
1369             if ((resource.lastModified() != lastModified &&
1370                     (!host.getAutoDeploy() ||
1371                             resource.lastModified() < currentTimeWithResolutionOffset ||
1372                             skipFileModificationResolutionCheck)) ||
1373                     update) {
1374                 if (!update) {
1375                     // Reload application
1376                     reload(app, nullnull);
1377                     update = true;
1378                 }
1379                 // Update times. More than one file may have been updated. We
1380                 // don't want to trigger a series of reloads.
1381                 app.reloadResources.put(resources[i],
1382                         Long.valueOf(resource.lastModified()));
1383             }
1384             app.timestamp = System.currentTimeMillis();
1385         }
1386     }
1387
1388
1389     /*
1390      * Note: If either of fileToRemove and newDocBase are null, both will be
1391      *       ignored.
1392      */

1393     private void reload(DeployedApplication app, File fileToRemove, String newDocBase) {
1394         if(log.isInfoEnabled())
1395             log.info(sm.getString("hostConfig.reload", app.name));
1396         Context context = (Context) host.findChild(app.name);
1397         if (context.getState().isAvailable()) {
1398             if (fileToRemove != null && newDocBase != null) {
1399                 context.addLifecycleListener(
1400                         new ExpandedDirectoryRemovalListener(fileToRemove, newDocBase));
1401             }
1402             // Reload catches and logs exceptions
1403             context.reload();
1404         } else {
1405             // If the context was not started (for example an error
1406             // in web.xml) we'll still get to try to start
1407             if (fileToRemove != null && newDocBase != null) {
1408                 ExpandWar.delete(fileToRemove);
1409                 context.setDocBase(newDocBase);
1410             }
1411             try {
1412                 context.start();
1413             } catch (Exception e) {
1414                 log.error(sm.getString("hostConfig.context.restart", app.name), e);
1415             }
1416         }
1417     }
1418
1419
1420     private void undeploy(DeployedApplication app) {
1421         if (log.isInfoEnabled())
1422             log.info(sm.getString("hostConfig.undeploy", app.name));
1423         Container context = host.findChild(app.name);
1424         try {
1425             host.removeChild(context);
1426         } catch (Throwable t) {
1427             ExceptionUtils.handleThrowable(t);
1428             log.warn(sm.getString
1429                      ("hostConfig.context.remove", app.name), t);
1430         }
1431         deployed.remove(app.name);
1432     }
1433
1434
1435     private void deleteRedeployResources(DeployedApplication app, String[] resources, int i,
1436             boolean deleteReloadResources) {
1437
1438         // Delete other redeploy resources
1439         for (int j = i + 1; j < resources.length; j++) {
1440             File current = new File(resources[j]);
1441             // Never delete per host context.xml defaults
1442             if (Constants.HostContextXml.equals(current.getName())) {
1443                 continue;
1444             }
1445             // Only delete resources in the appBase or the
1446             // host's configBase
1447             if (isDeletableResource(app, current)) {
1448                 if (log.isDebugEnabled()) {
1449                     log.debug("Delete " + current);
1450                 }
1451                 ExpandWar.delete(current);
1452             }
1453         }
1454
1455         // Delete reload resources (to remove any remaining .xml descriptor)
1456         if (deleteReloadResources) {
1457             String[] resources2 = app.reloadResources.keySet().toArray(new String[0]);
1458             for (int j = 0; j < resources2.length; j++) {
1459                 File current = new File(resources2[j]);
1460                 // Never delete per host context.xml defaults
1461                 if (Constants.HostContextXml.equals(current.getName())) {
1462                     continue;
1463                 }
1464                 // Only delete resources in the appBase or the host's
1465                 // configBase
1466                 if (isDeletableResource(app, current)) {
1467                     if (log.isDebugEnabled()) {
1468                         log.debug("Delete " + current);
1469                     }
1470                     ExpandWar.delete(current);
1471                 }
1472             }
1473         }
1474     }
1475
1476
1477     /*
1478      * Delete any resource that would trigger the automatic deployment code to
1479      * re-deploy the application. This means deleting:
1480      * - any resource located in the appBase
1481      * - any deployment descriptor located under the configBase
1482      * - symlinks in the appBase or configBase for either of the above
1483      */

1484     private boolean isDeletableResource(DeployedApplication app, File resource) {
1485         // The resource may be a file, a directory or a symlink to a file or
1486         // directory.
1487
1488         // Check that the resource is absolute. This should always be the case.
1489         if (!resource.isAbsolute()) {
1490             log.warn(sm.getString("hostConfig.resourceNotAbsolute", app.name, resource));
1491             return false;
1492         }
1493
1494         // Determine where the resource is located
1495         String canonicalLocation;
1496         try {
1497             canonicalLocation = resource.getParentFile().getCanonicalPath();
1498         } catch (IOException e) {
1499             log.warn(sm.getString(
1500                     "hostConfig.canonicalizing", resource.getParentFile(), app.name), e);
1501             return false;
1502         }
1503
1504         String canonicalAppBase;
1505         try {
1506             canonicalAppBase = host.getAppBaseFile().getCanonicalPath();
1507         } catch (IOException e) {
1508             log.warn(sm.getString(
1509                     "hostConfig.canonicalizing", host.getAppBaseFile(), app.name), e);
1510             return false;
1511         }
1512
1513         if (canonicalLocation.equals(canonicalAppBase)) {
1514             // Resource is located in the appBase so it may be deleted
1515             return true;
1516         }
1517
1518         String canonicalConfigBase;
1519         try {
1520             canonicalConfigBase = host.getConfigBaseFile().getCanonicalPath();
1521         } catch (IOException e) {
1522             log.warn(sm.getString(
1523                     "hostConfig.canonicalizing", host.getConfigBaseFile(), app.name), e);
1524             return false;
1525         }
1526
1527         if (canonicalLocation.equals(canonicalConfigBase) &&
1528                 resource.getName().endsWith(".xml")) {
1529             // Resource is an xml file in the configBase so it may be deleted
1530             return true;
1531         }
1532
1533         // All other resources should not be deleted
1534         return false;
1535     }
1536
1537
1538     public void beforeStart() {
1539         if (host.getCreateDirs()) {
1540             File[] dirs = new File[] {host.getAppBaseFile(),host.getConfigBaseFile()};
1541             for (int i=0; i<dirs.length; i++) {
1542                 if (!dirs[i].mkdirs() && !dirs[i].isDirectory()) {
1543                     log.error(sm.getString("hostConfig.createDirs",dirs[i]));
1544                 }
1545             }
1546         }
1547     }
1548
1549
1550     /**
1551      * Process a "start" event for this Host.
1552      */

1553     public void start() {
1554
1555         if (log.isDebugEnabled())
1556             log.debug(sm.getString("hostConfig.start"));
1557
1558         try {
1559             ObjectName hostON = host.getObjectName();
1560             oname = new ObjectName
1561                 (hostON.getDomain() + ":type=Deployer,host=" + host.getName());
1562             Registry.getRegistry(nullnull).registerComponent
1563                 (this, oname, this.getClass().getName());
1564         } catch (Exception e) {
1565             log.warn(sm.getString("hostConfig.jmx.register", oname), e);
1566         }
1567
1568         if (!host.getAppBaseFile().isDirectory()) {
1569             log.error(sm.getString("hostConfig.appBase", host.getName(),
1570                     host.getAppBaseFile().getPath()));
1571             host.setDeployOnStartup(false);
1572             host.setAutoDeploy(false);
1573         }
1574
1575         if (host.getDeployOnStartup())
1576             deployApps();
1577
1578     }
1579
1580
1581     /**
1582      * Process a "stop" event for this Host.
1583      */

1584     public void stop() {
1585
1586         if (log.isDebugEnabled())
1587             log.debug(sm.getString("hostConfig.stop"));
1588
1589         if (oname != null) {
1590             try {
1591                 Registry.getRegistry(nullnull).unregisterComponent(oname);
1592             } catch (Exception e) {
1593                 log.warn(sm.getString("hostConfig.jmx.unregister", oname), e);
1594             }
1595         }
1596         oname = null;
1597     }
1598
1599
1600     /**
1601      * Check status of all webapps.
1602      */

1603     protected void check() {
1604
1605         if (host.getAutoDeploy()) {
1606             // Check for resources modification to trigger redeployment
1607             DeployedApplication[] apps =
1608                 deployed.values().toArray(new DeployedApplication[0]);
1609             for (int i = 0; i < apps.length; i++) {
1610                 if (!isServiced(apps[i].name))
1611                     checkResources(apps[i], false);
1612             }
1613
1614             // Check for old versions of applications that can now be undeployed
1615             if (host.getUndeployOldVersions()) {
1616                 checkUndeploy();
1617             }
1618
1619             // Hotdeploy applications
1620             deployApps();
1621         }
1622     }
1623
1624
1625     /**
1626      * Check status of a specific web application and reload, redeploy or deploy
1627      * it as necessary. This method is for use with functionality such as
1628      * management web applications that upload new/updated web applications and
1629      * need to trigger the appropriate action to deploy them. This method
1630      * assumes that the web application is currently marked as serviced and that
1631      * any uploading/updating has been completed before this method is called.
1632      * Any action taken as a result of the checks will complete before this
1633      * method returns.
1634      *
1635      * @param name The name of the web application to check
1636      */

1637     public void check(String name) {
1638         DeployedApplication app = deployed.get(name);
1639         if (app != null) {
1640             checkResources(app, true);
1641         }
1642         deployApps(name);
1643     }
1644
1645     /**
1646      * Check for old versions of applications using parallel deployment that are
1647      * now unused (have no active sessions) and undeploy any that are found.
1648      */

1649     public synchronized void checkUndeploy() {
1650         if (deployed.size() < 2) {
1651             return;
1652         }
1653
1654         // Need ordered set of names
1655         SortedSet<String> sortedAppNames = new TreeSet<>();
1656         sortedAppNames.addAll(deployed.keySet());
1657
1658         Iterator<String> iter = sortedAppNames.iterator();
1659
1660         ContextName previous = new ContextName(iter.next(), false);
1661         do {
1662             ContextName current = new ContextName(iter.next(), false);
1663
1664             if (current.getPath().equals(previous.getPath())) {
1665                 // Current and previous are same path - current will always
1666                 // be a later version
1667                 Context previousContext = (Context) host.findChild(previous.getName());
1668                 Context currentContext = (Context) host.findChild(current.getName());
1669                 if (previousContext != null && currentContext != null &&
1670                         currentContext.getState().isAvailable() &&
1671                         !isServiced(previous.getName())) {
1672                     Manager manager = previousContext.getManager();
1673                     if (manager != null) {
1674                         int sessionCount;
1675                         if (manager instanceof DistributedManager) {
1676                             sessionCount = ((DistributedManager) manager).getActiveSessionsFull();
1677                         } else {
1678                             sessionCount = manager.getActiveSessions();
1679                         }
1680                         if (sessionCount == 0) {
1681                             if (log.isInfoEnabled()) {
1682                                 log.info(sm.getString(
1683                                         "hostConfig.undeployVersion", previous.getName()));
1684                             }
1685                             DeployedApplication app = deployed.get(previous.getName());
1686                             String[] resources = app.redeployResources.keySet().toArray(new String[0]);
1687                             // Version is unused - undeploy it completely
1688                             // The -1 is a 'trick' to ensure all redeploy
1689                             // resources are removed
1690                             undeploy(app);
1691                             deleteRedeployResources(app, resources, -1, true);
1692                         }
1693                     }
1694                 }
1695             }
1696             previous = current;
1697         } while (iter.hasNext());
1698     }
1699
1700     /**
1701      * Add a new Context to be managed by us.
1702      * Entry point for the admin webapp, and other JMX Context controllers.
1703      * @param context The context instance
1704      */

1705     public void manageApp(Context context)  {
1706
1707         String contextName = context.getName();
1708
1709         if (deployed.containsKey(contextName))
1710             return;
1711
1712         DeployedApplication deployedApp =
1713                 new DeployedApplication(contextName, false);
1714
1715         // Add the associated docBase to the redeployed list if it's a WAR
1716         boolean isWar = false;
1717         if (context.getDocBase() != null) {
1718             File docBase = new File(context.getDocBase());
1719             if (!docBase.isAbsolute()) {
1720                 docBase = new File(host.getAppBaseFile(), context.getDocBase());
1721             }
1722             deployedApp.redeployResources.put(docBase.getAbsolutePath(),
1723                     Long.valueOf(docBase.lastModified()));
1724             if (docBase.getAbsolutePath().toLowerCase(Locale.ENGLISH).endsWith(".war")) {
1725                 isWar = true;
1726             }
1727         }
1728         host.addChild(context);
1729         // Add the eventual unpacked WAR and all the resources which will be
1730         // watched inside it
1731         boolean unpackWAR = unpackWARs;
1732         if (unpackWAR && context instanceof StandardContext) {
1733             unpackWAR = ((StandardContext) context).getUnpackWAR();
1734         }
1735         if (isWar && unpackWAR) {
1736             File docBase = new File(host.getAppBaseFile(), context.getBaseName());
1737             deployedApp.redeployResources.put(docBase.getAbsolutePath(),
1738                         Long.valueOf(docBase.lastModified()));
1739             addWatchedResources(deployedApp, docBase.getAbsolutePath(), context);
1740         } else {
1741             addWatchedResources(deployedApp, null, context);
1742         }
1743         deployed.put(contextName, deployedApp);
1744     }
1745
1746     /**
1747      * Remove a webapp from our control.
1748      * Entry point for the admin webapp, and other JMX Context controllers.
1749      * @param contextName The context name
1750      */

1751     public void unmanageApp(String contextName) {
1752         if(isServiced(contextName)) {
1753             deployed.remove(contextName);
1754             host.removeChild(host.findChild(contextName));
1755         }
1756     }
1757
1758     // ----------------------------------------------------- Instance Variables
1759
1760
1761     /**
1762      * This class represents the state of a deployed application, as well as
1763      * the monitored resources.
1764      */

1765     protected static class DeployedApplication {
1766         public DeployedApplication(String name, boolean hasDescriptor) {
1767             this.name = name;
1768             this.hasDescriptor = hasDescriptor;
1769         }
1770
1771         /**
1772          * Application context path. The assertion is that
1773          * (host.getChild(name) != null).
1774          */

1775         public final String name;
1776
1777         /**
1778          * Does this application have a context.xml descriptor file on the
1779          * host's configBase?
1780          */

1781         public final boolean hasDescriptor;
1782
1783         /**
1784          * Any modification of the specified (static) resources will cause a
1785          * redeployment of the application. If any of the specified resources is
1786          * removed, the application will be undeployed. Typically, this will
1787          * contain resources like the context.xml file, a compressed WAR path.
1788          * The value is the last modification time.
1789          */

1790         public final LinkedHashMap<String, Long> redeployResources =
1791                 new LinkedHashMap<>();
1792
1793         /**
1794          * Any modification of the specified (static) resources will cause a
1795          * reload of the application. This will typically contain resources
1796          * such as the web.xml of a webapp, but can be configured to contain
1797          * additional descriptors.
1798          * The value is the last modification time.
1799          */

1800         public final HashMap<String, Long> reloadResources = new HashMap<>();
1801
1802         /**
1803          * Instant where the application was last put in service.
1804          */

1805         public long timestamp = System.currentTimeMillis();
1806
1807         /**
1808          * In some circumstances, such as when unpackWARs is true, a directory
1809          * may be added to the appBase that is ignored. This flag indicates that
1810          * the user has been warned so that the warning is not logged on every
1811          * run of the auto deployer.
1812          */

1813         public boolean loggedDirWarning = false;
1814     }
1815
1816     private static class DeployDescriptor implements Runnable {
1817
1818         private HostConfig config;
1819         private ContextName cn;
1820         private File descriptor;
1821
1822         public DeployDescriptor(HostConfig config, ContextName cn,
1823                 File descriptor) {
1824             this.config = config;
1825             this.cn = cn;
1826             this.descriptor= descriptor;
1827         }
1828
1829         @Override
1830         public void run() {
1831             config.deployDescriptor(cn, descriptor);
1832         }
1833     }
1834
1835     private static class DeployWar implements Runnable {
1836
1837         private HostConfig config;
1838         private ContextName cn;
1839         private File war;
1840
1841         public DeployWar(HostConfig config, ContextName cn, File war) {
1842             this.config = config;
1843             this.cn = cn;
1844             this.war = war;
1845         }
1846
1847         @Override
1848         public void run() {
1849             config.deployWAR(cn, war);
1850         }
1851     }
1852
1853     private static class DeployDirectory implements Runnable {
1854
1855         private HostConfig config;
1856         private ContextName cn;
1857         private File dir;
1858
1859         public DeployDirectory(HostConfig config, ContextName cn, File dir) {
1860             this.config = config;
1861             this.cn = cn;
1862             this.dir = dir;
1863         }
1864
1865         @Override
1866         public void run() {
1867             config.deployDirectory(cn, dir);
1868         }
1869     }
1870
1871
1872     /*
1873      * The purpose of this class is to provide a way for HostConfig to get
1874      * a Context to delete an expanded WAR after the Context stops. This is to
1875      * resolve this issue described in Bug 57772. The alternative solutions
1876      * require either duplicating a lot of the Context.reload() code in
1877      * HostConfig or adding a new reload(boolean) method to Context that allows
1878      * the caller to optionally delete any expanded WAR.
1879      *
1880      * The LifecycleListener approach offers greater flexibility and enables the
1881      * behaviour to be changed / extended / removed in future without changing
1882      * the Context API.
1883      */

1884     private static class ExpandedDirectoryRemovalListener implements LifecycleListener {
1885
1886         private final File toDelete;
1887         private final String newDocBase;
1888
1889         /**
1890          * Create a listener that will ensure that any expanded WAR is removed
1891          * and the docBase set to the specified WAR.
1892          *
1893          * @param toDelete The file (a directory representing an expanded WAR)
1894          *                 to be deleted
1895          * @param newDocBase The new docBase for the Context
1896          */

1897         public ExpandedDirectoryRemovalListener(File toDelete, String newDocBase) {
1898             this.toDelete = toDelete;
1899             this.newDocBase = newDocBase;
1900         }
1901
1902         @Override
1903         public void lifecycleEvent(LifecycleEvent event) {
1904             if (Lifecycle.AFTER_STOP_EVENT.equals(event.getType())) {
1905                 // The context has stopped.
1906                 Context context = (Context) event.getLifecycle();
1907
1908                 // Remove the old expanded WAR.
1909                 ExpandWar.delete(toDelete);
1910
1911                 // Reset the docBase to trigger re-expansion of the WAR.
1912                 context.setDocBase(newDocBase);
1913
1914                 // Remove this listener from the Context else it will run every
1915                 // time the Context is stopped.
1916                 context.removeLifecycleListener(this);
1917             }
1918         }
1919     }
1920 }
1921