/*
* $Header: /home/cvs/jakarta-tomcat-4.0/catalina/src/share/org/apache/catalina/core/StandardHost.java,v 1.18.2.3 2002/01/27 21:13:15 remm Exp $
* $Revision: 1.18.2.3 $
* $Date: 2002/01/27 21:13:15 $
*
* ====================================================================
*
* The Apache Software License, Version 1.1
*
* Copyright (c) 1999 The Apache Software Foundation. All rights
* reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The end-user documentation included with the redistribution, if
* any, must include the following acknowlegement:
* "This product includes software developed by the
* Apache Software Foundation (http://www.apache.org/)."
* Alternately, this acknowlegement may appear in the software itself,
* if and wherever such third-party acknowlegements normally appear.
*
* 4. The names "The Jakarta Project", "Tomcat", and "Apache Software
* Foundation" must not be used to endorse or promote products derived
* from this software without prior written permission. For written
* permission, please contact apache@apache.org.
*
* 5. Products derived from this software may not be called "Apache"
* nor may "Apache" appear in their names without prior written
* permission of the Apache Group.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
* [Additional notices, if required by prior licensing conditions]
*
*/
package org.apache.catalina.core;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.catalina.Container;
import org.apache.catalina.Context;
import org.apache.catalina.Deployer;
import org.apache.catalina.Globals;
import org.apache.catalina.HttpRequest;
import org.apache.catalina.Host;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.Request;
import org.apache.catalina.Response;
import org.apache.catalina.Valve;
import org.apache.catalina.core.DefaultContext;
import org.apache.catalina.valves.ErrorDispatcherValve;
/**
* Standard implementation of the <b>Host</b> interface. Each
* child container must be a Context implementation to process the
* requests directed to a particular web application.
*
* @author Craig R. McClanahan
* @author Remy Maucherat
* @version $Revision: 1.18.2.3 $ $Date: 2002/01/27 21:13:15 $
*/
public class StandardHost
extends ContainerBase
implements Deployer, Host {
// ----------------------------------------------------------- Constructors
/**
* Create a new StandardHost component with the default basic Valve.
*/
public StandardHost() {
super();
pipeline.setBasic(new StandardHostValve());
}
// ----------------------------------------------------- Instance Variables
/**
* The set of aliases for this Host.
*/
private String[] aliases = new String[0];
/**
* The application root for this Host.
*/
private String appBase = ".";
/**
* The Java class name of the default context configuration class
* for deployed web applications.
*/
private String configClass =
"org.apache.catalina.startup.ContextConfig";
/**
* The Java class name of the default Context implementation class for
* deployed web applications.
*/
private String contextClass =
"org.apache.catalina.core.StandardContext";
/**
* The Java class name of the default error reporter implementation class
* for deployed web applications.
*/
private String errorReportValveClass =
"org.apache.catalina.valves.ErrorReportValve";
/**
* The descriptive information string for this implementation.
*/
private static final String info =
"org.apache.catalina.core.StandardHost/1.0";
/**
* The Java class name of the default Mapper class for this Container.
*/
private String mapperClass =
"org.apache.catalina.core.StandardHostMapper";
/**
* Unpack WARs property.
*/
private boolean unpackWARs = true;
/**
* DefaultContext config
*/
private DefaultContext defaultContext;
// ------------------------------------------------------------- Properties
/**
* Return the application root for this Host. This can be an absolute
* pathname, a relative pathname, or a URL.
*/
public String getAppBase() {
return (this.appBase);
}
/**
* Set the application root for this Host. This can be an absolute
* pathname, a relative pathname, or a URL.
*
* @param appBase The new application root
*/
public void setAppBase(String appBase) {
String oldAppBase = this.appBase;
this.appBase = appBase;
support.firePropertyChange("appBase", oldAppBase, this.appBase);
}
/**
* Return the Java class name of the context configuration class
* for new web applications.
*/
public String getConfigClass() {
return (this.configClass);
}
/**
* Set the Java class name of the context configuration class
* for new web applications.
*
* @param configClass The new context configuration class
*/
public void setConfigClass(String configClass) {
String oldConfigClass = this.configClass;
this.configClass = configClass;
support.firePropertyChange("configClass",
oldConfigClass, this.configClass);
}
/**
* Set the DefaultContext
* for new web applications.
*
* @param defaultContext The new DefaultContext
*/
public void addDefaultContext(DefaultContext defaultContext) {
DefaultContext oldDefaultContext = this.defaultContext;
this.defaultContext = defaultContext;
support.firePropertyChange("defaultContext",
oldDefaultContext, this.defaultContext);
}
/**
* Return the Java class name of the Context implementation class
* for new web applications.
*/
public String getContextClass() {
return (this.contextClass);
}
/**
* Set the Java class name of the Context implementation class
* for new web applications.
*
* @param contextClass The new context implementation class
*/
public void setContextClass(String contextClass) {
String oldContextClass = this.contextClass;
this.contextClass = contextClass;
support.firePropertyChange("contextClass",
oldContextClass, this.contextClass);
}
/**
* Return the default Mapper class name.
*/
public String getMapperClass() {
return (this.mapperClass);
}
/**
* Set the default Mapper class name.
*
* @param mapperClass The new default Mapper class name
*/
public void setMapperClass(String mapperClass) {
String oldMapperClass = this.mapperClass;
this.mapperClass = mapperClass;
support.firePropertyChange("mapperClass",
oldMapperClass, this.mapperClass);
}
/**
* Return the Java class name of the error report valve class
* for new web applications.
*/
public String getErrorReportValveClass() {
return (this.errorReportValveClass);
}
/**
* Set the Java class name of the error report valve class
* for new web applications.
*
* @param errorReportValveClass The new error report valve class
*/
public void setErrorReportValveClass(String errorReportValveClass) {
String oldErrorReportValveClassClass = this.errorReportValveClass;
this.errorReportValveClass = errorReportValveClass;
support.firePropertyChange("errorReportValveClass",
oldErrorReportValveClassClass,
this.errorReportValveClass);
}
/**
* Return the canonical, fully qualified, name of the virtual host
* this Container represents.
*/
public String getName() {
return (name);
}
/**
* Set the canonical, fully qualified, name of the virtual host
* this Container represents.
*
* @param name Virtual host name
*
* @exception IllegalArgumentException if name is null
*/
public void setName(String name) {
if (name == null)
throw new IllegalArgumentException
(sm.getString("standardHost.nullName"));
name = name.toLowerCase(); // Internally all names are lower case
String oldName = this.name;
this.name = name;
support.firePropertyChange("name", oldName, this.name);
}
/**
* Unpack WARs flag accessor.
*/
public boolean isUnpackWARs() {
return (unpackWARs);
}
/**
* Unpack WARs flag mutator.
*/
public void setUnpackWARs(boolean unpackWARs) {
this.unpackWARs = unpackWARs;
}
// --------------------------------------------------------- Public Methods
/**
* Import the DefaultContext config into a web application context.
*
* @param context web application context to import default context
*/
public void importDefaultContext(Context context) {
if( this.defaultContext != null )
this.defaultContext.importDefaultContext(context);
}
/**
* Add an alias name that should be mapped to this same Host.
*
* @param alias The alias to be added
*/
public void addAlias(String alias) {
alias = alias.toLowerCase();
// Skip duplicate aliases
for (int i = 0; i < aliases.length; i++) {
if (aliases[i].equals(alias))
return;
}
// Add this alias to the list
String newAliases[] = new String[aliases.length + 1];
for (int i = 0; i < aliases.length; i++)
newAliases[i] = aliases[i];
newAliases[aliases.length] = alias;
aliases = newAliases;
// Inform interested listeners
fireContainerEvent(ADD_ALIAS_EVENT, alias);
}
/**
* Add a child Container, only if the proposed child is an implementation
* of Context.
*
* @param child Child container to be added
*/
public void addChild(Container child) {
if (!(child instanceof Context))
throw new IllegalArgumentException
(sm.getString("standardHost.notContext"));
super.addChild(child);
}
/**
* Return the set of alias names for this Host. If none are defined,
* a zero length array is returned.
*/
public String[] findAliases() {
return (this.aliases);
}
/**
* Return descriptive information about this Container implementation and
* the corresponding version number, in the format
* <code><description>/<version></code>.
*/
public String getInfo() {
return (info);
}
/**
* Return the Context that would be used to process the specified
* host-relative request URI, if any; otherwise return <code>null</code>.
*
* @param uri Request URI to be mapped
*/
public Context map(String uri) {
if (debug > 0)
log("Mapping request URI '" + uri + "'");
if (uri == null)
return (null);
// Match on the longest possible context path prefix
if (debug > 1)
log(" Trying the longest context path prefix");
Context context = null;
while (true) {
context = (Context) findChild(uri);
if (context != null)
break;
int slash = uri.lastIndexOf('/');
if (slash < 0)
break;
uri = uri.substring(0, slash);
}
// If no Context matches, select the default Context
if (context == null) {
if (debug > 1)
log(" Trying the default context");
context = (Context) findChild("");
}
// Complain if no Context has been selected
if (context == null) {
log(sm.getString("standardHost.mappingError", uri));
return (null);
}
// Return the mapped Context (if any)
if (debug > 0)
log(" Mapped to context '" + context.getPath() + "'");
return (context);
}
/**
* Remove the specified alias name from the aliases for this Host.
*
* @param alias Alias name to be removed
*/
public void removeAlias(String alias) {
alias = alias.toLowerCase();
synchronized (aliases) {
// Make sure this alias is currently present
int n = -1;
for (int i = 0; i < aliases.length; i++) {
if (aliases[i].equals(alias)) {
n = i;
break;
}
}
if (n < 0)
return;
// Remove the specified alias
int j = 0;
String results[] = new String[aliases.length - 1];
for (int i = 0; i < aliases.length; i++) {
if (i != n)
results[j++] = aliases[i];
}
aliases = results;
}
// Inform interested listeners
fireContainerEvent(REMOVE_ALIAS_EVENT, alias);
}
/**
* Return a String representation of this component.
*/
public String toString() {
StringBuffer sb = new StringBuffer();
if (getParent() != null) {
sb.append(getParent().toString());
sb.append(".");
}
sb.append("StandardHost[");
sb.append(getName());
sb.append("]");
return (sb.toString());
}
/**
* Start this host.
*
* @exception IllegalStateException if this component has already been
* started
* @exception LifecycleException if this component detects a fatal error
* that prevents it from being started
*/
public synchronized void start() throws LifecycleException {
// Set error report valve
if ((errorReportValveClass != null)
&& (!errorReportValveClass.equals(""))) {
try {
Valve valve = (Valve) Class.forName(errorReportValveClass)
.newInstance();
addValve(valve);
} catch (Throwable t) {
log(sm.getString
("standardHost.invalidErrorReportValveClass",
errorReportValveClass));
}
}
// Set dispatcher valve
addValve(new ErrorDispatcherValve());
super.start();
}
// ------------------------------------------------------- Deployer Methods
/**
* Install a new web application, whose web application archive is at the
* specified URL, into this container with the specified context path.
* A context path of "" (the empty string) should be used for the root
* application for this container. Otherwise, the context path must
* start with a slash.
* <p>
* If this application is successfully installed, a ContainerEvent of type
* <code>INSTALL_EVENT</code> will be sent to all registered listeners,
* with the newly created <code>Context</code> as an argument.
*
* @param contextPath The context path to which this application should
* be installed (must be unique)
* @param war A URL of type "jar:" that points to a WAR file, or type
* "file:" that points to an unpacked directory structure containing
* the web application to be installed
*
* @exception IllegalArgumentException if the specified context path
* is malformed (it must be "" or start with a slash)
* @exception IllegalArgumentException if the specified context path
* is already attached to an existing web application
* @exception IOException if an input/output error was encountered
* during install
*/
public void install(String contextPath, URL war) throws IOException {
// Validate the format and state of our arguments
if (contextPath == null)
throw new IllegalArgumentException
(sm.getString("standardHost.pathRequired"));
if (!contextPath.equals("") && !contextPath.startsWith("/"))
throw new IllegalArgumentException
(sm.getString("standardHost.pathFormat", contextPath));
if (findDeployedApp(contextPath) != null)
throw new IllegalArgumentException
(sm.getString("standardHost.pathUsed", contextPath));
if (war == null)
throw new IllegalArgumentException
(sm.getString("standardHost.warRequired"));
// Prepare the local variables we will require
String url = war.toString();
String docBase = null;
log(sm.getString("standardHost.installing", contextPath, url));
// Expand a WAR archive into an unpacked directory if needed
if (isUnpackWARs()) {
if (url.startsWith("jar:"))
docBase = expand(war);
else if (url.startsWith("file://"))
docBase = url.substring(7);
else if (url.startsWith("file:"))
docBase = url.substring(5);
else
throw new IllegalArgumentException
(sm.getString("standardHost.warURL", url));
// Make sure the document base directory exists and is readable
File docBaseDir = new File(docBase);
if (!docBaseDir.exists() || !docBaseDir.isDirectory() ||
!docBaseDir.canRead())
throw new IllegalArgumentException
(sm.getString("standardHost.accessBase", docBase));
} else {
if (url.startsWith("jar:")) {
url = url.substring(4, url.length() - 2);
}
if (url.startsWith("file://"))
docBase = url.substring(7);
else if (url.startsWith("file:"))
docBase = url.substring(5);
else
throw new IllegalArgumentException
(sm.getString("standardHost.warURL", url));
}
// Install this new web application
try {
Class clazz = Class.forName(contextClass);
Context context = (Context) clazz.newInstance();
context.setPath(contextPath);
context.setDocBase(docBase);
if (context instanceof Lifecycle) {
clazz = Class.forName(configClass);
LifecycleListener listener =
(LifecycleListener) clazz.newInstance();
((Lifecycle) context).addLifecycleListener(listener);
}
addChild(context);
fireContainerEvent(INSTALL_EVENT, context);
} catch (Exception e) {
log(sm.getString("standardHost.installError", contextPath), e);
throw new IOException(e.toString());
}
}
/**
* Return the Context for the deployed application that is associated
* with the specified context path (if any); otherwise return
* <code>null</code>.
*
* @param contextPath The context path of the requested web application
*/
public Context findDeployedApp(String contextPath) {
if (contextPath == null)
return (null);
synchronized (children) {
return ((Context) children.get(contextPath));
}
}
/**
* Return the context paths of all deployed web applications in this
* Container. If there are no deployed applications, a zero-length
* array is returned.
*/
public String[] findDeployedApps() {
synchronized (children) {
String results[] = new String[children.size()];
return ((String[]) children.keySet().toArray(results));
}
}
/**
* Remove an existing web application, attached to the specified context
* path. If this application is successfully removed, a
* ContainerEvent of type <code>REMOVE_EVENT</code> will be sent to all
* registered listeners, with the removed <code>Context</code> as
* an argument.
*
* @param contextPath The context path of the application to be removed
*
* @exception IllegalArgumentException if the specified context path
* is malformed (it must be "" or start with a slash)
* @exception IllegalArgumentException if the specified context path does
* not identify a currently installed web application
* @exception IOException if an input/output error occurs during
* removal
*/
public void remove(String contextPath) throws IOException {
// Validate the format and state of our arguments
if (contextPath == null)
throw new IllegalArgumentException
(sm.getString("standardHost.pathRequired"));
if (!contextPath.equals("") && !contextPath.startsWith("/"))
throw new IllegalArgumentException
(sm.getString("standardHost.pathFormat", contextPath));
Context context = findDeployedApp(contextPath);
if (context == null)
throw new IllegalArgumentException
(sm.getString("standardHost.pathMissing", contextPath));
// Remove this web application
log(sm.getString("standardHost.removing", contextPath));
try {
removeChild(context);
} catch (Exception e) {
log(sm.getString("standardHost.removeError", contextPath), e);
throw new IOException(e.toString());
}
}
/**
* Start an existing web application, attached to the specified context
* path. Only starts a web application if it is not running.
*
* @param contextPath The context path of the application to be started
*
* @exception IllegalArgumentException if the specified context path
* is malformed (it must be "" or start with a slash)
* @exception IllegalArgumentException if the specified context path does
* not identify a currently installed web application
* @exception IOException if an input/output error occurs during
* startup
*/
public void start(String contextPath) throws IOException {
// Validate the format and state of our arguments
if (contextPath == null)
throw new IllegalArgumentException
(sm.getString("standardHost.pathRequired"));
if (!contextPath.equals("") && !contextPath.startsWith("/"))
throw new IllegalArgumentException
(sm.getString("standardHost.pathFormat", contextPath));
Context context = findDeployedApp(contextPath);
if (context == null)
throw new IllegalArgumentException
(sm.getString("standardHost.pathMissing", contextPath));
log("standardHost.start " + contextPath);
try {
((Lifecycle) context).start();
} catch (LifecycleException e) {
log("standardHost.start " + contextPath + ": ", e);
throw new IllegalStateException
("standardHost.start " + contextPath + ": " + e);
}
}
/**
* Stop an existing web application, attached to the specified context
* path. Only stops a web application if it is running.
*
* @param contextPath The context path of the application to be stopped
*
* @exception IllegalArgumentException if the specified context path
* is malformed (it must be "" or start with a slash)
* @exception IllegalArgumentException if the specified context path does
* not identify a currently installed web application
* @exception IOException if an input/output error occurs while stopping
* the web application
*/
public void stop(String contextPath) throws IOException {
// Validate the format and state of our arguments
if (contextPath == null)
throw new IllegalArgumentException
(sm.getString("standardHost.pathRequired"));
if (!contextPath.equals("") && !contextPath.startsWith("/"))
throw new IllegalArgumentException
(sm.getString("standardHost.pathFormat", contextPath));
Context context = findDeployedApp(contextPath);
if (context == null)
throw new IllegalArgumentException
(sm.getString("standardHost.pathMissing", contextPath));
log("standardHost.stop " + contextPath);
try {
((Lifecycle) context).stop();
} catch (LifecycleException e) {
log("standardHost.stop " + contextPath + ": ", e);
throw new IllegalStateException
("standardHost.stop " + contextPath + ": " + e);
}
}
// ------------------------------------------------------ Protected Methods
/**
* Add a default Mapper implementation if none have been configured
* explicitly.
*
* @param mapperClass Java class name of the default Mapper
*/
protected void addDefaultMapper(String mapperClass) {
super.addDefaultMapper(this.mapperClass);
}
/**
* Expand the WAR file found at the specified URL into an unpacked
* directory structure, and return the absolute pathname to the expanded
* directory.
*
* @param war URL of the web application archive to be expanded
* (must start with "jar:")
*
* @exception IllegalArgumentException if this is not a "jar:" URL
* @exception IOException if an input/output error was encountered
* during expansion
*/
protected String expand(URL war) throws IOException {
// Calculate the directory name of the expanded directory
if (debug >= 1)
log("expand(" + war.toString() + ")");
String pathname = war.toString().replace('\\', '/');
if (pathname.endsWith("!/"))
pathname = pathname.substring(0, pathname.length() - 2);
int period = pathname.lastIndexOf('.');
if (period >= pathname.length() - 4)
pathname = pathname.substring(0, period);
int slash = pathname.lastIndexOf('/');
if (slash >= 0)
pathname = pathname.substring(slash + 1);
if (debug >= 1)
log(" Proposed directory name: " + pathname);
// Make sure that there is no such directory already existing
File appBase = new File(getAppBase());
if (!appBase.isAbsolute())
appBase = new File(System.getProperty("catalina.base"),
getAppBase());
if (!appBase.exists() || !appBase.isDirectory())
throw new IOException
(sm.getString("standardHost.appBase",
appBase.getAbsolutePath()));
File docBase = new File(appBase, pathname);
if (docBase.exists()) {
// War file is already installed
return (docBase.getAbsolutePath());
}
docBase.mkdir();
if (debug >= 2)
log(" Have created expansion directory " +
docBase.getAbsolutePath());
// Expand the WAR into the new document base directory
JarURLConnection juc = (JarURLConnection) war.openConnection();
juc.setUseCaches(false);
JarFile jarFile = juc.getJarFile();
if (debug >= 2)
log(" Have opened JAR file successfully");
Enumeration jarEntries = jarFile.entries();
if (debug >= 2)
log(" Have retrieved entries enumeration");
while (jarEntries.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) jarEntries.nextElement();
String name = jarEntry.getName();
if (debug >= 2)
log(" Am processing entry " + name);
int last = name.lastIndexOf('/');
if (last >= 0) {
File parent = new File(docBase,
name.substring(0, last));
if (debug >= 2)
log(" Creating parent directory " + parent);
parent.mkdirs();
}
if (name.endsWith("/"))
continue;
if (debug >= 2)
log(" Creating expanded file " + name);
InputStream input = jarFile.getInputStream(jarEntry);
expand(input, docBase, name);
input.close();
}
jarFile.close();
// Return the absolute path to our new document base directory
return (docBase.getAbsolutePath());
}
/**
* Expand the specified input stream into the specified directory, creating
* a file named from the specified relative path.
*
* @param input InputStream to be copied
* @param docBase Document base directory into which we are expanding
* @param name Relative pathname of the file to be created
*
* @exception IOException if an input/output error occurs
*/
protected void expand(InputStream input, File docBase, String name)
throws IOException {
File file = new File(docBase, name);
BufferedOutputStream output =
new BufferedOutputStream(new FileOutputStream(file));
byte buffer[] = new byte[2048];
while (true) {
int n = input.read(buffer);
if (n <= 0)
break;
output.write(buffer, 0, n);
}
output.close();
}
/**
* Remove the specified directory and all of its contents.
*
* @param dir Directory to be removed
*
* @exception IOException if an input/output error occurs
*/
protected void remove(File dir) throws IOException {
String list[] = dir.list();
for (int i = 0; i < list.length; i++) {
File file = new File(dir, list[i]);
if (file.isDirectory()) {
remove(file);
} else {
if (!file.delete())
throw new IOException("Cannot delete file " +
file.getAbsolutePath());
}
}
if (!dir.delete())
throw new IOException("Cannot delete directory " +
dir.getAbsolutePath());
}
}