/*
* Copyright 2003-2006 Rick Knowles <winstone-devel at lists sourceforge net>
* Distributed under the terms of either:
* - the common development and distribution license (CDDL), v1.0; or
* - the GNU Lesser General Public License, v2.1 or later
*/
package winstone;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.RequestLog;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.RequestLogHandler;
import org.eclipse.jetty.webapp.WebAppContext;
import winstone.cmdline.Option;
import javax.servlet.SessionTrackingMode;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* Manages the references to individual webapps within the container. This object handles
* the mapping of url-prefixes to webapps, and init and shutdown of any webapps it manages.
*
* @author <a href="mailto:rick_knowles@hotmail.com">Rick Knowles</a>
* @version $Id: HostConfiguration.java,v 1.8 2007/08/02 06:16:00 rickknowles Exp $
*/
public class HostConfiguration {
private final Server server;
private String hostname;
private Map args;
private Map<String,WebAppContext> webapps;
private ClassLoader commonLibCL;
private File commonLibCLPaths[];
private MimeTypes mimeTypes = new MimeTypes();
private final LoginService loginService;
public HostConfiguration(Server server, String hostname, ClassLoader commonLibCL,
File commonLibCLPaths[], Map args, File webappsDir) throws IOException {
this.server = server;
this.hostname = hostname;
this.args = args;
this.webapps = new Hashtable<String,WebAppContext>();
this.commonLibCL = commonLibCL;
this.commonLibCLPaths = commonLibCLPaths;
try {
// Build the realm
Class realmClass = Option.REALM_CLASS_NAME.get(args, LoginService.class, commonLibCL);
Constructor realmConstr = realmClass.getConstructor(Map.class);
loginService = (LoginService) realmConstr.newInstance(args);
} catch (Throwable err) {
throw (IOException)new IOException("Failed to setup authentication realm").initCause(err);
}
// Is this the single or multiple configuration ? Check args
File warfile = Option.WARFILE.get(args);
File webroot = Option.WEBROOT.get(args);
Handler handler;
// If single-webapp mode
if (webappsDir == null && ((warfile != null) || (webroot != null))) {
String prefix = Option.PREFIX.get(args);
if (prefix.endsWith("/")) // trim off the trailing '/' that Jetty doesn't like
prefix = prefix.substring(0,prefix.length()-1);
handler = configureAccessLog(create(getWebRoot(webroot,warfile), prefix),"webapp");
}
// Otherwise multi-webapp mode
else {
handler = initMultiWebappDir(webappsDir);
}
{// load additional mime types
loadBuiltinMimeTypes();
String types = Option.MIME_TYPES.get(args);
if (types!=null) {
StringTokenizer mappingST = new StringTokenizer(types, ":", false);
for (; mappingST.hasMoreTokens(); ) {
String mapping = mappingST.nextToken();
int delimPos = mapping.indexOf('=');
if (delimPos == -1)
continue;
String extension = mapping.substring(0, delimPos);
String mimeType = mapping.substring(delimPos + 1);
this.mimeTypes.addMimeMapping(extension.toLowerCase(), mimeType);
}
}
}
server.setHandler(handler);
Logger.log(Logger.DEBUG, Launcher.RESOURCES, "HostConfig.InitComplete",
this.webapps.size() + "", this.webapps.keySet() + "");
}
private void loadBuiltinMimeTypes() {
InputStream in = getClass().getResourceAsStream("mime.properties");
try {
Properties props = new Properties();
props.load(in);
for (Entry<Object, Object> e : props.entrySet()) {
mimeTypes.addMimeMapping(e.getKey().toString(),e.getValue().toString());
}
} catch (IOException e) {
throw new Error("Failed to load the built-in MIME types",e);
} finally {
try {
in.close();
} catch (IOException e) {
// ignore
}
}
}
/**
* @param webAppName
* Unique name given to the access logger.
*/
private Handler configureAccessLog(Handler handler, String webAppName) {
try {
Class loggerClass = Option.ACCESS_LOGGER_CLASSNAME.get(args, RequestLog.class, commonLibCL);
if (loggerClass!=null) {
// Build the realm
Constructor loggerConstr = loggerClass.getConstructor(String.class, Map.class);
RequestLogHandler rlh = new RequestLogHandler();
rlh.setHandler(handler);
rlh.setRequestLog((RequestLog) loggerConstr.newInstance(webAppName, args));
return rlh;
} else {
Logger.log(Logger.DEBUG, Launcher.RESOURCES, "WebAppConfig.LoggerDisabled");
}
} catch (Throwable err) {
Logger.log(Logger.ERROR, Launcher.RESOURCES,
"WebAppConfig.LoggerError", "", err);
}
return handler;
}
private WebAppContext create(File app, String prefix) {
WebAppContext wac = new WebAppContext(app.getAbsolutePath(),prefix) {
@Override
public void preConfigure() throws Exception {
// to have WebAppClassLoader inherit from commonLibCL,
// we need to set it as the context classloader.
Thread t = Thread.currentThread();
ClassLoader ccl = t.getContextClassLoader();
t.setContextClassLoader(commonLibCL);
try {
super.preConfigure();
} finally {
t.setContextClassLoader(ccl);
}
}
@Override
public void postConfigure() throws Exception {
super.postConfigure();
// if specified, override the value in web.xml
int sessionTimeout = Option.SESSION_TIMEOUT.get(args);
if (sessionTimeout>0)
getSessionHandler().setMaxInactiveInterval(sessionTimeout * 60);
}
};
wac.getSecurityHandler().setLoginService(loginService);
wac.setThrowUnavailableOnStartupException(true); // if boot fails, abort the process instead of letting empty Jetty run
wac.setMimeTypes(mimeTypes);
wac.getSessionHandler().setSessionTrackingModes(Collections.singleton(SessionTrackingMode.COOKIE));
wac.getSessionHandler().setSessionCookie(WinstoneSession.SESSION_COOKIE_NAME);
this.webapps.put(wac.getContextPath(),wac);
return wac;
}
public String getHostname() {
return this.hostname;
}
public void reloadWebApp(String prefix) {
WebAppContext webApp = this.webapps.get(prefix);
if (webApp != null) {
try {
webApp.stop();
webApp.start();
} catch (Exception e) {
throw new WinstoneException("Failed to redeploy "+prefix,e);
}
} else {
throw new WinstoneException(Launcher.RESOURCES.getString("HostConfig.PrefixUnknown", prefix));
}
}
/**
* Setup the webroot. If a warfile is supplied, extract any files that the
* war file is newer than. If none is supplied, use the default temp
* directory.
*/
protected File getWebRoot(File requestedWebroot, File warfile) throws IOException {
if (warfile != null) {
Logger.log(Logger.INFO, Launcher.RESOURCES,
"HostConfig.BeginningWarExtraction");
// open the war file
if (!warfile.exists() || !warfile.isFile())
throw new WinstoneException(Launcher.RESOURCES.getString(
"HostConfig.WarFileInvalid", warfile));
// Get the webroot folder (or a temp dir if none supplied)
File unzippedDir;
if (requestedWebroot != null) {
unzippedDir = requestedWebroot;
} else {
File tempFile = File.createTempFile("dummy", "dummy");
String userName = System.getProperty("user.name");
unzippedDir = new File(tempFile.getParent(),
(userName != null ? WinstoneResourceBundle.globalReplace(userName,
new String[][] {{"/", ""}, {"\\", ""}, {",", ""}}) + "/" : "") +
"winstone/" + warfile.getName());
tempFile.delete();
}
if (unzippedDir.exists()) {
if (!unzippedDir.isDirectory()) {
throw new WinstoneException(Launcher.RESOURCES.getString(
"HostConfig.WebRootNotDirectory", unzippedDir.getPath()));
} else {
Logger.log(Logger.DEBUG, Launcher.RESOURCES,
"HostConfig.WebRootExists", unzippedDir.getCanonicalPath());
}
}
// check consistency and if out-of-sync, recreate
File timestampFile = new File(unzippedDir,".timestamp");
if(!timestampFile.exists() || Math.abs(timestampFile.lastModified()- warfile.lastModified())>1000) {
// contents of the target directory is inconsistent from the war.
deleteRecursive(unzippedDir);
unzippedDir.mkdirs();
} else {
// files are up to date
return unzippedDir;
}
// Iterate through the files
byte buffer[] = new byte[8192];
JarFile warArchive = new JarFile(warfile);
for (Enumeration e = warArchive.entries(); e.hasMoreElements();) {
JarEntry element = (JarEntry) e.nextElement();
if (element.isDirectory()) {
continue;
}
String elemName = element.getName();
// If archive date is newer than unzipped file, overwrite
File outFile = new File(unzippedDir, elemName);
if (outFile.exists() && (outFile.lastModified() > warfile.lastModified())) {
continue;
}
outFile.getParentFile().mkdirs();
// Copy out the extracted file
InputStream inContent = warArchive.getInputStream(element);
OutputStream outStream = new FileOutputStream(outFile);
int readBytes = inContent.read(buffer);
while (readBytes != -1) {
outStream.write(buffer, 0, readBytes);
readBytes = inContent.read(buffer);
}
inContent.close();
outStream.close();
}
// extraction completed
new FileOutputStream(timestampFile).close();
timestampFile.setLastModified(warfile.lastModified());
// Return webroot
return unzippedDir;
} else {
return requestedWebroot;
}
}
private void deleteRecursive(File dir) {
File[] children = dir.listFiles();
if(children!=null) {
for (File child : children) {
deleteRecursive(child);
}
}
dir.delete();
}
protected ContextHandlerCollection initMultiWebappDir(File webappsDir) {
ContextHandlerCollection webApps = new ContextHandlerCollection();
if (webappsDir == null) {
webappsDir = new File("webapps");
}
if (!webappsDir.exists()) {
throw new WinstoneException(Launcher.RESOURCES.getString("HostConfig.WebAppDirNotFound", webappsDir.getPath()));
} else if (!webappsDir.isDirectory()) {
throw new WinstoneException(Launcher.RESOURCES.getString("HostConfig.WebAppDirIsNotDirectory", webappsDir.getPath()));
} else {
File children[] = webappsDir.listFiles();
for (File aChildren : children) {
String childName = aChildren.getName();
// Check any directories for warfiles that match, and skip: only deploy the war file
if (aChildren.isDirectory()) {
File matchingWarFile = new File(webappsDir, aChildren.getName() + ".war");
if (matchingWarFile.exists() && matchingWarFile.isFile()) {
Logger.log(Logger.DEBUG, Launcher.RESOURCES, "HostConfig.SkippingWarfileDir", childName);
} else {
String prefix = childName.equalsIgnoreCase("ROOT") ? "" : "/" + childName;
if (!this.webapps.containsKey(prefix)) {
try {
WebAppContext context = create(aChildren, prefix);
webApps.addHandler(configureAccessLog(context,childName));
Logger.log(Logger.INFO, Launcher.RESOURCES, "HostConfig.DeployingWebapp", childName);
} catch (Throwable err) {
Logger.log(Logger.ERROR, Launcher.RESOURCES, "HostConfig.WebappInitError", prefix, err);
}
}
}
} else if (childName.endsWith(".war")) {
String outputName = childName.substring(0, childName.lastIndexOf(".war"));
String prefix = outputName.equalsIgnoreCase("ROOT") ? "" : "/" + outputName;
if (!this.webapps.containsKey(prefix)) {
File outputDir = new File(webappsDir, outputName);
outputDir.mkdirs();
try {
WebAppContext context = create(
getWebRoot(new File(webappsDir, outputName), aChildren), prefix);
webApps.addHandler(configureAccessLog(context,outputName));
Logger.log(Logger.INFO, Launcher.RESOURCES, "HostConfig.DeployingWebapp", childName);
} catch (Throwable err) {
Logger.log(Logger.ERROR, Launcher.RESOURCES, "HostConfig.WebappInitError", prefix, err);
}
}
}
}
}
return webApps;
}
}