| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.catalina.startup; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.nio.file.Files; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.SortedSet; |
| import java.util.TreeSet; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.ExecutorService; |
| import java.util.concurrent.Future; |
| import java.util.jar.JarEntry; |
| import java.util.jar.JarFile; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import javax.management.ObjectName; |
| |
| import org.apache.catalina.Container; |
| import org.apache.catalina.Context; |
| import org.apache.catalina.DistributedManager; |
| import org.apache.catalina.Host; |
| import org.apache.catalina.Lifecycle; |
| import org.apache.catalina.LifecycleEvent; |
| import org.apache.catalina.LifecycleListener; |
| import org.apache.catalina.Manager; |
| import org.apache.catalina.core.StandardContext; |
| import org.apache.catalina.core.StandardHost; |
| import org.apache.catalina.util.ContextName; |
| import org.apache.catalina.util.IOTools; |
| import org.apache.juli.logging.Log; |
| import org.apache.juli.logging.LogFactory; |
| import org.apache.tomcat.jakartaee.Migration; |
| import org.apache.tomcat.util.ExceptionUtils; |
| import org.apache.tomcat.util.buf.UriUtil; |
| import org.apache.tomcat.util.digester.Digester; |
| import org.apache.tomcat.util.modeler.Registry; |
| import org.apache.tomcat.util.res.StringManager; |
| |
| /** |
| * Startup event listener for a <b>Host</b> that configures the properties of that Host, and the associated defined |
| * contexts. |
| * @author Remy Maucherat |
| */ |
| public class HostConfig implements LifecycleListener { |
| |
| private static final Log log = LogFactory.getLog(HostConfig.class); |
| |
| /** |
| * The string resources for this package. |
| */ |
| protected static final StringManager sm = StringManager.getManager(HostConfig.class); |
| |
| /** |
| * The resolution, in milliseconds, of file modification times. |
| */ |
| protected static final long FILE_MODIFICATION_RESOLUTION_MS = 1000; |
| |
| |
| // ----------------------------------------------------- Instance Variables |
| |
| /** |
| * The Java class name of the Context implementation we should use. |
| */ |
| protected String contextClass = "org.apache.catalina.core.StandardContext"; |
| |
| |
| /** |
| * The Host we are associated with. |
| */ |
| protected Host host = null; |
| |
| |
| /** |
| * The JMX ObjectName of this component. |
| */ |
| protected ObjectName oname = null; |
| |
| |
| /** |
| * Should we deploy XML Context config files packaged with WAR files and directories? |
| */ |
| protected boolean deployXML = false; |
| |
| |
| /** |
| * Should XML files be copied to $CATALINA_BASE/conf/<engine>/<host> by default when a web application |
| * is deployed? |
| */ |
| protected boolean copyXML = false; |
| |
| |
| /** |
| * Should we unpack WAR files when auto-deploying applications in the <code>appBase</code> directory? |
| */ |
| protected boolean unpackWARs = false; |
| |
| |
| /** |
| * Map of deployed applications. |
| */ |
| protected final Map<String,DeployedApplication> deployed = new ConcurrentHashMap<>(); |
| |
| |
| /** |
| * Set of applications which are being serviced, and shouldn't be deployed/undeployed/redeployed at the moment. |
| */ |
| private final Set<String> servicedSet = ConcurrentHashMap.newKeySet(); |
| |
| /** |
| * The <code>Digester</code> instance used to parse context descriptors. |
| */ |
| protected Digester digester = createDigester(contextClass); |
| private final Object digesterLock = new Object(); |
| |
| /** |
| * The list of Wars in the appBase to be ignored because they are invalid (e.g. contain /../ sequences). |
| */ |
| protected final Set<String> invalidWars = new HashSet<>(); |
| |
| // ------------------------------------------------------------- Properties |
| |
| |
| /** |
| * @return the Context implementation class name. |
| */ |
| public String getContextClass() { |
| return this.contextClass; |
| } |
| |
| |
| /** |
| * Set the Context implementation class name. |
| * |
| * @param contextClass The new Context implementation class name. |
| */ |
| public void setContextClass(String contextClass) { |
| |
| String oldContextClass = this.contextClass; |
| this.contextClass = contextClass; |
| |
| if (!oldContextClass.equals(contextClass)) { |
| synchronized (digesterLock) { |
| digester = createDigester(getContextClass()); |
| } |
| } |
| } |
| |
| |
| /** |
| * @return the deploy XML config file flag for this component. |
| */ |
| public boolean isDeployXML() { |
| return this.deployXML; |
| } |
| |
| |
| /** |
| * Set the deploy XML config file flag for this component. |
| * |
| * @param deployXML The new deploy XML flag |
| */ |
| public void setDeployXML(boolean deployXML) { |
| this.deployXML = deployXML; |
| } |
| |
| |
| /** |
| * @return the copy XML config file flag for this component. |
| */ |
| public boolean isCopyXML() { |
| return this.copyXML; |
| } |
| |
| |
| /** |
| * Set the copy XML config file flag for this component. |
| * |
| * @param copyXML The new copy XML flag |
| */ |
| public void setCopyXML(boolean copyXML) { |
| |
| this.copyXML = copyXML; |
| |
| } |
| |
| |
| /** |
| * @return the unpack WARs flag. |
| */ |
| public boolean isUnpackWARs() { |
| return this.unpackWARs; |
| } |
| |
| |
| /** |
| * Set the unpack WARs flag. |
| * |
| * @param unpackWARs The new unpack WARs flag |
| */ |
| public void setUnpackWARs(boolean unpackWARs) { |
| this.unpackWARs = unpackWARs; |
| } |
| |
| |
| // --------------------------------------------------------- Public Methods |
| |
| |
| /** |
| * Process the START event for an associated Host. |
| * |
| * @param event The lifecycle event that has occurred |
| */ |
| @Override |
| public void lifecycleEvent(LifecycleEvent event) { |
| |
| // Identify the host we are associated with |
| try { |
| host = (Host) event.getLifecycle(); |
| if (host instanceof StandardHost) { |
| setCopyXML(((StandardHost) host).isCopyXML()); |
| setDeployXML(((StandardHost) host).isDeployXML()); |
| setUnpackWARs(((StandardHost) host).isUnpackWARs()); |
| setContextClass(((StandardHost) host).getContextClass()); |
| } |
| } catch (ClassCastException e) { |
| log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e); |
| return; |
| } |
| |
| // Process the event that has occurred |
| switch (event.getType()) { |
| case Lifecycle.PERIODIC_EVENT -> check(); |
| case Lifecycle.BEFORE_START_EVENT -> beforeStart(); |
| case Lifecycle.START_EVENT -> start(); |
| case Lifecycle.STOP_EVENT -> stop(); |
| } |
| } |
| |
| |
| /** |
| * Add a serviced application to the list and indicates if the application was already present in the list. |
| * |
| * @param name the context name |
| * |
| * @return {@code true} if the application was not already in the list |
| */ |
| public boolean tryAddServiced(String name) { |
| return servicedSet.add(name); |
| } |
| |
| |
| /** |
| * Removed a serviced application from the list. |
| * |
| * @param name the context name |
| */ |
| public void removeServiced(String name) { |
| servicedSet.remove(name); |
| } |
| |
| |
| /** |
| * Get the instant where an application was deployed. |
| * |
| * @param name the context name |
| * |
| * @return 0L if no application with that name is deployed, or the instant on which the application was deployed |
| */ |
| public long getDeploymentTime(String name) { |
| synchronized (host) { |
| DeployedApplication app = deployed.get(name); |
| if (app == null) { |
| return 0L; |
| } |
| |
| return app.timestamp; |
| } |
| } |
| |
| |
| /** |
| * Has the specified application been deployed? Note applications defined in server.xml will not have been deployed. |
| * |
| * @param name the context name |
| * |
| * @return <code>true</code> if the application has been deployed and <code>false</code> if the application has not |
| * been deployed or does not exist |
| */ |
| public boolean isDeployed(String name) { |
| return deployed.containsKey(name); |
| } |
| |
| |
| // ------------------------------------------------------ Protected Methods |
| |
| |
| /** |
| * Create the digester which will be used to parse context config files. |
| * |
| * @param contextClassName The class which will be used to create the context instance |
| * |
| * @return the digester |
| */ |
| protected static Digester createDigester(String contextClassName) { |
| Digester digester = new Digester(); |
| digester.setValidating(false); |
| // Add object creation rule |
| digester.addObjectCreate("Context", contextClassName, "className"); |
| // Set the properties on that object (it doesn't matter if extra |
| // properties are set) |
| digester.addSetProperties("Context"); |
| return digester; |
| } |
| |
| protected File returnCanonicalPath(String path) { |
| File file = new File(path); |
| if (!file.isAbsolute()) { |
| file = new File(host.getCatalinaBase(), path); |
| } |
| try { |
| return file.getCanonicalFile(); |
| } catch (IOException ioe) { |
| return file; |
| } |
| } |
| |
| |
| /** |
| * Get the name of the configBase. For use with JMX management. |
| * |
| * @return the config base |
| */ |
| public String getConfigBaseName() { |
| return host.getConfigBaseFile().getAbsolutePath(); |
| } |
| |
| |
| /** |
| * Deploy applications for any directories or WAR files that are found in our "application root" directory. |
| */ |
| protected void deployApps() { |
| // Migrate legacy Java EE apps from legacyAppBase |
| migrateLegacyApps(); |
| File appBase = host.getAppBaseFile(); |
| File configBase = host.getConfigBaseFile(); |
| String[] filteredAppPaths = filterAppPaths(appBase.list()); |
| // Deploy XML descriptors from configBase |
| deployDescriptors(configBase, configBase.list()); |
| // Deploy WARs |
| deployWARs(appBase, filteredAppPaths); |
| // Deploy expanded folders |
| deployDirectories(appBase, filteredAppPaths); |
| } |
| |
| |
| /** |
| * Filter the list of application file paths to remove those that match the regular expression defined by |
| * {@link Host#getDeployIgnore()}. |
| * |
| * @param unfilteredAppPaths The list of application paths to filter |
| * |
| * @return The filtered list of application paths |
| */ |
| protected String[] filterAppPaths(String[] unfilteredAppPaths) { |
| Pattern filter = host.getDeployIgnorePattern(); |
| if (filter == null || unfilteredAppPaths == null) { |
| return unfilteredAppPaths; |
| } |
| |
| List<String> filteredList = new ArrayList<>(); |
| Matcher matcher = null; |
| for (String appPath : unfilteredAppPaths) { |
| if (matcher == null) { |
| matcher = filter.matcher(appPath); |
| } else { |
| matcher.reset(appPath); |
| } |
| if (matcher.matches()) { |
| if (log.isDebugEnabled()) { |
| log.debug(sm.getString("hostConfig.ignorePath", appPath)); |
| } |
| } else { |
| filteredList.add(appPath); |
| } |
| } |
| return filteredList.toArray(new String[0]); |
| } |
| |
| |
| /** |
| * Deploy applications for any directories or WAR files that are found in our "application root" directory. |
| * <p> |
| * Note: It is expected that the caller has successfully added the app to servicedSet before calling this method. |
| * |
| * @param name The context name which should be deployed |
| */ |
| protected void deployApps(String name) { |
| |
| File appBase = host.getAppBaseFile(); |
| File configBase = host.getConfigBaseFile(); |
| ContextName cn = new ContextName(name, false); |
| String baseName = cn.getBaseName(); |
| |
| if (deploymentExists(cn.getName())) { |
| return; |
| } |
| |
| // Deploy XML descriptor from configBase |
| File xml = new File(configBase, baseName + ".xml"); |
| if (xml.exists()) { |
| deployDescriptor(cn, xml); |
| return; |
| } |
| // Deploy WAR |
| File war = new File(appBase, baseName + ".war"); |
| if (war.exists()) { |
| deployWAR(cn, war); |
| return; |
| } |
| // Deploy expanded folder |
| File dir = new File(appBase, baseName); |
| if (dir.exists()) { |
| deployDirectory(cn, dir); |
| } |
| } |
| |
| |
| /** |
| * Deploy XML context descriptors. |
| * |
| * @param configBase The config base |
| * @param files The XML descriptors which should be deployed |
| */ |
| protected void deployDescriptors(File configBase, String[] files) { |
| |
| if (files == null) { |
| return; |
| } |
| |
| ExecutorService es = host.getStartStopExecutor(); |
| List<Future<?>> results = new ArrayList<>(); |
| |
| for (String file : files) { |
| File contextXml = new File(configBase, file); |
| |
| if (file.toLowerCase(Locale.ENGLISH).endsWith(".xml")) { |
| ContextName cn = new ContextName(file, true); |
| |
| if (tryAddServiced(cn.getName())) { |
| try { |
| if (deploymentExists(cn.getName())) { |
| removeServiced(cn.getName()); |
| continue; |
| } |
| |
| // DeployDescriptor will call removeServiced |
| results.add(es.submit(new DeployDescriptor(this, cn, contextXml))); |
| } catch (Throwable t) { |
| ExceptionUtils.handleThrowable(t); |
| removeServiced(cn.getName()); |
| throw t; |
| } |
| } |
| } |
| } |
| |
| for (Future<?> result : results) { |
| try { |
| result.get(); |
| } catch (Exception e) { |
| log.error(sm.getString("hostConfig.deployDescriptor.threaded.error"), e); |
| } |
| } |
| } |
| |
| |
| /** |
| * Deploy specified context descriptor. |
| * <p> |
| * Note: It is expected that the caller has successfully added the app to servicedSet before calling this method. |
| * |
| * @param cn The context name |
| * @param contextXml The descriptor |
| */ |
| @SuppressWarnings("null") // context is not null |
| protected void deployDescriptor(ContextName cn, File contextXml) { |
| |
| DeployedApplication deployedApp = new DeployedApplication(cn.getName(), true); |
| |
| long startTime = 0; |
| // Assume this is a configuration descriptor and deploy it |
| if (log.isInfoEnabled()) { |
| startTime = System.currentTimeMillis(); |
| log.info(sm.getString("hostConfig.deployDescriptor", contextXml.getAbsolutePath())); |
| } |
| |
| Context context = null; |
| boolean isExternalWar = false; |
| boolean isExternal = false; |
| File expandedDocBase; |
| |
| try { |
| synchronized (digesterLock) { |
| try (FileInputStream fis = new FileInputStream(contextXml)) { |
| context = (Context) digester.parse(fis); |
| } catch (Exception e) { |
| log.error(sm.getString("hostConfig.deployDescriptor.error", contextXml.getAbsolutePath()), e); |
| } finally { |
| digester.reset(); |
| if (context == null) { |
| context = new FailedContext(); |
| } |
| } |
| } |
| |
| if (context.getPath() != null) { |
| log.warn(sm.getString("hostConfig.deployDescriptor.path", context.getPath(), |
| contextXml.getAbsolutePath())); |
| } |
| |
| Class<?> clazz = Class.forName(host.getConfigClass()); |
| LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance(); |
| context.addLifecycleListener(listener); |
| |
| context.setConfigFile(contextXml.toURI().toURL()); |
| context.setName(cn.getName()); |
| context.setPath(cn.getPath()); |
| context.setWebappVersion(cn.getVersion()); |
| // Add the associated docBase to the redeployed list if it's a WAR |
| if (context.getDocBase() != null) { |
| File docBase = new File(context.getDocBase()); |
| if (!docBase.isAbsolute()) { |
| docBase = new File(host.getAppBaseFile(), context.getDocBase()); |
| } |
| // If external docBase, register .xml as redeploy first |
| if (!docBase.getCanonicalFile().toPath().startsWith(host.getAppBaseFile().toPath())) { |
| isExternal = true; |
| deployedApp.redeployResources.put(contextXml.getAbsolutePath(), |
| Long.valueOf(contextXml.lastModified())); |
| deployedApp.redeployResources.put(docBase.getAbsolutePath(), Long.valueOf(docBase.lastModified())); |
| if (docBase.getAbsolutePath().toLowerCase(Locale.ENGLISH).endsWith(".war")) { |
| isExternalWar = true; |
| } |
| // Check that a WAR or DIR in the appBase is not 'hidden' |
| File war = new File(host.getAppBaseFile(), cn.getBaseName() + ".war"); |
| if (war.exists()) { |
| log.warn(sm.getString("hostConfig.deployDescriptor.hiddenWar", contextXml.getAbsolutePath(), |
| war.getAbsolutePath())); |
| } |
| File dir = new File(host.getAppBaseFile(), cn.getBaseName()); |
| if (dir.exists()) { |
| log.warn(sm.getString("hostConfig.deployDescriptor.hiddenDir", contextXml.getAbsolutePath(), |
| dir.getAbsolutePath())); |
| } |
| } else { |
| log.warn(sm.getString("hostConfig.deployDescriptor.localDocBaseSpecified", docBase)); |
| // Ignore specified docBase |
| context.setDocBase(null); |
| } |
| } |
| |
| host.addChild(context); |
| } catch (Throwable t) { |
| ExceptionUtils.handleThrowable(t); |
| log.error(sm.getString("hostConfig.deployDescriptor.error", contextXml.getAbsolutePath()), t); |
| } finally { |
| // Get paths for WAR and expanded WAR in appBase |
| |
| // default to appBase dir + name |
| expandedDocBase = new File(host.getAppBaseFile(), cn.getBaseName()); |
| if (context != null && context.getDocBase() != null && |
| !context.getDocBase().toLowerCase(Locale.ENGLISH).endsWith(".war")) { |
| // first assume docBase is absolute |
| expandedDocBase = new File(context.getDocBase()); |
| if (!expandedDocBase.isAbsolute()) { |
| // if docBase specified and relative, it must be relative to appBase |
| expandedDocBase = new File(host.getAppBaseFile(), context.getDocBase()); |
| } |
| } |
| |
| boolean unpackWAR = unpackWARs; |
| if (unpackWAR && context instanceof StandardContext) { |
| unpackWAR = ((StandardContext) context).getUnpackWAR(); |
| } |
| |
| // Add the eventual unpacked WAR and all the resources which will be |
| // watched inside it |
| if (isExternalWar) { |
| if (unpackWAR) { |
| deployedApp.redeployResources.put(expandedDocBase.getAbsolutePath(), |
| Long.valueOf(expandedDocBase.lastModified())); |
| addWatchedResources(deployedApp, expandedDocBase.getAbsolutePath(), context); |
| } else { |
| addWatchedResources(deployedApp, null, context); |
| } |
| } else { |
| // Find an existing matching war and expanded folder |
| if (!isExternal) { |
| File warDocBase = new File(expandedDocBase.getAbsolutePath() + ".war"); |
| if (warDocBase.exists()) { |
| deployedApp.redeployResources.put(warDocBase.getAbsolutePath(), |
| Long.valueOf(warDocBase.lastModified())); |
| } else { |
| // Trigger a redeploy if a WAR is added |
| deployedApp.redeployResources.put(warDocBase.getAbsolutePath(), Long.valueOf(0)); |
| } |
| } |
| if (unpackWAR) { |
| deployedApp.redeployResources.put(expandedDocBase.getAbsolutePath(), |
| Long.valueOf(expandedDocBase.lastModified())); |
| addWatchedResources(deployedApp, expandedDocBase.getAbsolutePath(), context); |
| } else { |
| addWatchedResources(deployedApp, null, context); |
| } |
| if (!isExternal) { |
| // For external docBases, the context.xml will have been |
| // added above. |
| deployedApp.redeployResources.put(contextXml.getAbsolutePath(), |
| Long.valueOf(contextXml.lastModified())); |
| } |
| } |
| // Add the global redeploy resources (which are never deleted) at |
| // the end so they don't interfere with the deletion process |
| addGlobalRedeployResources(deployedApp); |
| } |
| |
| if (host.findChild(context.getName()) != null) { |
| deployed.put(context.getName(), deployedApp); |
| } |
| |
| if (log.isInfoEnabled()) { |
| log.info(sm.getString("hostConfig.deployDescriptor.finished", contextXml.getAbsolutePath(), |
| Long.valueOf(System.currentTimeMillis() - startTime))); |
| } |
| } |
| |
| |
| /** |
| * Deploy WAR files. |
| * |
| * @param appBase The base path for applications |
| * @param files The WARs to deploy |
| */ |
| protected void deployWARs(File appBase, String[] files) { |
| |
| if (files == null) { |
| return; |
| } |
| |
| ExecutorService es = host.getStartStopExecutor(); |
| List<Future<?>> results = new ArrayList<>(); |
| |
| for (String file : files) { |
| if (file.equalsIgnoreCase("META-INF")) { |
| continue; |
| } |
| if (file.equalsIgnoreCase("WEB-INF")) { |
| continue; |
| } |
| |
| File war = new File(appBase, file); |
| if (file.toLowerCase(Locale.ENGLISH).endsWith(".war") && war.isFile() && !invalidWars.contains(file)) { |
| ContextName cn = new ContextName(file, true); |
| if (tryAddServiced(cn.getName())) { |
| try { |
| if (deploymentExists(cn.getName())) { |
| DeployedApplication app = deployed.get(cn.getName()); |
| boolean unpackWAR = unpackWARs; |
| if (unpackWAR && host.findChild(cn.getName()) instanceof StandardContext) { |
| unpackWAR = ((StandardContext) host.findChild(cn.getName())).getUnpackWAR(); |
| } |
| if (!unpackWAR && app != null) { |
| // Need to check for a directory that should not be |
| // there |
| File dir = new File(appBase, cn.getBaseName()); |
| if (dir.exists()) { |
| if (!app.loggedDirWarning) { |
| log.warn(sm.getString("hostConfig.deployWar.hiddenDir", dir.getAbsoluteFile(), |
| war.getAbsoluteFile())); |
| app.loggedDirWarning = true; |
| } |
| } else { |
| app.loggedDirWarning = false; |
| } |
| } |
| removeServiced(cn.getName()); |
| continue; |
| } |
| |
| // Check for WARs with /../ /./ or similar sequences in the name |
| if (!validateContextPath(appBase, cn.getBaseName())) { |
| log.error(sm.getString("hostConfig.illegalWarName", file)); |
| invalidWars.add(file); |
| removeServiced(cn.getName()); |
| continue; |
| } |
| |
| // DeployWAR will call removeServiced |
| results.add(es.submit(new DeployWar(this, cn, war))); |
| } catch (Throwable t) { |
| ExceptionUtils.handleThrowable(t); |
| removeServiced(cn.getName()); |
| throw t; |
| } |
| } |
| } |
| } |
| |
| for (Future<?> result : results) { |
| try { |
| result.get(); |
| } catch (Exception e) { |
| log.error(sm.getString("hostConfig.deployWar.threaded.error"), e); |
| } |
| } |
| } |
| |
| |
| private boolean validateContextPath(File appBase, String contextPath) { |
| // More complicated than the ideal as the canonical path may or may |
| // not end with File.separator for a directory |
| |
| StringBuilder docBase; |
| String canonicalDocBase; |
| |
| try { |
| String canonicalAppBase = appBase.getCanonicalPath(); |
| docBase = new StringBuilder(canonicalAppBase); |
| if (canonicalAppBase.endsWith(File.separator)) { |
| docBase.append(contextPath.substring(1).replace('/', File.separatorChar)); |
| } else { |
| docBase.append(contextPath.replace('/', File.separatorChar)); |
| } |
| // At this point docBase should be canonical but will not end |
| // with File.separator |
| |
| canonicalDocBase = (new File(docBase.toString())).getCanonicalPath(); |
| |
| // If the canonicalDocBase ends with File.separator, add one to |
| // docBase before they are compared |
| if (canonicalDocBase.endsWith(File.separator)) { |
| docBase.append(File.separator); |
| } |
| } catch (IOException ioe) { |
| return false; |
| } |
| |
| // Compare the two. If they are not the same, the contextPath must |
| // have /../ like sequences in it |
| return canonicalDocBase.contentEquals(docBase); |
| } |
| |
| /** |
| * Deploy packed WAR. |
| * <p> |
| * Note: It is expected that the caller has successfully added the app to servicedSet before calling this method. |
| * |
| * @param cn The context name |
| * @param war The WAR file |
| */ |
| protected void deployWAR(ContextName cn, File war) { |
| |
| File xml = new File(host.getAppBaseFile(), cn.getBaseName() + "/" + Constants.ApplicationContextXml); |
| |
| File warTracker = new File(host.getAppBaseFile(), cn.getBaseName() + Constants.WarTracker); |
| |
| boolean xmlInWar = false; |
| try (JarFile jar = new JarFile(war)) { |
| JarEntry entry = jar.getJarEntry(Constants.ApplicationContextXml); |
| if (entry != null) { |
| xmlInWar = true; |
| } |
| } catch (IOException ignore) { |
| // Ignore |
| } |
| |
| // If there is an expanded directory then any xml in that directory |
| // should only be used if the directory is not out of date and |
| // unpackWARs is true. Note the code below may apply further limits |
| boolean useXml = |
| xml.exists() && unpackWARs && (!warTracker.exists() || warTracker.lastModified() == war.lastModified()); |
| // If the xml file exists then expandedDir must exist so no need to |
| // test that here |
| |
| Context context = null; |
| boolean deployThisXML = this.deployXML; |
| |
| try { |
| if (deployThisXML && useXml && !copyXML) { |
| synchronized (digesterLock) { |
| try { |
| context = (Context) digester.parse(xml); |
| } catch (Exception e) { |
| log.error(sm.getString("hostConfig.deployDescriptor.error", war.getAbsolutePath()), e); |
| } finally { |
| digester.reset(); |
| if (context == null) { |
| context = new FailedContext(); |
| } |
| } |
| } |
| context.setConfigFile(xml.toURI().toURL()); |
| } else if (deployThisXML && xmlInWar) { |
| synchronized (digesterLock) { |
| try (JarFile jar = new JarFile(war)) { |
| JarEntry entry = jar.getJarEntry(Constants.ApplicationContextXml); |
| try (InputStream istream = jar.getInputStream(entry)) { |
| context = (Context) digester.parse(istream); |
| } |
| } catch (Exception e) { |
| log.error(sm.getString("hostConfig.deployDescriptor.error", war.getAbsolutePath()), e); |
| } finally { |
| digester.reset(); |
| if (context == null) { |
| context = new FailedContext(); |
| } |
| context.setConfigFile(UriUtil.buildJarUrl(war, Constants.ApplicationContextXml)); |
| } |
| } |
| } else if (!deployThisXML && xmlInWar) { |
| // Block deployment as META-INF/context.xml may contain security |
| // configuration necessary for a secure deployment. |
| log.error(sm.getString("hostConfig.deployDescriptor.blocked", cn.getPath(), |
| Constants.ApplicationContextXml, |
| new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml"))); |
| } else { |
| context = (Context) Class.forName(contextClass).getConstructor().newInstance(); |
| } |
| } catch (Throwable t) { |
| ExceptionUtils.handleThrowable(t); |
| log.error(sm.getString("hostConfig.deployWar.error", war.getAbsolutePath()), t); |
| } finally { |
| if (context == null) { |
| context = new FailedContext(); |
| } |
| } |
| |
| boolean copyThisXml = false; |
| if (deployThisXML) { |
| if (host instanceof StandardHost) { |
| copyThisXml = ((StandardHost) host).isCopyXML(); |
| } |
| |
| // If Host is using default value Context can override it. |
| if (!copyThisXml && context instanceof StandardContext) { |
| copyThisXml = ((StandardContext) context).getCopyXML(); |
| } |
| |
| if (xmlInWar && copyThisXml) { |
| // Change location of XML file to config base |
| xml = new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml"); |
| try (JarFile jar = new JarFile(war)) { |
| JarEntry entry = jar.getJarEntry(Constants.ApplicationContextXml); |
| try (InputStream istream = jar.getInputStream(entry); |
| OutputStream ostream = new FileOutputStream(xml)) { |
| IOTools.flow(istream, ostream); |
| } |
| } catch (IOException ignore) { |
| // Ignore |
| } |
| } |
| } |
| |
| DeployedApplication deployedApp = |
| new DeployedApplication(cn.getName(), xml.exists() && deployThisXML && copyThisXml); |
| |
| long startTime = 0; |
| // Deploy the application in this WAR file |
| if (log.isInfoEnabled()) { |
| startTime = System.currentTimeMillis(); |
| log.info(sm.getString("hostConfig.deployWar", war.getAbsolutePath())); |
| } |
| |
| try { |
| // Populate redeploy resources with the WAR file |
| deployedApp.redeployResources.put(war.getAbsolutePath(), Long.valueOf(war.lastModified())); |
| |
| if (deployThisXML && xml.exists() && copyThisXml) { |
| deployedApp.redeployResources.put(xml.getAbsolutePath(), Long.valueOf(xml.lastModified())); |
| } else { |
| // In case an XML file is added to the config base later |
| deployedApp.redeployResources.put( |
| (new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml")).getAbsolutePath(), |
| Long.valueOf(0)); |
| } |
| |
| Class<?> clazz = Class.forName(host.getConfigClass()); |
| LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance(); |
| context.addLifecycleListener(listener); |
| |
| context.setName(cn.getName()); |
| context.setPath(cn.getPath()); |
| context.setWebappVersion(cn.getVersion()); |
| context.setDocBase(cn.getBaseName() + ".war"); |
| host.addChild(context); |
| } catch (Throwable t) { |
| ExceptionUtils.handleThrowable(t); |
| log.error(sm.getString("hostConfig.deployWar.error", war.getAbsolutePath()), t); |
| } finally { |
| // If we're unpacking WARs, the docBase will be mutated after |
| // starting the context |
| boolean unpackWAR = unpackWARs; |
| if (unpackWAR && context instanceof StandardContext) { |
| unpackWAR = ((StandardContext) context).getUnpackWAR(); |
| } |
| if (unpackWAR && context.getDocBase() != null) { |
| File docBase = new File(host.getAppBaseFile(), cn.getBaseName()); |
| deployedApp.redeployResources.put(docBase.getAbsolutePath(), Long.valueOf(docBase.lastModified())); |
| addWatchedResources(deployedApp, docBase.getAbsolutePath(), context); |
| if (deployThisXML && !copyThisXml && (xmlInWar || xml.exists())) { |
| deployedApp.redeployResources.put(xml.getAbsolutePath(), Long.valueOf(xml.lastModified())); |
| } |
| } else { |
| // Passing null for docBase means that no resources will be |
| // watched. This will be logged at debug level. |
| addWatchedResources(deployedApp, null, context); |
| } |
| // Add the global redeploy resources (which are never deleted) at |
| // the end so they don't interfere with the deletion process |
| addGlobalRedeployResources(deployedApp); |
| } |
| |
| deployed.put(cn.getName(), deployedApp); |
| |
| if (log.isInfoEnabled()) { |
| log.info(sm.getString("hostConfig.deployWar.finished", war.getAbsolutePath(), |
| Long.valueOf(System.currentTimeMillis() - startTime))); |
| } |
| } |
| |
| |
| /** |
| * Deploy exploded webapps. |
| * |
| * @param appBase The base path for applications |
| * @param files The exploded webapps that should be deployed |
| */ |
| protected void deployDirectories(File appBase, String[] files) { |
| |
| if (files == null) { |
| return; |
| } |
| |
| ExecutorService es = host.getStartStopExecutor(); |
| List<Future<?>> results = new ArrayList<>(); |
| |
| for (String file : files) { |
| if (file.equalsIgnoreCase("META-INF")) { |
| continue; |
| } |
| if (file.equalsIgnoreCase("WEB-INF")) { |
| continue; |
| } |
| |
| File dir = new File(appBase, file); |
| if (dir.isDirectory()) { |
| ContextName cn = new ContextName(file, false); |
| |
| if (tryAddServiced(cn.getName())) { |
| try { |
| if (deploymentExists(cn.getName())) { |
| removeServiced(cn.getName()); |
| continue; |
| } |
| |
| // DeployDirectory will call removeServiced |
| results.add(es.submit(new DeployDirectory(this, cn, dir))); |
| } catch (Throwable t) { |
| ExceptionUtils.handleThrowable(t); |
| removeServiced(cn.getName()); |
| throw t; |
| } |
| } |
| } |
| } |
| |
| for (Future<?> result : results) { |
| try { |
| result.get(); |
| } catch (Exception e) { |
| log.error(sm.getString("hostConfig.deployDir.threaded.error"), e); |
| } |
| } |
| } |
| |
| |
| /** |
| * Deploy exploded webapp. |
| * <p> |
| * Note: It is expected that the caller has successfully added the app to servicedSet before calling this method. |
| * |
| * @param cn The context name |
| * @param dir The path to the root folder of the webapp |
| */ |
| protected void deployDirectory(ContextName cn, File dir) { |
| |
| long startTime = 0; |
| // Deploy the application in this directory |
| if (log.isInfoEnabled()) { |
| startTime = System.currentTimeMillis(); |
| log.info(sm.getString("hostConfig.deployDir", dir.getAbsolutePath())); |
| } |
| |
| Context context = null; |
| File xml = new File(dir, Constants.ApplicationContextXml); |
| File xmlCopy = new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml"); |
| |
| DeployedApplication deployedApp; |
| boolean copyThisXml = isCopyXML(); |
| boolean deployThisXML = this.deployXML; |
| |
| try { |
| if (deployThisXML && xml.exists()) { |
| synchronized (digesterLock) { |
| try { |
| context = (Context) digester.parse(xml); |
| } catch (Exception e) { |
| log.error(sm.getString("hostConfig.deployDescriptor.error", xml), e); |
| context = new FailedContext(); |
| } finally { |
| digester.reset(); |
| if (context == null) { |
| context = new FailedContext(); |
| } |
| } |
| } |
| |
| if (!copyThisXml && context instanceof StandardContext) { |
| // Host is using default value. Context may override it. |
| copyThisXml = ((StandardContext) context).getCopyXML(); |
| } |
| |
| if (copyThisXml) { |
| Files.copy(xml.toPath(), xmlCopy.toPath()); |
| context.setConfigFile(xmlCopy.toURI().toURL()); |
| } else { |
| context.setConfigFile(xml.toURI().toURL()); |
| } |
| } else if (!deployThisXML && xml.exists()) { |
| // Block deployment as META-INF/context.xml may contain security |
| // configuration necessary for a secure deployment. |
| log.error(sm.getString("hostConfig.deployDescriptor.blocked", cn.getPath(), xml, xmlCopy)); |
| context = new FailedContext(); |
| } else { |
| context = (Context) Class.forName(contextClass).getConstructor().newInstance(); |
| } |
| |
| Class<?> clazz = Class.forName(host.getConfigClass()); |
| LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance(); |
| context.addLifecycleListener(listener); |
| |
| context.setName(cn.getName()); |
| context.setPath(cn.getPath()); |
| context.setWebappVersion(cn.getVersion()); |
| context.setDocBase(cn.getBaseName()); |
| host.addChild(context); |
| } catch (Throwable t) { |
| ExceptionUtils.handleThrowable(t); |
| log.error(sm.getString("hostConfig.deployDir.error", dir.getAbsolutePath()), t); |
| } finally { |
| deployedApp = new DeployedApplication(cn.getName(), xml.exists() && deployThisXML && copyThisXml); |
| |
| // Fake re-deploy resource to detect if a WAR is added at a later |
| // point |
| deployedApp.redeployResources.put(dir.getAbsolutePath() + ".war", Long.valueOf(0)); |
| deployedApp.redeployResources.put(dir.getAbsolutePath(), Long.valueOf(dir.lastModified())); |
| if (deployThisXML && xml.exists()) { |
| if (copyThisXml) { |
| deployedApp.redeployResources.put(xmlCopy.getAbsolutePath(), Long.valueOf(xmlCopy.lastModified())); |
| } else { |
| deployedApp.redeployResources.put(xml.getAbsolutePath(), Long.valueOf(xml.lastModified())); |
| // Fake re-deploy resource to detect if a context.xml file is |
| // added at a later point |
| deployedApp.redeployResources.put(xmlCopy.getAbsolutePath(), Long.valueOf(0)); |
| } |
| } else { |
| // Fake re-deploy resource to detect if a context.xml file is |
| // added at a later point |
| deployedApp.redeployResources.put(xmlCopy.getAbsolutePath(), Long.valueOf(0)); |
| if (!xml.exists()) { |
| deployedApp.redeployResources.put(xml.getAbsolutePath(), Long.valueOf(0)); |
| } |
| } |
| addWatchedResources(deployedApp, dir.getAbsolutePath(), context); |
| // Add the global redeploy resources (which are never deleted) at |
| // the end so they don't interfere with the deletion process |
| addGlobalRedeployResources(deployedApp); |
| } |
| |
| deployed.put(cn.getName(), deployedApp); |
| |
| if (log.isInfoEnabled()) { |
| log.info(sm.getString("hostConfig.deployDir.finished", dir.getAbsolutePath(), |
| Long.valueOf(System.currentTimeMillis() - startTime))); |
| } |
| } |
| |
| |
| protected void migrateLegacyApps() { |
| File appBase = host.getAppBaseFile(); |
| File legacyAppBase = host.getLegacyAppBaseFile(); |
| if (!legacyAppBase.isDirectory()) { |
| return; |
| } |
| |
| ExecutorService es = host.getStartStopExecutor(); |
| List<Future<?>> results = new ArrayList<>(); |
| |
| // Should not be null as we test above if this is a directory |
| String[] migrationCandidates = legacyAppBase.list(); |
| if (migrationCandidates == null) { |
| return; |
| } |
| for (String migrationCandidate : migrationCandidates) { |
| File source = new File(legacyAppBase, migrationCandidate); |
| File destination = new File(appBase, migrationCandidate); |
| |
| ContextName cn; |
| if (source.lastModified() > destination.lastModified()) { |
| if (source.isFile() && source.getName().toLowerCase(Locale.ENGLISH).endsWith(".war")) { |
| cn = new ContextName(migrationCandidate, true); |
| } else if (source.isDirectory()) { |
| cn = new ContextName(migrationCandidate, false); |
| } else { |
| continue; |
| } |
| |
| if (tryAddServiced(cn.getBaseName())) { |
| try { |
| // MigrateApp will call removeServiced |
| results.add(es.submit(new MigrateApp(this, cn, source, destination))); |
| } catch (Throwable t) { |
| ExceptionUtils.handleThrowable(t); |
| removeServiced(cn.getName()); |
| throw t; |
| } |
| } |
| } |
| } |
| |
| for (Future<?> result : results) { |
| try { |
| result.get(); |
| } catch (Exception e) { |
| log.error(sm.getString("hostConfig.migrateApp.threaded.error"), e); |
| } |
| } |
| } |
| |
| |
| protected void migrateLegacyApp(File source, File destination) { |
| File tempNew = null; |
| File tempOld; |
| try { |
| tempNew = File.createTempFile("new", null, host.getLegacyAppBaseFile()); |
| tempOld = File.createTempFile("old", null, host.getLegacyAppBaseFile()); |
| // createTempFile is not directly compatible with directories, so cleanup |
| Files.delete(tempNew.toPath()); |
| Files.delete(tempOld.toPath()); |
| |
| // The use of defaults is deliberate here to avoid having to |
| // recreate every configuration option on the host. Better to change |
| // the defaults if necessary than to start adding configuration |
| // options. Users that need non-default options can convert manually |
| // via migration.[sh|bat] |
| Migration migration = new Migration(); |
| migration.setSource(source); |
| migration.setDestination(tempNew); |
| migration.execute(); |
| |
| // Use rename |
| if (destination.exists()) { |
| Files.move(destination.toPath(), tempOld.toPath()); |
| } |
| Files.move(tempNew.toPath(), destination.toPath()); |
| ExpandWar.delete(tempOld); |
| |
| } catch (Throwable t) { |
| ExceptionUtils.handleThrowable(t); |
| log.warn(sm.getString("hostConfig.migrateError"), t); |
| } finally { |
| if (tempNew != null && tempNew.exists()) { |
| ExpandWar.delete(tempNew); |
| } |
| } |
| } |
| |
| |
| /** |
| * Check if a webapp is already deployed in this host. |
| * |
| * @param contextName of the context which will be checked |
| * |
| * @return <code>true</code> if the specified deployment exists |
| */ |
| protected boolean deploymentExists(String contextName) { |
| return deployed.containsKey(contextName) || (host.findChild(contextName) != null); |
| } |
| |
| |
| /** |
| * Add watched resources to the specified Context. |
| * |
| * @param app HostConfig deployed app |
| * @param docBase web app docBase |
| * @param context web application context |
| */ |
| protected void addWatchedResources(DeployedApplication app, String docBase, Context context) { |
| // FIXME: Feature idea. Add support for patterns (ex: WEB-INF/*, |
| // WEB-INF/*.xml), where we would only check if at least one |
| // resource is newer than app.timestamp |
| File docBaseFile = null; |
| if (docBase != null) { |
| docBaseFile = new File(docBase); |
| if (!docBaseFile.isAbsolute()) { |
| docBaseFile = new File(host.getAppBaseFile(), docBase); |
| } |
| } |
| String[] watchedResources = context.findWatchedResources(); |
| for (String watchedResource : watchedResources) { |
| File resource = new File(watchedResource); |
| if (!resource.isAbsolute()) { |
| if (docBase != null) { |
| resource = new File(docBaseFile, watchedResource); |
| } else { |
| if (log.isTraceEnabled()) { |
| log.trace("Ignoring non-existent WatchedResource '" + resource.getAbsolutePath() + "'"); |
| } |
| continue; |
| } |
| } |
| if (log.isTraceEnabled()) { |
| log.trace("Watching WatchedResource '" + resource.getAbsolutePath() + "'"); |
| } |
| app.reloadResources.put(resource.getAbsolutePath(), Long.valueOf(resource.lastModified())); |
| } |
| } |
| |
| |
| protected void addGlobalRedeployResources(DeployedApplication app) { |
| // Redeploy resources processing is hard-coded to never delete this file |
| File hostContextXml = new File(getConfigBaseName(), Constants.HostContextXml); |
| if (hostContextXml.isFile()) { |
| app.redeployResources.put(hostContextXml.getAbsolutePath(), Long.valueOf(hostContextXml.lastModified())); |
| } |
| |
| // Redeploy resources in CATALINA_BASE/conf are never deleted |
| File globalContextXml = returnCanonicalPath(Constants.DefaultContextXml); |
| if (globalContextXml.isFile()) { |
| app.redeployResources.put(globalContextXml.getAbsolutePath(), |
| Long.valueOf(globalContextXml.lastModified())); |
| } |
| } |
| |
| |
| /** |
| * Check resources for redeployment and reloading. |
| * |
| * @param app The web application to check |
| * @param skipFileModificationResolutionCheck When checking files for modification should the check that requires |
| * that any file modification must have occurred at least as long ago |
| * as the resolution of the file time stamp be skipped |
| */ |
| protected void checkResources(DeployedApplication app, boolean skipFileModificationResolutionCheck) { |
| String[] resources = app.redeployResources.keySet().toArray(new String[0]); |
| // Offset the current time by the resolution of File.lastModified() |
| long currentTimeWithResolutionOffset = System.currentTimeMillis() - FILE_MODIFICATION_RESOLUTION_MS; |
| for (int i = 0; i < resources.length; i++) { |
| File resource = new File(resources[i]); |
| if (log.isTraceEnabled()) { |
| log.trace("Checking context[" + app.name + "] redeploy resource " + resource); |
| } |
| long lastModified = app.redeployResources.get(resources[i]).longValue(); |
| if (resource.exists() || lastModified == 0) { |
| // File.lastModified() has a resolution of 1s (1000ms). The last |
| // modified time has to be more than 1000ms ago to ensure that |
| // modifications that take place in the same second are not |
| // missed. See Bug 57765. |
| if (resource.lastModified() != lastModified && |
| (!host.getAutoDeploy() || resource.lastModified() < currentTimeWithResolutionOffset || |
| skipFileModificationResolutionCheck)) { |
| if (resource.isDirectory()) { |
| // No action required for modified directory |
| app.redeployResources.put(resources[i], Long.valueOf(resource.lastModified())); |
| } else if (app.hasDescriptor && resource.getName().toLowerCase(Locale.ENGLISH).endsWith(".war")) { |
| // Modified WAR triggers a reload if there is an XML |
| // file present |
| // The only resource that should be deleted is the |
| // expanded WAR (if any) |
| Context context = (Context) host.findChild(app.name); |
| String docBase = context.getDocBase(); |
| if (!docBase.toLowerCase(Locale.ENGLISH).endsWith(".war")) { |
| // This is an expanded directory |
| File docBaseFile = new File(docBase); |
| if (!docBaseFile.isAbsolute()) { |
| docBaseFile = new File(host.getAppBaseFile(), docBase); |
| } |
| reload(app, docBaseFile, resource.getAbsolutePath()); |
| } else { |
| reload(app, null, null); |
| } |
| // Update times |
| app.redeployResources.put(resources[i], Long.valueOf(resource.lastModified())); |
| app.timestamp = System.currentTimeMillis(); |
| boolean unpackWAR = unpackWARs; |
| if (unpackWAR && context instanceof StandardContext) { |
| unpackWAR = ((StandardContext) context).getUnpackWAR(); |
| } |
| if (unpackWAR) { |
| addWatchedResources(app, context.getDocBase(), context); |
| } else { |
| addWatchedResources(app, null, context); |
| } |
| return; |
| } else { |
| // Everything else triggers a redeploy |
| // (just need to undeploy here, deploy will follow) |
| undeploy(app); |
| deleteRedeployResources(app, resources, i, false); |
| return; |
| } |
| } |
| } else { |
| // There is a chance the resource was only missing |
| // temporarily e.g. renamed during a text editor save |
| if (resource.exists() || !resource.getName().toLowerCase(Locale.ENGLISH).endsWith(".war")) { |
| try { |
| Thread.sleep(500); |
| } catch (InterruptedException e1) { |
| // Ignore |
| } |
| } |
| // Recheck the resource to see if it was really deleted |
| if (resource.exists()) { |
| continue; |
| } |
| // Undeploy application |
| undeploy(app); |
| deleteRedeployResources(app, resources, i, true); |
| return; |
| } |
| } |
| resources = app.reloadResources.keySet().toArray(new String[0]); |
| boolean update = false; |
| for (String s : resources) { |
| File resource = new File(s); |
| if (log.isTraceEnabled()) { |
| log.trace("Checking context[" + app.name + "] reload resource " + resource); |
| } |
| long lastModified = app.reloadResources.get(s).longValue(); |
| // File.lastModified() has a resolution of 1s (1000ms). The last |
| // modified time has to be more than 1000ms ago to ensure that |
| // modifications that take place in the same second are not |
| // missed. See Bug 57765. |
| if ((resource.lastModified() != lastModified && |
| (!host.getAutoDeploy() || resource.lastModified() < currentTimeWithResolutionOffset || |
| skipFileModificationResolutionCheck)) || |
| update) { |
| if (!update) { |
| // Reload application |
| reload(app, null, null); |
| update = true; |
| } |
| // Update times. More than one file may have been updated. We |
| // don't want to trigger a series of reloads. |
| app.reloadResources.put(s, Long.valueOf(resource.lastModified())); |
| } |
| app.timestamp = System.currentTimeMillis(); |
| } |
| } |
| |
| |
| /* |
| * Note: If either of fileToRemove and newDocBase are null, both will be ignored. |
| */ |
| private void reload(DeployedApplication app, File fileToRemove, String newDocBase) { |
| if (log.isInfoEnabled()) { |
| log.info(sm.getString("hostConfig.reload", app.name)); |
| } |
| Context context = (Context) host.findChild(app.name); |
| if (context.getState().isAvailable()) { |
| if (fileToRemove != null && newDocBase != null) { |
| context.addLifecycleListener(new ExpandedDirectoryRemovalListener(fileToRemove, newDocBase)); |
| } |
| // Reload catches and logs exceptions |
| context.reload(); |
| } else { |
| // If the context was not started (for example an error |
| // in web.xml) we'll still get to try to start |
| if (fileToRemove != null && newDocBase != null) { |
| ExpandWar.delete(fileToRemove); |
| context.setDocBase(newDocBase); |
| } |
| try { |
| context.start(); |
| } catch (Exception e) { |
| log.error(sm.getString("hostConfig.context.restart", app.name), e); |
| } |
| } |
| } |
| |
| |
| private void undeploy(DeployedApplication app) { |
| if (log.isInfoEnabled()) { |
| log.info(sm.getString("hostConfig.undeploy", app.name)); |
| } |
| Container context = host.findChild(app.name); |
| try { |
| host.removeChild(context); |
| } catch (Throwable t) { |
| ExceptionUtils.handleThrowable(t); |
| log.warn(sm.getString("hostConfig.context.remove", app.name), t); |
| } |
| deployed.remove(app.name); |
| } |
| |
| |
| private void deleteRedeployResources(DeployedApplication app, String[] resources, int i, |
| boolean deleteReloadResources) { |
| |
| // Delete other redeploy resources |
| for (int j = i + 1; j < resources.length; j++) { |
| File current = new File(resources[j]); |
| // Never delete per host context.xml defaults |
| if (Constants.HostContextXml.equals(current.getName())) { |
| continue; |
| } |
| // Only delete resources in the appBase or the |
| // host's configBase |
| if (isDeletableResource(app, current)) { |
| if (log.isDebugEnabled()) { |
| log.debug(sm.getString("hostConfig.delete", current)); |
| } |
| ExpandWar.delete(current); |
| } |
| } |
| |
| // Delete reload resources (to remove any remaining .xml descriptor) |
| if (deleteReloadResources) { |
| String[] resources2 = app.reloadResources.keySet().toArray(new String[0]); |
| for (String s : resources2) { |
| File current = new File(s); |
| // Never delete per host context.xml defaults |
| if (Constants.HostContextXml.equals(current.getName())) { |
| continue; |
| } |
| // Only delete resources in the appBase or the host's |
| // configBase |
| if (isDeletableResource(app, current)) { |
| if (log.isDebugEnabled()) { |
| log.debug(sm.getString("hostConfig.delete", current)); |
| } |
| ExpandWar.delete(current); |
| } |
| } |
| } |
| } |
| |
| |
| /* |
| * Delete any resource that would trigger the automatic deployment code to re-deploy the application. This means |
| * deleting: |
| * |
| * - any resource located in the appBase |
| * |
| * - any deployment descriptor located under the configBase |
| * |
| * - symlinks in the appBase or configBase for either of the above |
| */ |
| private boolean isDeletableResource(DeployedApplication app, File resource) { |
| // The resource may be a file, a directory or a symlink to a file or directory. |
| |
| // Check that the resource is absolute. This should always be the case. |
| if (!resource.isAbsolute()) { |
| log.warn(sm.getString("hostConfig.resourceNotAbsolute", app.name, resource)); |
| return false; |
| } |
| |
| // Determine where the resource is located |
| String canonicalLocation; |
| try { |
| canonicalLocation = resource.getParentFile().getCanonicalPath(); |
| } catch (IOException ioe) { |
| log.warn(sm.getString("hostConfig.canonicalizing", resource.getParentFile(), app.name), ioe); |
| return false; |
| } |
| |
| String canonicalAppBase; |
| try { |
| canonicalAppBase = host.getAppBaseFile().getCanonicalPath(); |
| } catch (IOException ioe) { |
| log.warn(sm.getString("hostConfig.canonicalizing", host.getAppBaseFile(), app.name), ioe); |
| return false; |
| } |
| |
| if (canonicalLocation.equals(canonicalAppBase)) { |
| // Resource is located in the appBase so it may be deleted |
| return true; |
| } |
| |
| String canonicalConfigBase; |
| try { |
| canonicalConfigBase = host.getConfigBaseFile().getCanonicalPath(); |
| } catch (IOException ioe) { |
| log.warn(sm.getString("hostConfig.canonicalizing", host.getConfigBaseFile(), app.name), ioe); |
| return false; |
| } |
| |
| // Resource is a xml file in the configBase so it may be deleted |
| return canonicalLocation.equals(canonicalConfigBase) && resource.getName().endsWith(".xml"); |
| // All other resources should not be deleted |
| } |
| |
| |
| public void beforeStart() { |
| if (host.getCreateDirs()) { |
| File[] dirs = new File[] { host.getAppBaseFile(), host.getConfigBaseFile() }; |
| for (File dir : dirs) { |
| if (!dir.mkdirs() && !dir.isDirectory()) { |
| log.error(sm.getString("hostConfig.createDirs", dir)); |
| } |
| } |
| } |
| } |
| |
| |
| /** |
| * Process a "start" event for this Host. |
| */ |
| public void start() { |
| |
| if (log.isTraceEnabled()) { |
| log.trace(sm.getString("hostConfig.start")); |
| } |
| |
| try { |
| ObjectName hostON = host.getObjectName(); |
| oname = new ObjectName(hostON.getDomain() + ":type=Deployer,host=" + host.getName()); |
| Registry.getRegistry(null).registerComponent(this, oname, this.getClass().getName()); |
| } catch (Exception e) { |
| log.warn(sm.getString("hostConfig.jmx.register", oname), e); |
| } |
| |
| if (!host.getAppBaseFile().isDirectory()) { |
| log.error(sm.getString("hostConfig.appBase", host.getName(), host.getAppBaseFile().getPath())); |
| host.setDeployOnStartup(false); |
| host.setAutoDeploy(false); |
| } |
| |
| if (host.getDeployOnStartup()) { |
| deployApps(); |
| } |
| } |
| |
| |
| /** |
| * Process a "stop" event for this Host. |
| */ |
| public void stop() { |
| |
| if (log.isTraceEnabled()) { |
| log.trace(sm.getString("hostConfig.stop")); |
| } |
| |
| if (oname != null) { |
| try { |
| Registry.getRegistry(null).unregisterComponent(oname); |
| } catch (Exception e) { |
| log.warn(sm.getString("hostConfig.jmx.unregister", oname), e); |
| } |
| } |
| oname = null; |
| } |
| |
| |
| /** |
| * Check status of all webapps. |
| */ |
| protected void check() { |
| |
| if (host.getAutoDeploy()) { |
| // Check for resources modification to trigger redeployment |
| DeployedApplication[] apps = deployed.values().toArray(new DeployedApplication[0]); |
| for (DeployedApplication app : apps) { |
| if (tryAddServiced(app.name)) { |
| try { |
| checkResources(app, false); |
| } finally { |
| removeServiced(app.name); |
| } |
| } |
| } |
| |
| // Check for old versions of applications that can now be undeployed |
| if (host.getUndeployOldVersions()) { |
| checkUndeploy(); |
| } |
| |
| // Hotdeploy applications |
| deployApps(); |
| } |
| } |
| |
| |
| /** |
| * Check status of a specific web application and reload, redeploy or deploy it as necessary. This method is for use |
| * with functionality such as management web applications that upload new/updated web applications and need to |
| * trigger the appropriate action to deploy them. This method assumes that any uploading/updating has been completed |
| * before this method is called. Any action taken as a result of the checks will complete before this method |
| * returns. |
| * |
| * @param name The name of the web application to check |
| */ |
| public void check(String name) { |
| synchronized (host) { |
| if (!host.getState().isAvailable()) { |
| return; |
| } |
| if (tryAddServiced(name)) { |
| try { |
| DeployedApplication app = deployed.get(name); |
| if (app != null) { |
| checkResources(app, true); |
| } |
| deployApps(name); |
| } finally { |
| removeServiced(name); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Check for old versions of applications using parallel deployment that are now unused (have no active sessions) |
| * and undeploy any that are found. |
| */ |
| public void checkUndeploy() { |
| synchronized (host) { |
| if (deployed.size() < 2) { |
| return; |
| } |
| |
| // Need ordered set of names |
| SortedSet<String> sortedAppNames = new TreeSet<>(deployed.keySet()); |
| |
| Iterator<String> iter = sortedAppNames.iterator(); |
| |
| ContextName previous = new ContextName(iter.next(), false); |
| do { |
| ContextName current = new ContextName(iter.next(), false); |
| |
| if (current.getPath().equals(previous.getPath())) { |
| // Current and previous are same path - current will always |
| // be a later version |
| Context previousContext = (Context) host.findChild(previous.getName()); |
| Context currentContext = (Context) host.findChild(current.getName()); |
| if (previousContext != null && currentContext != null && currentContext.getState().isAvailable() && |
| tryAddServiced(previous.getName())) { |
| try { |
| Manager manager = previousContext.getManager(); |
| if (manager != null) { |
| int sessionCount; |
| if (manager instanceof DistributedManager) { |
| sessionCount = ((DistributedManager) manager).getActiveSessionsFull(); |
| } else { |
| sessionCount = manager.getActiveSessions(); |
| } |
| if (sessionCount == 0) { |
| if (log.isInfoEnabled()) { |
| log.info(sm.getString("hostConfig.undeployVersion", previous.getName())); |
| } |
| DeployedApplication app = deployed.get(previous.getName()); |
| String[] resources = app.redeployResources.keySet().toArray(new String[0]); |
| // Version is unused - undeploy it completely |
| // The -1 is a 'trick' to ensure all redeploy |
| // resources are removed |
| undeploy(app); |
| deleteRedeployResources(app, resources, -1, true); |
| } |
| } |
| } finally { |
| removeServiced(previous.getName()); |
| } |
| } |
| } |
| previous = current; |
| } while (iter.hasNext()); |
| } |
| } |
| |
| /** |
| * Add a new Context to be managed by us. Entry point for the admin webapp, and other JMX Context controllers. |
| * |
| * @param context The context instance |
| */ |
| public void manageApp(Context context) { |
| |
| String contextName = context.getName(); |
| |
| if (deployed.containsKey(contextName)) { |
| return; |
| } |
| |
| DeployedApplication deployedApp = new DeployedApplication(contextName, false); |
| |
| // Add the associated docBase to the redeployed list if it's a WAR |
| boolean isWar = false; |
| if (context.getDocBase() != null) { |
| File docBase = new File(context.getDocBase()); |
| if (!docBase.isAbsolute()) { |
| docBase = new File(host.getAppBaseFile(), context.getDocBase()); |
| } |
| deployedApp.redeployResources.put(docBase.getAbsolutePath(), Long.valueOf(docBase.lastModified())); |
| if (docBase.getAbsolutePath().toLowerCase(Locale.ENGLISH).endsWith(".war")) { |
| isWar = true; |
| } |
| } |
| host.addChild(context); |
| // Add the eventual unpacked WAR and all the resources which will be |
| // watched inside it |
| boolean unpackWAR = unpackWARs; |
| if (unpackWAR && context instanceof StandardContext) { |
| unpackWAR = ((StandardContext) context).getUnpackWAR(); |
| } |
| if (isWar && unpackWAR) { |
| File docBase = new File(host.getAppBaseFile(), context.getBaseName()); |
| deployedApp.redeployResources.put(docBase.getAbsolutePath(), Long.valueOf(docBase.lastModified())); |
| addWatchedResources(deployedApp, docBase.getAbsolutePath(), context); |
| } else { |
| addWatchedResources(deployedApp, null, context); |
| } |
| deployed.put(contextName, deployedApp); |
| } |
| |
| /** |
| * Remove a webapp from our control. Entry point for the admin webapp, and other JMX Context controllers. |
| * <p> |
| * Note: It is expected that the caller has successfully added the app to servicedSet before calling this method. |
| * |
| * @param contextName The context name |
| */ |
| public void unmanageApp(String contextName) { |
| deployed.remove(contextName); |
| host.removeChild(host.findChild(contextName)); |
| } |
| |
| // ----------------------------------------------------- Instance Variables |
| |
| |
| /** |
| * This class represents the state of a deployed application, as well as the monitored resources. |
| */ |
| protected static class DeployedApplication { |
| public DeployedApplication(String name, boolean hasDescriptor) { |
| this.name = name; |
| this.hasDescriptor = hasDescriptor; |
| } |
| |
| /** |
| * Application context path. The assertion is that (host.getChild(name) != null). |
| */ |
| public final String name; |
| |
| /** |
| * Does this application have a context.xml descriptor file on the host's configBase? |
| */ |
| public final boolean hasDescriptor; |
| |
| /** |
| * Any modification of the specified (static) resources will cause a redeployment of the application. If any of |
| * the specified resources is removed, the application will be undeployed. Typically, this will contain |
| * resources like the context.xml file, a compressed WAR path. The value is the last modification time. |
| */ |
| public final LinkedHashMap<String,Long> redeployResources = new LinkedHashMap<>(); |
| |
| /** |
| * Any modification of the specified (static) resources will cause a reload of the application. This will |
| * typically contain resources such as the web.xml of a webapp, but can be configured to contain additional |
| * descriptors. The value is the last modification time. |
| */ |
| public final HashMap<String,Long> reloadResources = new HashMap<>(); |
| |
| /** |
| * Instant where the application was last put in service. |
| */ |
| public long timestamp = System.currentTimeMillis(); |
| |
| /** |
| * In some circumstances, such as when unpackWARs is true, a directory may be added to the appBase that is |
| * ignored. This flag indicates that the user has been warned so that the warning is not logged on every run of |
| * the auto deployer. |
| */ |
| public boolean loggedDirWarning = false; |
| } |
| |
| private record DeployDescriptor(HostConfig config, ContextName cn, File descriptor) implements Runnable { |
| @Override |
| public void run() { |
| try { |
| config.deployDescriptor(cn, descriptor); |
| } finally { |
| config.removeServiced(cn.getName()); |
| } |
| } |
| } |
| |
| private record DeployWar(HostConfig config, ContextName cn, File war) implements Runnable { |
| @Override |
| public void run() { |
| try { |
| config.deployWAR(cn, war); |
| } finally { |
| config.removeServiced(cn.getName()); |
| } |
| } |
| } |
| |
| private record DeployDirectory(HostConfig config, ContextName cn, File dir) implements Runnable { |
| @Override |
| public void run() { |
| try { |
| config.deployDirectory(cn, dir); |
| } finally { |
| config.removeServiced(cn.getName()); |
| } |
| } |
| } |
| |
| |
| private record MigrateApp(HostConfig config, ContextName cn, File source, File destination) implements Runnable { |
| @Override |
| public void run() { |
| try { |
| config.migrateLegacyApp(source, destination); |
| } finally { |
| config.removeServiced(cn.getName()); |
| } |
| } |
| } |
| |
| |
| /* |
| * The purpose of this class is to provide a way for HostConfig to get a Context to delete an expanded WAR after the |
| * Context stops. This is to resolve this issue described in Bug 57772. The alternative solutions require either |
| * duplicating a lot of the Context.reload() code in HostConfig or adding a new reload(boolean) method to Context |
| * that allows the caller to optionally delete any expanded WAR. |
| * |
| * The LifecycleListener approach offers greater flexibility and enables the behaviour to be changed / extended / |
| * removed in future without changing the Context API. |
| */ |
| private record ExpandedDirectoryRemovalListener(File toDelete, String newDocBase) implements LifecycleListener { |
| @Override |
| public void lifecycleEvent(LifecycleEvent event) { |
| if (Lifecycle.AFTER_STOP_EVENT.equals(event.getType())) { |
| // The context has stopped. |
| Context context = (Context) event.getLifecycle(); |
| |
| // Remove the old expanded WAR. |
| ExpandWar.delete(toDelete); |
| |
| // Reset the docBase to trigger re-expansion of the WAR. |
| context.setDocBase(newDocBase); |
| |
| // Remove this listener from the Context else it will run every |
| // time the Context is stopped. |
| context.removeLifecycleListener(this); |
| } |
| } |
| } |
| } |