| /* |
| * 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.net.MalformedURLException; |
| import java.net.URL; |
| import java.net.URLConnection; |
| import java.security.Principal; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.jar.JarEntry; |
| import java.util.jar.JarFile; |
| import java.util.logging.Level; |
| import java.util.logging.LogManager; |
| import java.util.logging.Logger; |
| |
| import jakarta.servlet.Servlet; |
| import jakarta.servlet.ServletException; |
| import jakarta.servlet.annotation.WebServlet; |
| |
| import org.apache.catalina.Container; |
| import org.apache.catalina.Context; |
| import org.apache.catalina.Engine; |
| import org.apache.catalina.Globals; |
| import org.apache.catalina.Host; |
| import org.apache.catalina.Lifecycle; |
| import org.apache.catalina.LifecycleEvent; |
| import org.apache.catalina.LifecycleException; |
| import org.apache.catalina.LifecycleListener; |
| import org.apache.catalina.Realm; |
| import org.apache.catalina.Server; |
| import org.apache.catalina.Service; |
| import org.apache.catalina.Wrapper; |
| import org.apache.catalina.authenticator.NonLoginAuthenticator; |
| import org.apache.catalina.connector.Connector; |
| import org.apache.catalina.core.ContainerBase; |
| import org.apache.catalina.core.NamingContextListener; |
| import org.apache.catalina.core.StandardContext; |
| import org.apache.catalina.core.StandardEngine; |
| import org.apache.catalina.core.StandardHost; |
| import org.apache.catalina.core.StandardServer; |
| import org.apache.catalina.core.StandardService; |
| import org.apache.catalina.core.StandardWrapper; |
| import org.apache.catalina.realm.GenericPrincipal; |
| import org.apache.catalina.realm.RealmBase; |
| import org.apache.catalina.util.ContextName; |
| import org.apache.catalina.util.IOTools; |
| import org.apache.tomcat.util.ExceptionUtils; |
| import org.apache.tomcat.util.buf.UriUtil; |
| import org.apache.tomcat.util.compat.JreCompat; |
| import org.apache.tomcat.util.descriptor.web.LoginConfig; |
| import org.apache.tomcat.util.file.ConfigFileLoader; |
| import org.apache.tomcat.util.file.ConfigurationSource; |
| import org.apache.tomcat.util.modeler.Registry; |
| import org.apache.tomcat.util.res.StringManager; |
| |
| // TODO: lazy init for the temp dir - only when a JSP is compiled or |
| // get temp dir is called we need to create it. This will avoid the |
| // need for the baseDir |
| |
| // TODO: allow contexts without a base dir - i.e. |
| // only programmatic. This would disable the default servlet. |
| |
| /** |
| * Minimal tomcat starter for embedding/unit tests. |
| * |
| * <p> |
| * Tomcat supports multiple styles of configuration and |
| * startup - the most common and stable is server.xml-based, |
| * implemented in org.apache.catalina.startup.Bootstrap. |
| * |
| * <p> |
| * This class is for use in apps that embed tomcat. |
| * |
| * <p> |
| * Requirements: |
| * <ul> |
| * <li>all tomcat classes and possibly servlets are in the classpath. |
| * (for example all is in one big jar, or in eclipse CP, or in |
| * any other combination)</li> |
| * |
| * <li>we need one temporary directory for work files</li> |
| * |
| * <li>no config file is required. This class provides methods to |
| * use if you have a webapp with a web.xml file, but it is |
| * optional - you can use your own servlets.</li> |
| * </ul> |
| * |
| * <p> |
| * There are a variety of 'add' methods to configure servlets and webapps. These |
| * methods, by default, create a simple in-memory security realm and apply it. |
| * If you need more complex security processing, you can define a subclass of |
| * this class. |
| * |
| * <p> |
| * This class provides a set of convenience methods for configuring web |
| * application contexts; all overloads of the method <code>addWebapp()</code>. |
| * These methods are equivalent to adding a web application to the Host's |
| * appBase (normally the webapps directory). These methods create a Context, |
| * configure it with the equivalent of the defaults provided by |
| * <code>conf/web.xml</code> (see {@link #initWebappDefaults(String)} for |
| * details) and add the Context to a Host. These methods do not use a global |
| * default web.xml; rather, they add a {@link LifecycleListener} to configure |
| * the defaults. Any WEB-INF/web.xml and META-INF/context.xml packaged with the |
| * application will be processed normally. Normal web fragment and |
| * {@link jakarta.servlet.ServletContainerInitializer} processing will be applied. |
| * |
| * <p> |
| * In complex cases, you may prefer to use the ordinary Tomcat API to create |
| * webapp contexts; for example, you might need to install a custom Loader |
| * before the call to {@link Host#addChild(Container)}. To replicate the basic |
| * behavior of the <code>addWebapp</code> methods, you may want to call two |
| * methods of this class: {@link #noDefaultWebXmlPath()} and |
| * {@link #getDefaultWebXmlListener()}. |
| * |
| * <p> |
| * {@link #getDefaultWebXmlListener()} returns a {@link LifecycleListener} that |
| * adds the standard DefaultServlet, JSP processing, and welcome files. If you |
| * add this listener, you must prevent Tomcat from applying any standard global |
| * web.xml with ... |
| * |
| * <p> |
| * {@link #noDefaultWebXmlPath()} returns a dummy pathname to configure to |
| * prevent {@link ContextConfig} from trying to apply a global web.xml file. |
| * |
| * <p> |
| * This class provides a main() and few simple CLI arguments, |
| * see setters for doc. It can be used for simple tests and |
| * demo. |
| * |
| * @see <a href="https://gitbox.apache.org/repos/asf?p=tomcat.git;a=blob;f=test/org/apache/catalina/startup/TestTomcat.java">TestTomcat</a> |
| * @author Costin Manolache |
| */ |
| public class Tomcat { |
| |
| private static final StringManager sm = StringManager.getManager(Tomcat.class); |
| |
| // Some logging implementations use weak references for loggers so there is |
| // the possibility that logging configuration could be lost if GC runs just |
| // after Loggers are configured but before they are used. The purpose of |
| // this Map is to retain strong references to explicitly configured loggers |
| // so that configuration is not lost. |
| private final Map<String, Logger> pinnedLoggers = new HashMap<>(); |
| |
| protected Server server; |
| |
| protected int port = 8080; |
| protected String hostname = "localhost"; |
| protected String basedir; |
| |
| private final Map<String, String> userPass = new HashMap<>(); |
| private final Map<String, List<String>> userRoles = new HashMap<>(); |
| private final Map<String, Principal> userPrincipals = new HashMap<>(); |
| |
| private boolean addDefaultWebXmlToWebapp = true; |
| |
| public Tomcat() { |
| ExceptionUtils.preload(); |
| } |
| |
| /** |
| * Tomcat requires that the base directory is set because the defaults for |
| * a number of other locations, such as the work directory, are derived from |
| * the base directory. This should be the first method called. |
| * <p> |
| * If this method is not called then Tomcat will attempt to use these |
| * locations in the following order: |
| * <ol> |
| * <li>if set, the catalina.base system property</li> |
| * <li>if set, the catalina.home system property</li> |
| * <li>The user.dir system property (the directory where Java was run from) |
| * where a directory named tomcat.$PORT will be created. $PORT is the |
| * value configured via {@link #setPort(int)} which defaults to 8080 if |
| * not set</li> |
| * </ol> |
| * The user should ensure that the file permissions for the base directory |
| * are appropriate. |
| * <p> |
| * TODO: disable work dir if not needed ( no jsp, etc ). |
| * |
| * @param basedir The Tomcat base folder on which all others will be derived |
| */ |
| public void setBaseDir(String basedir) { |
| this.basedir = basedir; |
| } |
| |
| /** |
| * Set the port for the default connector. The default connector will |
| * only be created if getConnector is called. |
| * @param port The port number |
| */ |
| public void setPort(int port) { |
| this.port = port; |
| } |
| |
| /** |
| * The the hostname of the default host, default is |
| * 'localhost'. |
| * @param s The default host name |
| */ |
| public void setHostname(String s) { |
| hostname = s; |
| } |
| |
| |
| /** |
| * This is equivalent to adding a web application to a Host's appBase |
| * (usually Tomcat's webapps directory). By default, the equivalent of the |
| * default web.xml will be applied to the web application (see |
| * {@link #initWebappDefaults(String)}). This may be prevented by calling |
| * {@link #setAddDefaultWebXmlToWebapp(boolean)} with {@code false}. Any |
| * <code>WEB-INF/web.xml</code> and <code>META-INF/context.xml</code> |
| * packaged with the application will always be processed and normal web |
| * fragment and {@link jakarta.servlet.ServletContainerInitializer} |
| * processing will always be applied. |
| * |
| * @param contextPath The context mapping to use, "" for root context. |
| * @param docBase Base directory for the context, for static files. Must |
| * exist and be an absolute path. |
| * |
| * @return the deployed context |
| */ |
| public Context addWebapp(String contextPath, String docBase) { |
| return addWebapp(getHost(), contextPath, docBase); |
| } |
| |
| |
| /** |
| * Copy the specified WAR file to the Host's appBase and then call |
| * {@link #addWebapp(String, String)} with the newly copied WAR. The WAR |
| * will <b>NOT</b> be removed from the Host's appBase when the Tomcat |
| * instance stops. Note that {@link ExpandWar} provides utility methods that |
| * may be used to delete the WAR and/or expanded directory if required. |
| * |
| * @param contextPath The context mapping to use, "" for root context. |
| * @param source The location from which the WAR should be copied |
| * |
| * @return The deployed Context |
| * |
| * @throws IOException If an I/O error occurs while copying the WAR file |
| * from the specified URL to the appBase |
| */ |
| public Context addWebapp(String contextPath, URL source) throws IOException { |
| |
| ContextName cn = new ContextName(contextPath, null); |
| |
| // Make sure a conflicting web application has not already been deployed |
| Host h = getHost(); |
| if (h.findChild(cn.getName()) != null) { |
| throw new IllegalArgumentException(sm.getString("tomcat.addWebapp.conflictChild", |
| source, contextPath, cn.getName())); |
| } |
| |
| // Make sure appBase does not contain a conflicting web application |
| File targetWar = new File(h.getAppBaseFile(), cn.getBaseName() + ".war"); |
| File targetDir = new File(h.getAppBaseFile(), cn.getBaseName()); |
| |
| if (targetWar.exists()) { |
| throw new IllegalArgumentException(sm.getString("tomcat.addWebapp.conflictFile", |
| source, contextPath, targetWar.getAbsolutePath())); |
| } |
| if (targetDir.exists()) { |
| throw new IllegalArgumentException(sm.getString("tomcat.addWebapp.conflictFile", |
| source, contextPath, targetDir.getAbsolutePath())); |
| } |
| |
| // Should be good to copy the WAR now |
| URLConnection uConn = source.openConnection(); |
| |
| try (InputStream is = uConn.getInputStream(); |
| OutputStream os = new FileOutputStream(targetWar)) { |
| IOTools.flow(is, os); |
| } |
| |
| return addWebapp(contextPath, targetWar.getAbsolutePath()); |
| } |
| |
| |
| /** |
| * Add a context - programmatic mode, no default web.xml used. This means |
| * that there is no JSP support (no JSP servlet), no default servlet and |
| * no web socket support unless explicitly enabled via the programmatic |
| * interface. There is also no |
| * {@link jakarta.servlet.ServletContainerInitializer} processing and no |
| * annotation processing. If a |
| * {@link jakarta.servlet.ServletContainerInitializer} is added |
| * programmatically, there will still be no scanning for |
| * {@link jakarta.servlet.annotation.HandlesTypes} matches. |
| * |
| * <p> |
| * API calls equivalent with web.xml: |
| * |
| * <pre>{@code |
| * // context-param |
| * ctx.addParameter("name", "value"); |
| * |
| * |
| * // error-page |
| * ErrorPage ep = new ErrorPage(); |
| * ep.setErrorCode(500); |
| * ep.setLocation("/error.html"); |
| * ctx.addErrorPage(ep); |
| * |
| * ctx.addMimeMapping("ext", "type"); |
| * }</pre> |
| * |
| * |
| * <p> |
| * Note: If you reload the Context, all your configuration will be lost. If |
| * you need reload support, consider using a LifecycleListener to provide |
| * your configuration. |
| * |
| * <p> |
| * TODO: add the rest |
| * |
| * @param contextPath The context mapping to use, "" for root context. |
| * @param docBase Base directory for the context, for static files. |
| * Must exist, relative to the server home |
| * @return the deployed context |
| */ |
| public Context addContext(String contextPath, String docBase) { |
| return addContext(getHost(), contextPath, docBase); |
| } |
| |
| /** |
| * Equivalent to <servlet><servlet-name><servlet-class>. |
| * |
| * <p> |
| * In general it is better/faster to use the method that takes a |
| * Servlet as param - this one can be used if the servlet is not |
| * commonly used, and want to avoid loading all deps. |
| * ( for example: jsp servlet ) |
| * |
| * You can customize the returned servlet, ex: |
| * <pre> |
| * wrapper.addInitParameter("name", "value"); |
| * </pre> |
| * |
| * @param contextPath Context to add Servlet to |
| * @param servletName Servlet name (used in mappings) |
| * @param servletClass The class to be used for the Servlet |
| * @return The wrapper for the servlet |
| */ |
| public Wrapper addServlet(String contextPath, |
| String servletName, |
| String servletClass) { |
| Container ctx = getHost().findChild(contextPath); |
| return addServlet((Context) ctx, servletName, servletClass); |
| } |
| |
| /** |
| * Static version of {@link #addServlet(String, String, String)} |
| * @param ctx Context to add Servlet to |
| * @param servletName Servlet name (used in mappings) |
| * @param servletClass The class to be used for the Servlet |
| * @return The wrapper for the servlet |
| */ |
| public static Wrapper addServlet(Context ctx, |
| String servletName, |
| String servletClass) { |
| // will do class for name and set init params |
| Wrapper sw = ctx.createWrapper(); |
| if (sw == null) { |
| return null; |
| } |
| sw.setServletClass(servletClass); |
| sw.setName(servletName); |
| ctx.addChild(sw); |
| |
| return sw; |
| } |
| |
| /** |
| * Add an existing Servlet to the context with no class.forName or |
| * initialisation. |
| * @param contextPath Context to add Servlet to |
| * @param servletName Servlet name (used in mappings) |
| * @param servlet The Servlet to add |
| * @return The wrapper for the servlet |
| */ |
| public Wrapper addServlet(String contextPath, |
| String servletName, |
| Servlet servlet) { |
| Container ctx = getHost().findChild(contextPath); |
| return addServlet((Context) ctx, servletName, servlet); |
| } |
| |
| /** |
| * Static version of {@link #addServlet(String, String, Servlet)}. |
| * @param ctx Context to add Servlet to |
| * @param servletName Servlet name (used in mappings) |
| * @param servlet The Servlet to add |
| * @return The wrapper for the servlet |
| */ |
| public static Wrapper addServlet(Context ctx, |
| String servletName, |
| Servlet servlet) { |
| // will do class for name and set init params |
| Wrapper sw = new ExistingStandardWrapper(servlet); |
| sw.setName(servletName); |
| ctx.addChild(sw); |
| |
| return sw; |
| } |
| |
| |
| /** |
| * Initialize the server given the specified configuration source. |
| * The server will be loaded according to the Tomcat configuration |
| * files contained in the source (server.xml, web.xml, context.xml, |
| * SSL certificates, etc). |
| * If no configuration source is specified, it will use the default |
| * locations for these files. |
| * @param source The configuration source |
| */ |
| public void init(ConfigurationSource source) { |
| init(source, null); |
| } |
| |
| /** |
| * Initialize the server given the specified configuration source. |
| * The server will be loaded according to the Tomcat configuration |
| * files contained in the source (server.xml, web.xml, context.xml, |
| * SSL certificates, etc). |
| * If no configuration source is specified, it will use the default |
| * locations for these files. |
| * @param source The configuration source |
| * @param catalinaArguments The arguments that should be passed to Catalina |
| */ |
| public void init(ConfigurationSource source, String[] catalinaArguments) { |
| ConfigFileLoader.setSource(source); |
| addDefaultWebXmlToWebapp = false; |
| Catalina catalina = new Catalina(); |
| // Load the Catalina instance with the regular configuration files |
| // from specified source |
| if (catalinaArguments == null) { |
| catalina.load(); |
| } else { |
| catalina.load(catalinaArguments); |
| } |
| // Retrieve and set the server |
| server = catalina.getServer(); |
| } |
| |
| |
| /** |
| * Initialize the server. |
| * |
| * @throws LifecycleException Init error |
| */ |
| public void init() throws LifecycleException { |
| getServer(); |
| server.init(); |
| } |
| |
| |
| /** |
| * Start the server. |
| * |
| * @throws LifecycleException Start error |
| */ |
| public void start() throws LifecycleException { |
| getServer(); |
| server.start(); |
| } |
| |
| /** |
| * Stop the server. |
| * |
| * @throws LifecycleException Stop error |
| */ |
| public void stop() throws LifecycleException { |
| getServer(); |
| server.stop(); |
| } |
| |
| |
| /** |
| * Destroy the server. This object cannot be used once this method has been |
| * called. |
| * |
| * @throws LifecycleException Destroy error |
| */ |
| public void destroy() throws LifecycleException { |
| getServer(); |
| server.destroy(); |
| // Could null out objects here |
| } |
| |
| /** |
| * Add a user for the in-memory realm. All created apps use this |
| * by default, can be replaced using setRealm(). |
| * @param user The user name |
| * @param pass The password |
| */ |
| public void addUser(String user, String pass) { |
| userPass.put(user, pass); |
| } |
| |
| /** |
| * Add a role to a user. |
| * @see #addUser(String, String) |
| * @param user The user name |
| * @param role The role name |
| */ |
| public void addRole(String user, String role) { |
| userRoles.computeIfAbsent(user, k -> new ArrayList<>()).add(role); |
| } |
| |
| // ------- Extra customization ------- |
| // You can tune individual Tomcat objects, using internal APIs |
| |
| /** |
| * Get the default HTTP connector that is used by the embedded |
| * Tomcat. It is first configured connector in the service. |
| * If there's no connector defined, it will create and add a default |
| * connector using the port and address specified in this Tomcat |
| * instance, and return it for further customization. |
| * |
| * @return The connector object |
| */ |
| public Connector getConnector() { |
| Service service = getService(); |
| if (service.findConnectors().length > 0) { |
| return service.findConnectors()[0]; |
| } |
| // The same as in standard Tomcat configuration. |
| // This creates a NIO HTTP connector. |
| Connector connector = new Connector("HTTP/1.1"); |
| connector.setPort(port); |
| service.addConnector(connector); |
| return connector; |
| } |
| |
| /** |
| * Set the specified connector in the service, if it is not already |
| * present. |
| * @param connector The connector instance to add |
| */ |
| public void setConnector(Connector connector) { |
| Service service = getService(); |
| boolean found = false; |
| for (Connector serviceConnector : service.findConnectors()) { |
| if (connector == serviceConnector) { |
| found = true; |
| break; |
| } |
| } |
| if (!found) { |
| service.addConnector(connector); |
| } |
| } |
| |
| /** |
| * Get the service object. Can be used to add more |
| * connectors and few other global settings. |
| * @return The service |
| */ |
| public Service getService() { |
| return getServer().findServices()[0]; |
| } |
| |
| /** |
| * Sets the current host - all future webapps will |
| * be added to this host. When tomcat starts, the |
| * host will be the default host. |
| * |
| * @param host The current host |
| */ |
| public void setHost(Host host) { |
| Engine engine = getEngine(); |
| boolean found = false; |
| for (Container engineHost : engine.findChildren()) { |
| if (engineHost == host) { |
| found = true; |
| break; |
| } |
| } |
| if (!found) { |
| engine.addChild(host); |
| } |
| } |
| |
| public Host getHost() { |
| Engine engine = getEngine(); |
| if (engine.findChildren().length > 0) { |
| return (Host) engine.findChildren()[0]; |
| } |
| |
| Host host = new StandardHost(); |
| host.setName(hostname); |
| getEngine().addChild(host); |
| return host; |
| } |
| |
| /** |
| * Access to the engine, for further customization. |
| * @return The engine |
| */ |
| public Engine getEngine() { |
| Service service = getServer().findServices()[0]; |
| if (service.getContainer() != null) { |
| return service.getContainer(); |
| } |
| Engine engine = new StandardEngine(); |
| engine.setName( "Tomcat" ); |
| engine.setDefaultHost(hostname); |
| engine.setRealm(createDefaultRealm()); |
| service.setContainer(engine); |
| return engine; |
| } |
| |
| /** |
| * Get the server object. You can add listeners and few more |
| * customizations. JNDI is disabled by default. |
| * @return The Server |
| */ |
| public Server getServer() { |
| |
| if (server != null) { |
| return server; |
| } |
| |
| System.setProperty("catalina.useNaming", "false"); |
| |
| server = new StandardServer(); |
| |
| initBaseDir(); |
| |
| // Set configuration source |
| ConfigFileLoader.setSource(new CatalinaBaseConfigurationSource(new File(basedir), null)); |
| |
| server.setPort( -1 ); |
| |
| Service service = new StandardService(); |
| service.setName("Tomcat"); |
| server.addService(service); |
| return server; |
| } |
| |
| /** |
| * @param host The host in which the context will be deployed |
| * @param contextPath The context mapping to use, "" for root context. |
| * @param dir Base directory for the context, for static files. |
| * Must exist, relative to the server home |
| * @return the deployed context |
| * @see #addContext(String, String) |
| */ |
| public Context addContext(Host host, String contextPath, String dir) { |
| return addContext(host, contextPath, contextPath, dir); |
| } |
| |
| /** |
| * @param host The host in which the context will be deployed |
| * @param contextPath The context mapping to use, "" for root context. |
| * @param contextName The context name |
| * @param dir Base directory for the context, for static files. |
| * Must exist, relative to the server home |
| * @return the deployed context |
| * @see #addContext(String, String) |
| */ |
| public Context addContext(Host host, String contextPath, String contextName, |
| String dir) { |
| silence(host, contextName); |
| Context ctx = createContext(host, contextPath); |
| ctx.setName(contextName); |
| ctx.setPath(contextPath); |
| ctx.setDocBase(dir); |
| ctx.addLifecycleListener(new FixContextListener()); |
| |
| if (host == null) { |
| getHost().addChild(ctx); |
| } else { |
| host.addChild(ctx); |
| } |
| return ctx; |
| } |
| |
| |
| /** |
| * This is equivalent to adding a web application to a Host's appBase |
| * (usually Tomcat's webapps directory). By default, the equivalent of the |
| * default web.xml will be applied to the web application (see |
| * {@link #initWebappDefaults(String)}). This may be prevented by calling |
| * {@link #setAddDefaultWebXmlToWebapp(boolean)} with {@code false}. Any |
| * <code>WEB-INF/web.xml</code> and <code>META-INF/context.xml</code> |
| * packaged with the application will always be processed and normal web |
| * fragment and {@link jakarta.servlet.ServletContainerInitializer} processing |
| * will always be applied. |
| * |
| * @param host The host in which the context will be deployed |
| * @param contextPath The context mapping to use, "" for root context. |
| * @param docBase Base directory for the context, for static files. Must |
| * exist and be an absolute path. |
| * |
| * @return the deployed context |
| */ |
| public Context addWebapp(Host host, String contextPath, String docBase) { |
| LifecycleListener listener = null; |
| try { |
| Class<?> clazz = Class.forName(getHost().getConfigClass()); |
| listener = (LifecycleListener) clazz.getConstructor().newInstance(); |
| } catch (ReflectiveOperationException e) { |
| // Wrap in IAE since we can't easily change the method signature to |
| // to throw the specific checked exceptions |
| throw new IllegalArgumentException(e); |
| } |
| |
| return addWebapp(host, contextPath, docBase, listener); |
| } |
| |
| |
| /** |
| * This is equivalent to adding a web application to a Host's appBase |
| * (usually Tomcat's webapps directory). By default, the equivalent of the |
| * default web.xml will be applied to the web application (see |
| * {@link #initWebappDefaults(String)}). This may be prevented by calling |
| * {@link #setAddDefaultWebXmlToWebapp(boolean)} with {@code false}. Any |
| * <code>WEB-INF/web.xml</code> and <code>META-INF/context.xml</code> |
| * packaged with the application will always be processed and normal web |
| * fragment and {@link jakarta.servlet.ServletContainerInitializer} processing |
| * will always be applied. |
| * |
| * @param host The host in which the context will be deployed |
| * @param contextPath The context mapping to use, "" for root context. |
| * @param docBase Base directory for the context, for static files. Must |
| * exist and be an absolute path. |
| * @param config Custom context configuration helper. Any configuration |
| * will be in addition to equivalent of the default |
| * web.xml configuration described above. |
| * |
| * @return the deployed context |
| */ |
| public Context addWebapp(Host host, String contextPath, String docBase, |
| LifecycleListener config) { |
| |
| silence(host, contextPath); |
| |
| Context ctx = createContext(host, contextPath); |
| ctx.setPath(contextPath); |
| ctx.setDocBase(docBase); |
| |
| if (addDefaultWebXmlToWebapp) { |
| ctx.addLifecycleListener(getDefaultWebXmlListener()); |
| } |
| |
| ctx.setConfigFile(getWebappConfigFile(docBase, contextPath)); |
| |
| ctx.addLifecycleListener(config); |
| |
| if (addDefaultWebXmlToWebapp && (config instanceof ContextConfig)) { |
| // prevent it from looking ( if it finds one - it'll have dup error ) |
| ((ContextConfig) config).setDefaultWebXml(noDefaultWebXmlPath()); |
| } |
| |
| if (host == null) { |
| getHost().addChild(ctx); |
| } else { |
| host.addChild(ctx); |
| } |
| |
| return ctx; |
| } |
| |
| /** |
| * Return a listener that provides the required configuration items for JSP |
| * processing. From the standard Tomcat global web.xml. Pass this to |
| * {@link Context#addLifecycleListener(LifecycleListener)} and then pass the |
| * result of {@link #noDefaultWebXmlPath()} to |
| * {@link ContextConfig#setDefaultWebXml(String)}. |
| * @return a listener object that configures default JSP processing. |
| */ |
| public LifecycleListener getDefaultWebXmlListener() { |
| return new DefaultWebXmlListener(); |
| } |
| |
| /** |
| * @return a pathname to pass to |
| * {@link ContextConfig#setDefaultWebXml(String)} when using |
| * {@link #getDefaultWebXmlListener()}. |
| */ |
| public String noDefaultWebXmlPath() { |
| return Constants.NoDefaultWebXml; |
| } |
| |
| // ---------- Helper methods and classes ------------------- |
| |
| /** |
| * Create an in-memory realm. You can replace it for contexts with a real |
| * one. The Realm created here will be added to the Engine by default and |
| * may be replaced at the Engine level or over-ridden (as per normal Tomcat |
| * behaviour) at the Host or Context level. |
| * @return a realm instance |
| */ |
| protected Realm createDefaultRealm() { |
| return new SimpleRealm(); |
| } |
| |
| |
| private class SimpleRealm extends RealmBase { |
| |
| @Override |
| protected String getPassword(String username) { |
| return userPass.get(username); |
| } |
| |
| @Override |
| protected Principal getPrincipal(String username) { |
| Principal p = userPrincipals.get(username); |
| if (p == null) { |
| String pass = userPass.get(username); |
| if (pass != null) { |
| p = new GenericPrincipal(username, |
| userRoles.get(username)); |
| userPrincipals.put(username, p); |
| } |
| } |
| return p; |
| } |
| } |
| |
| |
| protected void initBaseDir() { |
| String catalinaHome = System.getProperty(Globals.CATALINA_HOME_PROP); |
| if (basedir == null) { |
| basedir = System.getProperty(Globals.CATALINA_BASE_PROP); |
| } |
| if (basedir == null) { |
| basedir = catalinaHome; |
| } |
| if (basedir == null) { |
| // Create a temp dir. |
| basedir = System.getProperty("user.dir") + "/tomcat." + port; |
| } |
| |
| File baseFile = new File(basedir); |
| if (baseFile.exists()) { |
| if (!baseFile.isDirectory()) { |
| throw new IllegalArgumentException(sm.getString("tomcat.baseDirNotDir", baseFile)); |
| } |
| } else { |
| if (!baseFile.mkdirs()) { |
| // Failed to create base directory |
| throw new IllegalStateException(sm.getString("tomcat.baseDirMakeFail", baseFile)); |
| } |
| /* |
| * If file permissions were going to be set on the newly created |
| * directory, this is the place to do it. However, even simple |
| * calls such as File.setReadable(boolean,boolean) behaves |
| * differently on different platforms. Therefore, setBaseDir |
| * documents that the user needs to do this. |
| */ |
| } |
| try { |
| baseFile = baseFile.getCanonicalFile(); |
| } catch (IOException e) { |
| baseFile = baseFile.getAbsoluteFile(); |
| } |
| server.setCatalinaBase(baseFile); |
| System.setProperty(Globals.CATALINA_BASE_PROP, baseFile.getPath()); |
| basedir = baseFile.getPath(); |
| |
| if (catalinaHome == null) { |
| server.setCatalinaHome(baseFile); |
| } else { |
| File homeFile = new File(catalinaHome); |
| if (!homeFile.isDirectory() && !homeFile.mkdirs()) { |
| // Failed to create home directory |
| throw new IllegalStateException(sm.getString("tomcat.homeDirMakeFail", homeFile)); |
| } |
| try { |
| homeFile = homeFile.getCanonicalFile(); |
| } catch (IOException e) { |
| homeFile = homeFile.getAbsoluteFile(); |
| } |
| server.setCatalinaHome(homeFile); |
| } |
| System.setProperty(Globals.CATALINA_HOME_PROP, |
| server.getCatalinaHome().getPath()); |
| } |
| |
| static final String[] silences = new String[] { |
| "org.apache.coyote.http11.Http11NioProtocol", |
| "org.apache.catalina.core.StandardService", |
| "org.apache.catalina.core.StandardEngine", |
| "org.apache.catalina.startup.ContextConfig", |
| "org.apache.catalina.core.ApplicationContext", |
| "org.apache.catalina.core.AprLifecycleListener" |
| }; |
| |
| private boolean silent = false; |
| |
| /** |
| * Controls if the loggers will be silenced or not. |
| * @param silent <code>true</code> sets the log level to WARN for the |
| * loggers that log information on Tomcat start up. This |
| * prevents the usual startup information being logged. |
| * <code>false</code> sets the log level to the default |
| * level of INFO. |
| */ |
| public void setSilent(boolean silent) { |
| this.silent = silent; |
| for (String s : silences) { |
| Logger logger = Logger.getLogger(s); |
| pinnedLoggers.put(s, logger); |
| if (silent) { |
| logger.setLevel(Level.WARNING); |
| } else { |
| logger.setLevel(Level.INFO); |
| } |
| } |
| } |
| |
| private void silence(Host host, String contextPath) { |
| String loggerName = getLoggerName(host, contextPath); |
| Logger logger = Logger.getLogger(loggerName); |
| pinnedLoggers.put(loggerName, logger); |
| if (silent) { |
| logger.setLevel(Level.WARNING); |
| } else { |
| logger.setLevel(Level.INFO); |
| } |
| } |
| |
| |
| /** |
| * By default, when calling addWebapp() to create a Context, the settings from |
| * from the default web.xml are added to the context. Calling this method with |
| * a <code>false</code> value prior to calling addWebapp() allows to opt out of |
| * the default settings. In that event you will need to add the configurations |
| * yourself, either programmatically or by using web.xml deployment descriptors. |
| * @param addDefaultWebXmlToWebapp <code>false</code> will prevent the class from |
| * automatically adding the default settings when |
| * calling addWebapp(). |
| * <code>true</code> will add the default settings |
| * and is the default behavior. |
| * @see #addWebapp(Host, String, String, LifecycleListener) |
| */ |
| public void setAddDefaultWebXmlToWebapp(boolean addDefaultWebXmlToWebapp){ |
| this.addDefaultWebXmlToWebapp = addDefaultWebXmlToWebapp; |
| } |
| |
| |
| /* |
| * Uses essentially the same logic as {@link ContainerBase#logName()}. |
| */ |
| private String getLoggerName(Host host, String contextName) { |
| if (host == null) { |
| host = getHost(); |
| } |
| StringBuilder loggerName = new StringBuilder(); |
| loggerName.append(ContainerBase.class.getName()); |
| loggerName.append(".["); |
| // Engine name |
| loggerName.append(host.getParent().getName()); |
| loggerName.append("].["); |
| // Host name |
| loggerName.append(host.getName()); |
| loggerName.append("].["); |
| // Context name |
| if (contextName == null || contextName.equals("")) { |
| loggerName.append('/'); |
| } else if (contextName.startsWith("##")) { |
| loggerName.append('/'); |
| loggerName.append(contextName); |
| } |
| loggerName.append(']'); |
| |
| return loggerName.toString(); |
| } |
| |
| /** |
| * Create the configured {@link Context} for the given <code>host</code>. |
| * The default constructor of the class that was configured with |
| * {@link StandardHost#setContextClass(String)} will be used |
| * |
| * @param host |
| * host for which the {@link Context} should be created, or |
| * <code>null</code> if default host should be used |
| * @param url |
| * path of the webapp which should get the {@link Context} |
| * @return newly created {@link Context} |
| */ |
| private Context createContext(Host host, String url) { |
| String defaultContextClass = StandardContext.class.getName(); |
| String contextClass = StandardContext.class.getName(); |
| if (host == null) { |
| host = this.getHost(); |
| } |
| if (host instanceof StandardHost) { |
| contextClass = ((StandardHost) host).getContextClass(); |
| } |
| try { |
| if (defaultContextClass.equals(contextClass)) { |
| return new StandardContext(); |
| } else { |
| return (Context) Class.forName(contextClass).getConstructor() |
| .newInstance(); |
| } |
| |
| } catch (ReflectiveOperationException | IllegalArgumentException | SecurityException e) { |
| throw new IllegalArgumentException(sm.getString("tomcat.noContextClass", contextClass, host, url), e); |
| } |
| } |
| |
| /** |
| * Enables JNDI naming which is disabled by default. Server must implement |
| * {@link Lifecycle} in order for the {@link NamingContextListener} to be |
| * used. |
| * |
| */ |
| public void enableNaming() { |
| // Make sure getServer() has been called as that is where naming is |
| // disabled |
| getServer(); |
| server.addLifecycleListener(new NamingContextListener()); |
| |
| System.setProperty("catalina.useNaming", "true"); |
| |
| String value = "org.apache.naming"; |
| String oldValue = |
| System.getProperty(javax.naming.Context.URL_PKG_PREFIXES); |
| if (oldValue != null) { |
| if (oldValue.contains(value)) { |
| value = oldValue; |
| } else { |
| value = value + ":" + oldValue; |
| } |
| } |
| System.setProperty(javax.naming.Context.URL_PKG_PREFIXES, value); |
| |
| value = System.getProperty |
| (javax.naming.Context.INITIAL_CONTEXT_FACTORY); |
| if (value == null) { |
| System.setProperty |
| (javax.naming.Context.INITIAL_CONTEXT_FACTORY, |
| "org.apache.naming.java.javaURLContextFactory"); |
| } |
| } |
| |
| |
| /** |
| * Provide default configuration for a context. This is broadly the |
| * programmatic equivalent of the default web.xml and provides the following |
| * features: |
| * <ul> |
| * <li>Default servlet mapped to "/"</li> |
| * <li>JSP servlet mapped to "*.jsp" and ""*.jspx"</li> |
| * <li>Session timeout of 30 minutes</li> |
| * <li>MIME mappings (subset of those in conf/web.xml)</li> |
| * <li>Welcome files</li> |
| * </ul> |
| * TODO: Align the MIME mappings with conf/web.xml - possibly via a common |
| * file. |
| * |
| * @param contextPath The path of the context to set the defaults for |
| */ |
| public void initWebappDefaults(String contextPath) { |
| Container ctx = getHost().findChild(contextPath); |
| initWebappDefaults((Context) ctx); |
| } |
| |
| |
| /** |
| * Static version of {@link #initWebappDefaults(String)}. |
| * |
| * @param ctx The context to set the defaults for |
| */ |
| public static void initWebappDefaults(Context ctx) { |
| // Default servlet |
| Wrapper servlet = addServlet( |
| ctx, "default", "org.apache.catalina.servlets.DefaultServlet"); |
| servlet.setLoadOnStartup(1); |
| servlet.setOverridable(true); |
| |
| // JSP servlet (by class name - to avoid loading all deps) |
| servlet = addServlet( |
| ctx, "jsp", "org.apache.jasper.servlet.JspServlet"); |
| servlet.addInitParameter("fork", "false"); |
| servlet.setLoadOnStartup(3); |
| servlet.setOverridable(true); |
| |
| // Servlet mappings |
| ctx.addServletMappingDecoded("/", "default"); |
| ctx.addServletMappingDecoded("*.jsp", "jsp"); |
| ctx.addServletMappingDecoded("*.jspx", "jsp"); |
| |
| // Sessions |
| ctx.setSessionTimeout(30); |
| |
| // MIME type mappings |
| addDefaultMimeTypeMappings(ctx); |
| |
| // Welcome files |
| ctx.addWelcomeFile("index.html"); |
| ctx.addWelcomeFile("index.htm"); |
| ctx.addWelcomeFile("index.jsp"); |
| } |
| |
| |
| /** |
| * Add the default MIME type mappings to the provide Context. |
| * |
| * @param context The web application to which the default MIME type |
| * mappings should be added. |
| */ |
| public static void addDefaultMimeTypeMappings(Context context) { |
| Properties defaultMimeMappings = new Properties(); |
| try (InputStream is = Tomcat.class.getResourceAsStream("MimeTypeMappings.properties")) { |
| defaultMimeMappings.load(is); |
| for (Map.Entry<Object, Object> entry: defaultMimeMappings.entrySet()) { |
| context.addMimeMapping((String) entry.getKey(), (String) entry.getValue()); |
| } |
| } catch (IOException e) { |
| throw new IllegalStateException(sm.getString("tomcat.defaultMimeTypeMappingsFail"), e); |
| } |
| } |
| |
| |
| /** |
| * Fix startup sequence - required if you don't use web.xml. |
| * |
| * <p> |
| * The start() method in context will set 'configured' to false - and |
| * expects a listener to set it back to true. |
| */ |
| public static class FixContextListener implements LifecycleListener { |
| |
| @Override |
| public void lifecycleEvent(LifecycleEvent event) { |
| try { |
| Context context = (Context) event.getLifecycle(); |
| if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) { |
| context.setConfigured(true); |
| |
| // Process annotations |
| WebAnnotationSet.loadApplicationAnnotations(context); |
| |
| // LoginConfig is required to process @ServletSecurity |
| // annotations |
| if (context.getLoginConfig() == null) { |
| context.setLoginConfig(new LoginConfig("NONE", null, null, null)); |
| context.getPipeline().addValve(new NonLoginAuthenticator()); |
| } |
| } |
| } catch (ClassCastException e) { |
| } |
| } |
| } |
| |
| |
| /** |
| * Fix reload - required if reloading and using programmatic configuration. |
| * When a context is reloaded, any programmatic configuration is lost. This |
| * listener sets the equivalent of conf/web.xml when the context starts. |
| */ |
| public static class DefaultWebXmlListener implements LifecycleListener { |
| @Override |
| public void lifecycleEvent(LifecycleEvent event) { |
| if (Lifecycle.BEFORE_START_EVENT.equals(event.getType())) { |
| initWebappDefaults((Context) event.getLifecycle()); |
| } |
| } |
| } |
| |
| |
| /** |
| * Helper class for wrapping existing servlets. This disables servlet |
| * lifecycle and normal reloading, but also reduces overhead and provide |
| * more direct control over the servlet. |
| */ |
| public static class ExistingStandardWrapper extends StandardWrapper { |
| private final Servlet existing; |
| |
| public ExistingStandardWrapper( Servlet existing ) { |
| this.existing = existing; |
| this.asyncSupported = hasAsync(existing); |
| } |
| |
| private static boolean hasAsync(Servlet existing) { |
| boolean result = false; |
| Class<?> clazz = existing.getClass(); |
| WebServlet ws = clazz.getAnnotation(WebServlet.class); |
| if (ws != null) { |
| result = ws.asyncSupported(); |
| } |
| return result; |
| } |
| |
| @Override |
| public synchronized Servlet loadServlet() throws ServletException { |
| if (!instanceInitialized) { |
| existing.init(facade); |
| instanceInitialized = true; |
| } |
| return existing; |
| } |
| @Override |
| public long getAvailable() { |
| return 0; |
| } |
| @Override |
| public boolean isUnavailable() { |
| return false; |
| } |
| @Override |
| public Servlet getServlet() { |
| return existing; |
| } |
| @Override |
| public String getServletClass() { |
| return existing.getClass().getName(); |
| } |
| } |
| |
| protected URL getWebappConfigFile(String path, String contextName) { |
| File docBase = new File(path); |
| if (docBase.isDirectory()) { |
| return getWebappConfigFileFromDirectory(docBase, contextName); |
| } else { |
| return getWebappConfigFileFromWar(docBase, contextName); |
| } |
| } |
| |
| private URL getWebappConfigFileFromDirectory(File docBase, String contextName) { |
| URL result = null; |
| File webAppContextXml = new File(docBase, Constants.ApplicationContextXml); |
| if (webAppContextXml.exists()) { |
| try { |
| result = webAppContextXml.toURI().toURL(); |
| } catch (MalformedURLException e) { |
| Logger.getLogger(getLoggerName(getHost(), contextName)).log(Level.WARNING, |
| sm.getString("tomcat.noContextXml", docBase), e); |
| } |
| } |
| return result; |
| } |
| |
| private URL getWebappConfigFileFromWar(File docBase, String contextName) { |
| URL result = null; |
| try (JarFile jar = new JarFile(docBase)) { |
| JarEntry entry = jar.getJarEntry(Constants.ApplicationContextXml); |
| if (entry != null) { |
| result = UriUtil.buildJarUrl(docBase, Constants.ApplicationContextXml); |
| } |
| } catch (IOException e) { |
| Logger.getLogger(getLoggerName(getHost(), contextName)).log(Level.WARNING, |
| sm.getString("tomcat.noContextXml", docBase), e); |
| } |
| return result; |
| } |
| |
| static { |
| // Graal native images don't load any configuration except the VM default |
| if (JreCompat.isGraalAvailable()) { |
| try (InputStream is = new FileInputStream(new File(System.getProperty("java.util.logging.config.file", "conf/logging.properties")))) { |
| LogManager.getLogManager().readConfiguration(is); |
| } catch (SecurityException | IOException e) { |
| // Ignore, the VM default will be used |
| } |
| } |
| } |
| |
| /** |
| * Main executable method for use with a Maven packager. |
| * @param args the command line arguments |
| * @throws Exception if an error occurs |
| */ |
| public static void main(String[] args) throws Exception { |
| // Process some command line parameters |
| String[] catalinaArguments = null; |
| for (int i = 0; i < args.length; i++) { |
| if (args[i].equals("--no-jmx")) { |
| Registry.disableRegistry(); |
| } else if (args[i].equals("--catalina")) { |
| // This was already processed before |
| // Skip the rest of the arguments as they are for Catalina |
| ArrayList<String> result = new ArrayList<>(); |
| for (int j = i + 1; j < args.length; j++) { |
| result.add(args[j]); |
| } |
| catalinaArguments = result.toArray(new String[0]); |
| break; |
| } |
| } |
| Tomcat tomcat = new Tomcat(); |
| // Create a Catalina instance and let it parse the configuration files |
| // It will also set a shutdown hook to stop the Server when needed |
| // Use the default configuration source |
| tomcat.init(null, catalinaArguments); |
| boolean await = false; |
| String path = ""; |
| // Process command line parameters |
| for (int i = 0; i < args.length; i++) { |
| if (args[i].equals("--war")) { |
| if (++i >= args.length) { |
| throw new IllegalArgumentException(sm.getString("tomcat.invalidCommandLine", args[i - 1])); |
| } |
| File war = new File(args[i]); |
| tomcat.addWebapp(path, war.getAbsolutePath()); |
| } else if (args[i].equals("--path")) { |
| if (++i >= args.length) { |
| throw new IllegalArgumentException(sm.getString("tomcat.invalidCommandLine", args[i - 1])); |
| } |
| path = args[i]; |
| } else if (args[i].equals("--await")) { |
| await = true; |
| } else if (args[i].equals("--no-jmx")) { |
| // This was already processed before |
| } else if (args[i].equals("--catalina")) { |
| // This was already processed before |
| // Skip the rest of the arguments as they are for Catalina |
| break; |
| } else { |
| throw new IllegalArgumentException(sm.getString("tomcat.invalidCommandLine", args[i])); |
| } |
| } |
| tomcat.start(); |
| // Ideally the utility threads are non daemon |
| if (await) { |
| tomcat.getServer().await(); |
| } |
| } |
| |
| } |