/*
* @(#)$Id: ExtConfigListener.java 131 2008-10-31 17:09:46Z unsaved $
*
* Copyright 2008 by the JWebMail Development Team and Sebastian Schaffert.
*
* Licensed 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 net.wastl.webmail.config;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.naming.InitialContext;
import javax.naming.NameNotFoundException;
import javax.naming.NamingException;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import net.wastl.webmail.misc.ExpandableProperties;
import net.wastl.webmail.misc.Helper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Purpose
* <UL>
* <LI>Set webapp attribute app.contextpath to the app's unique runtime Context
* Path. This will be unique even for multiple deployments of the same
* application distro.
* <LI>Set webapp attribute deployment.name to a String base on the context path
* <LI>Set webapp attribute rtconfig.dir to the absolute path of a runtime
* config directory. Value is determined dynamically at runtime, based on the
* Runtime environment. The directory is specific to this deployment instance of
* this web app. To keep configs independent of the distributable app., the
* designated directory should be external to the application.
* <LI>Load a runtime Properties object from file "meta.properties" in the
* rtconfig.dir directory described above, and write the Properties object to a
* webapp attribute so the properties will be available to the app.
* <LI>In addition to primary purposes, also automatically sets Java System
* property 'webapps.rtconfig.dir'.
* </UL>
* <P>
* The System Property SHOULD NOT be application-specific or
* app-instance-specific if the app is to remain portable, since some app
* servers share one set of System Properties for all web app instances.
* </P>
* <P>
* The property contextPath or application attribute 'context.path' satisfies
* the need for application-specific switching. Example config files with
* webapps.rtconfig.dir set to '/local/configs'
* <UL>
* <LI>/local/configs/appa/meta.properties
* <LI>/local/configs/appc/meta.properties
* <LI>/local/configs/appd/meta.properties
* </UL>
* webapps.rtconfig.dir defaults to <CODE>${user.home}</CODE>. Since the app
* also has access to the rt.configdir value, you can put any and all kinds of
* runtime resources alongside the meta.properties file.
* <P>
* The variables ${rt.configdir} and ${app.contextpath} will be expanded if they
* occur inside a meta.properties file. The latter allows for safely specifying
* other files alongside the meta.properties file without worrying about the
* vicissitudes of relative paths.
* </P>
* <P>
* One would think that the running app could easily detect its own runtime
* context path, but alas, that's impossible to do in a portable way (until
* after requests are being served... and that is too late).
* </P>
*
* @author blaine.simpson@admc.com
*/
public class ExtConfigListener implements ServletContextListener {
/*
* It's very difficult to choose between camelBack and dot.delimited keys
* for attributes. dot.delimited is much more elegant on the configuration
* side, in .properties and XML files, but these dots break the ability for
* JavaBean tools and utilities to dereference (e.g. EL, JSTL, Spring).
* Also, can't have a getter or setter with a dot in it. Due to the
* convenience factor, going with dot-delimited until and if this causes us
* problems.
*/
private static Log log = LogFactory.getLog(ExtConfigListener.class);
/** Corresponds to the context.path setting. */
protected String contextPath = null;
/** Derived from contextPath. */
protected String deploymentName = null;
protected File lockFile = null;
public void contextInitialized(ServletContextEvent sce) {
Helper.logThreads("Top of ExtCongigListener.contextInitialized()");
final ServletContext sc = sce.getServletContext();
contextPath = sc.getInitParameter("default.contextpath");
try {
final Object o =
new InitialContext().lookup("java:comp/env/app.contextpath");
contextPath = (String) o;
log.debug("app.contextpath set by webapp env property");
} catch (final NameNotFoundException nnfe) {
} catch (final NamingException nnfe) {
log.fatal("Runtime failure when looking up env property", nnfe);
throw new RuntimeException(
"Runtime failure when looking up env property", nnfe);
}
if (contextPath == null) {
log.fatal("Required setting 'app.contextpath' is not set as either "
+ "a app webapp JNDI env param, nor by default context "
+ "init parameter 'default.contextpath'");
throw new IllegalStateException(
"Required setting 'app.contextpath' is not set as either "
+ "a app webapp JNDI env param, nor by default context "
+ "init parameter 'default.contextpath'");
}
if (contextPath.equals("/ROOT")) {
log.fatal("Refusing to use context path of '/ROOT' to avoid "
+ "ambiguity with default context path");
throw new IllegalStateException(
"Refusing to use context path of '/ROOT' to avoid "
+ "ambiguity with default context path");
}
deploymentName = generateDeploymentName();
log.info("Initializing configs for runtime deployment name '"
+ deploymentName + "'");
String dirProp = System.getProperty("webapps.rtconfig.dir");
if (dirProp == null) {
dirProp = System.getProperty("user.home");
System.setProperty("webapps.rtconfig.dir", dirProp);
}
final File rtConfigDir = new File(dirProp, deploymentName);
final File metaFile = new File(rtConfigDir, "meta.properties");
lockFile = new File(rtConfigDir, "lock.txt");
if (lockFile.exists()) {
log.fatal("Presence of lock file '"
+ lockFile.getAbsolutePath()
+ "' indicates the instance is already running");
lockFile = null;
throw new IllegalStateException("Presence of lock file "
+ "indicates the instance is already running");
}
// From this point on, we know that:
// IF LOCK FILE EXISTS, we have created it and all is well
// IF LOCK FILE DOES NOT EXIST, we need to create it ASAP
if (rtConfigDir.isDirectory()) {
mkLockFile();
}
// We create lock file as early as possible.
// If we can't make it here, it will be created in installXmlStorage.
if (!rtConfigDir.isDirectory() || !metaFile.isFile()) {
try {
installXmlStorage(rtConfigDir, metaFile);
log.warn("New XML storage system successfully loaded. "
+ "Metadata file '" + metaFile.getAbsolutePath() + "'");
} catch (final IOException e) {
log.fatal("Failed to set up a new XML storage system", e);
throw new IllegalStateException(
"Failed to set up a new XML storage system", e);
}
}
if (!lockFile.exists()) {
// Being extra safe
log.fatal("Assertion failed. Internal locking error in "
+ getClass().getName() + '.');
lockFile = null;
throw new IllegalStateException(
"Assertion failed. Internal locking error in "
+ getClass().getName() + '.');
}
final ExpandableProperties metaProperties = new ExpandableProperties();
try {
metaProperties.load(new FileInputStream(metaFile));
} catch (final IOException ioe) {
log.fatal("Failed to read meta props file '"
+ metaFile.getAbsolutePath() + "'", ioe);
throw new IllegalStateException("Failed to read meta props file '"
+ metaFile.getAbsolutePath() + "'", ioe);
}
final Properties expandProps = new Properties();
expandProps.setProperty("rtconfig.dir", rtConfigDir.getAbsolutePath());
expandProps.setProperty("app.contextpath", contextPath);
expandProps.setProperty("deployment.name", deploymentName);
try {
metaProperties.expand(expandProps); // Expand ${} properties
} catch (final Throwable t) {
log.fatal("Failed to expand properties in meta file '"
+ metaFile.getAbsolutePath() + "'", t);
throw new IllegalStateException(
"Failed to expand properties in meta file '"
+ metaFile.getAbsolutePath() + "'", t);
}
String requiredKeysString;
requiredKeysString = sc.getInitParameter("required.metaprop.keys");
if (requiredKeysString != null) {
final Set<String> requiredKeys = new HashSet<String>(
Arrays.asList(requiredKeysString.split("\\s*,\\s*", -1)));
requiredKeys.removeAll(metaProperties.keySet());
if (requiredKeys.size() > 0) {
log.fatal("Meta properties file '" + metaFile.getAbsolutePath()
+ "' missing required property(s): " + requiredKeys);
throw new IllegalStateException("Meta properties file '"
+ metaFile.getAbsolutePath()
+ "' missing required property(s): " + requiredKeys);
}
}
sc.setAttribute("app.contextpath", contextPath);
sc.setAttribute("deployment.name", deploymentName);
sc.setAttribute("rtconfig.dir", rtConfigDir);
sc.setAttribute("meta.properties", metaProperties);
log.debug("'app.contextpath', 'rtconfig.dir', 'meta.properties' "
+ "successfully published to app context for "
+ deploymentName);
}
public void contextDestroyed(ServletContextEvent sce) {
log.info("App '" + deploymentName + "' shutting down.\n"
+ "All Servlets and Filters have been destroyed");
if (lockFile != null) {
if (lockFile.delete()) {
// In my experience, this return status is unreliable.
log.info("Lock file '" + lockFile.getAbsolutePath()
+ "' removed");
} else {
log.error("Failed to remove lock file '"
+ lockFile.getAbsolutePath() + "' removed");
}
}
}
/**
* @param baseDir
* Parent directory of metaFile
* @param metaFile
* Properties file to be created. IT CAN NOT EXIST YET!
* @throws IOException
* if fail to create new XML Storage system
*/
protected void installXmlStorage(File baseDir, File metaFile)
throws IOException {
log.warn("Will attempt install a brand new data store");
final File dataDir = new File(baseDir, "data");
if (dataDir.exists())
throw new IOException("Target data path dir already exists: "
+ dataDir.getAbsolutePath());
if (!baseDir.isDirectory()) {
final File parentDir = baseDir.getParentFile();
if (!parentDir.canWrite())
throw new IOException("Cannot create base RT directory '"
+ baseDir.getAbsolutePath() + "'");
if (!baseDir.mkdir())
throw new IOException("Failed to create base RT directory '"
+ baseDir.getAbsolutePath() + "'");
log.debug(
"Created base RT dir '" + baseDir.getAbsolutePath() + "'");
mkLockFile();
}
if (!baseDir.canWrite()) throw new IOException(
"Do not have privilegest to create meta file '"
+ metaFile.getAbsolutePath() + "'");
if (!dataDir.mkdir())
throw new IOException("Failed to create data directory '"
+ dataDir.getAbsolutePath() + "'");
log.debug("Created data dir '" + dataDir.getAbsolutePath() + "'");
// In my experience, you can't trust the return values of the
// File.mkdir() method. But the file creations or extractions
// wild fail below in that case, so that's no problem.
// Could create a Properties object and save it, but why?
final PrintWriter pw = new PrintWriter(new FileWriter(metaFile));
try {
pw.println("webmail.data.path: ${rtconfig.dir}/data");
pw.println("webmail.mimetypes.filepath: "
+ "${rtconfig.dir}/mimetypes.txt");
pw.flush();
} finally {
pw.close();
}
final InputStream zipFileStream =
getClass().getResourceAsStream("/data.zip");
if (zipFileStream == null) throw new IOException(
"Zip file 'data.zip' missing from web application");
final InputStream mimeInStream =
getClass().getResourceAsStream("/mimetypes.txt");
if (mimeInStream == null) throw new IOException(
"Mime-types file 'mimetypes.txt' missing from web application");
ZipEntry entry;
File newNode;
FileOutputStream fileStream;
long fileSize, bytesRead;
int i;
final byte[] buffer = new byte[10240];
final FileOutputStream mimeOutStream =
new FileOutputStream(new File(baseDir, "mimetypes.txt"));
try {
while ((i = mimeInStream.read(buffer)) > 0) {
mimeOutStream.write(buffer, 0, i);
}
mimeOutStream.flush();
} finally {
mimeOutStream.close();
}
log.debug("Extracted mime types file");
final ZipInputStream zipStream = new ZipInputStream(zipFileStream);
try {
while ((entry = zipStream.getNextEntry()) != null) {
newNode = new File(dataDir, entry.getName());
if (entry.isDirectory()) {
if (!newNode.mkdir())
throw new IOException("Failed to extract dir '"
+ entry.getName() + "' from 'data.zip' file");
log.debug("Extracted dir '" + entry.getName() + "' to '"
+ newNode.getAbsolutePath() + "'");
zipStream.closeEntry();
continue;
}
fileSize = entry.getSize();
fileStream = new FileOutputStream(newNode);
try {
bytesRead = 0;
while ((i = zipStream.read(buffer)) > 0) {
fileStream.write(buffer, 0, i);
bytesRead += i;
}
fileStream.flush();
} finally {
fileStream.close();
}
zipStream.closeEntry();
if (bytesRead != fileSize)
throw new IOException("Expected " + fileSize
+ " bytes for '" + entry.getName()
+ ", but extracted " + bytesRead + " bytes to '"
+ newNode.getAbsolutePath() + "'");
log.debug("Extracted file '" + entry.getName() + "' to '"
+ newNode.getAbsolutePath() + "'");
}
} finally {
zipStream.close();
}
}
static Pattern cpPattern = Pattern.compile("/(\\w+)$");
protected String generateDeploymentName() {
if (contextPath == null) return null;
if (contextPath.length() == 0) return "ROOT";
final Matcher m = cpPattern.matcher(contextPath);
if (m.matches()) return m.group(1);
log.error("Malformatted context path '" + contextPath + "'");
return null;
}
protected void mkLockFile() {
if (lockFile.exists()) throw new IllegalStateException(
"Attempting to create Lock file, but it already exists");
PrintWriter pw = null;
try {
pw = new PrintWriter(new FileWriter(lockFile));
pw.println(deploymentName + " started at " + new java.util.Date());
pw.flush();
} catch (final IOException ioe) {
log.fatal("Failed to write lock file '"
+ lockFile.getAbsolutePath() + "'", ioe);
throw new IllegalStateException("Failed to write lock file '"
+ lockFile.getAbsolutePath() + "'", ioe);
} finally {
if (pw != null) {
pw.close();
}
}
}
}