/*==========================================================================*\ | $Id: WCServletAdaptor.java,v 1.19 2011/06/10 00:08:20 aallowat Exp $ |*-------------------------------------------------------------------------*| | Copyright (C) 2006-2008 Virginia Tech | | This file is part of Web-CAT. | | Web-CAT is free software; you can redistribute it and/or modify | it under the terms of the GNU Affero General Public License as published | by the Free Software Foundation; either version 3 of the License, or | (at your option) any later version. | | Web-CAT is distributed in the hope that it will be useful, | but WITHOUT ANY WARRANTY; without even the implied warranty of | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | GNU General Public License for more details. | | You should have received a copy of the GNU Affero General Public License | along with Web-CAT; if not, see <http://www.gnu.org/licenses/>. \*==========================================================================*/ package net.sf.webcat; import java.io.*; import java.lang.reflect.Method; import java.util.*; import java.util.zip.*; import javax.servlet.ServletException; import javax.servlet.http.*; import net.sf.webcat.FileUtilities; import net.sf.webcat.SubsystemUpdater; import net.sf.webcat.WCServletContext; import net.sf.webcat.WCUpdater; // ------------------------------------------------------------------------- /** * This is a custom subclass of com.webobjects.jspservlet.WOServletAdaptor. * It adds transparent capabilities for self-updating Java subsystems * within Web-CAT, before the application starts up. * * @author stedwar2 * @author Last changed by $Author: aallowat $ * @version $Revision: 1.19 $, $Date: 2011/06/10 00:08:20 $ */ @SuppressWarnings("serial") public class WCServletAdaptor extends com.webobjects.jspservlet.WOServletAdaptor { //~ Constructors .......................................................... // ---------------------------------------------------------- /** * Creates a new adaptor object. * @throws ServletException */ public WCServletAdaptor() throws ServletException { super(); instance = this; } // ---------------------------------------------------------- /** * Get access to the current adaptor object. * @return a reference to the most recently created adaptor, or null * if there isn't one */ public static WCServletAdaptor getInstance() { return instance; } //~ Public Methods ........................................................ // ---------------------------------------------------------- /** * Start up the servlet--this method contains the heavy-weight one-time * actions at application startup, including updating any necessary * subsystems before the main application class is loaded or the * classpath is completely set up. * @throws ServletException */ public void init() throws ServletException { // Cache the additional HTTP methods that we need to handle (for // WebDAV support). additionalHttpMethods = new HashSet<String>(); additionalHttpMethods.add("COPY"); additionalHttpMethods.add("LOCK"); additionalHttpMethods.add("MKCOL"); additionalHttpMethods.add("MOVE"); additionalHttpMethods.add("PROPFIND"); additionalHttpMethods.add("PROPPATCH"); additionalHttpMethods.add("UNLOCK"); String webInfRoot = super.getServletContext().getRealPath("WEB-INF"); File webInfDir = new File(webInfRoot); propertiesFile = new File(webInfDir, "update.properties"); loadProperties(); systemUpdater = WCUpdater.getInstance(); systemUpdater.setup(webInfDir); updateDir = systemUpdater.getUpdateDir(); frameworkDir = systemUpdater.getFrameworkDir(); applyNecessaryUpdates(webInfDir); try { super.init(); } catch (NoClassDefFoundError e) { initFailed = e; // for (StackTraceElement frame : e.getStackTrace()) // { // String fileName = frame.getFileName(); // if (fileName != null && fileName.toLowerCase().contains("gcj")) // { // gcjDetected = true; // break; // } // } } catch (javax.servlet.UnavailableException e) { // Failure during startup initFailed = e; } //Run background update process if(willUpdateAutomatically()) { //Hard coded values currently: Update occurs every 4 minutes //Most likly change to read in a property containing update checks systemUpdater.startBackgroundUpdaterThread(1000, 240000); } } // ---------------------------------------------------------- /** * Access the servlet context object for this servlet. This is a wrapper * around the superclass default implementation that returns a * {@link WCServletContext} object wrapped around the real servlet * context, to provide access to a customized classpath via * {@link WCServletContext#getInitParameter(String)}. * @return the context object * @see javax.servlet.GenericServlet#getServletContext() */ public javax.servlet.ServletContext getServletContext() { javax.servlet.ServletContext result = super.getServletContext(); if (result != null) { if (result != innerContext) { wrappedContext = new WCServletContext(result, woClasspath); } innerContext = result; result = wrappedContext; } return result; } // ---------------------------------------------------------- public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { if (initFailed != null) { sendExceptionNotice(response); } else { super.doGet(request, response); } } // ---------------------------------------------------------- public void doPut(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { doGet(request, response); } // ---------------------------------------------------------- public void doDelete(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { doGet(request, response); } // ---------------------------------------------------------- public void doOptions(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { doGet(request, response); } // ---------------------------------------------------------- public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { if (additionalHttpMethods.contains(request.getMethod())) { doGet(request, response); } else { super.service(request, response); } } // ---------------------------------------------------------- /** * Determine if this adaptor will attempt to automatically update all * subsystems on start-up. * @return true if subsystems will be automatically updated */ public boolean willUpdateAutomatically() { String val = properties.getProperty("updateAutomatically", "1") .toLowerCase(); return val.equals("1") || val.equals("true") || val.equals("on") || val.equals("yes"); } // ---------------------------------------------------------- /** * Set whether or not this adaptor will attempt to automatically update * all subsystems on start-up. * @param value true if this adaptor will auto-update subsystems */ public void setWillUpdateAutomatically(boolean value) { properties.setProperty("updateAutomatically", value ? "1" : "0"); commitProperties(); } // ---------------------------------------------------------- /** * Access the collection of subsystems in this application. * @return a collection of {@link SubsystemUpdater} objects representing * the available subsystems */ public Collection<SubsystemUpdater> subsystems() { return systemUpdater.subsystems(); } // ---------------------------------------------------------- /** * Get a file representing the directory where downloaded updates should * be placed. * @return a collection of {@link SubsystemUpdater} objects representing * the available subsystems */ public File updateDownloadLocation() { return updateDir; } // ---------------------------------------------------------- /** * Get the version for the Bootstrap build containing this class. * @return the version number as a string */ public String version() { return VERSION; } //~ Private Methods ....................................................... // ---------------------------------------------------------- /** * Build an exception notification message as the designated http response. * @param response the http response being generated * @throws IOException if one arises while writing the response */ private void sendExceptionNotice(HttpServletResponse response) throws IOException { response.setContentType("text/html"); PrintWriter out = new PrintWriter(response.getOutputStream()); out.println("<html><head>"); out.println("<title>Web-CAT Startup Failure</title>"); out.println("</head><body>"); out.println("<h1>Web-CAT Startup Failure</h1>"); out.println("<p>Web-CAT threw an unexpected exception during "); out.println("initialization. Please shut down the web application "); out.println("and fix the problem.</p>"); String vmName = System.getProperty("java.vm.name"); if (vmName != null && vmName.toLowerCase().contains("gcj")) { out.println("<p>In this case, it appears that you are <b>using"); out.println("gcj</b> to run your servlet container. Web-CAT "); out.println("does not run properly under gcj at this time."); out.println("Please install Sun's JDK and"); out.println("<a href=\"http://web-cat.org/WCWiki/"); out.println("SwitchToSunJdk\">configure your servlet"); out.println("container</a> to use it instead.</p>"); } out.println("<p>For more information, locate your "); out.println("servlet container's <b>stdout log file</b> and "); out.println("examine it to identify the exception stack trace.</p>"); out.println("<p>For assistance, send the stdout log file to "); out.println("the Web-CAT project team at "); out.println("<a href=\"mailto:webcat@vt.edu\">webcat@vt.edu</a>.</p>"); Throwable nested = initFailed; while (nested.getCause() != null) { nested = nested.getCause(); } out.println("<h2>Root Cause</h2>\n<pre>"); nested.printStackTrace(out); out.println("</pre>"); if (nested != initFailed) { out.println("<h2>Full Exception Details</h2>\n<pre>"); initFailed.printStackTrace(out); nested = initFailed.getCause(); while (nested != null) { out.println("\nCaused by:"); nested.printStackTrace(out); nested = nested.getCause(); } out.println("</pre>"); } out.println("</body></html>"); out.flush(); out.close(); } // ---------------------------------------------------------- /** * Apply any necessary updates and compute the woClasspath value. * @param webInfDir the WEB-INF directory as a file object */ private void applyNecessaryUpdates(File webInfDir) { File appDir = webInfDir.getParentFile(); applyPendingUpdates(frameworkDir, appDir); if (frameworkDir != null && frameworkDir.isDirectory()) { File[] subdirs = frameworkDir.listFiles(); java.util.Arrays.sort(subdirs, new FrameworkComparator()); woClasspath = classPathFrom(subdirs, systemUpdater.getMainBundle()); System.out.println("Dynamically computed classpath:"); System.out.print(woClasspath); } } // ---------------------------------------------------------- /** * Scan the update directory for any downloaded update files, apply * them, and then delete them. * @param aFrameworkDir The place to unpack updates * @param appDir The application directory, which is where any top-level * updates (e.g., "webcat_*" files) are unpacked */ private void applyPendingUpdates(File aFrameworkDir, File appDir) { if (updateDir.exists() && updateDir.isDirectory()) { for (File jar : updateDir.listFiles()) { for (String extension : SubsystemUpdater.JAVA_ARCHIVE_EXTENSIONS) { if (jar.getName().endsWith(extension)) { File unpackDir = aFrameworkDir; if (jar.getName().startsWith(APP_JAR_PREFIX)) { unpackDir = appDir; } try { prepareUnpackingDir(unpackDir, jar); System.out.println("Applying update from " + jar.getName()); ZipFile zipFile = new ZipFile(jar); FileUtilities.unZip(zipFile, unpackDir); zipFile.close(); if (!jar.delete()) { System.out.println( "WCServletAdaptor: ERROR: unable to delete " + jar.getAbsolutePath()); } } catch (java.io.IOException e) { System.out.println("WCServletAdaptor: ERROR: " + "unpacking update bundle: " + e); System.out.println("on file: " + jar.getAbsolutePath()); } } } } } } // ---------------------------------------------------------- /** * Perform any pre-cleaning steps needed before unpacking a subsystem * update jar. This method will delete the old contents of the subsystem * if necessary, based on the properties stored in the * {@link SubsystemUpdater} corresponding to this jar file. * * @param unpackDir the directory where the jar will be unpacked * @param jar the jar file that will be unpacked */ private void prepareUnpackingDir(File unpackDir, File jar) { String frameworkName = jar.getName().replaceFirst( "_[0-9]+(\\.[0-9]+)*\\..*$", ""); boolean isAppWrapper = APP_JAR_PREFIX.equals(frameworkName); File thisFrameworkDir = new File(unpackDir, isAppWrapper ? "WEB-INF/Web-CAT.woa/Contents" : frameworkName + ".framework"); SubsystemUpdater updater = getUpdaterFor(thisFrameworkDir); String[] alsoContains = null; { String alsoContainsRaw = updater.getProperty("alsoContains"); if (alsoContainsRaw != null) { alsoContains = alsoContainsRaw.split(",\\s*"); } } String[] removeUnused = null; { String removeUnusedRaw = updater.getProperty("removeUnused"); if (removeUnusedRaw != null) { removeUnused = removeUnusedRaw.split(",\\s*"); } } Map<String, String> preserveOnUpdate = null; { String preserveOnUpdateRaw = updater.getProperty("preserveOnUpdate"); if (isAppWrapper && preserveOnUpdateRaw == null) { preserveOnUpdateRaw = "WEB-INF/lib," + "WEB-INF/web.xml," + "WEB-INF/update.properties," + "WEB-INF/Web-CAT.woa/Contents/Frameworks," + "WEB-INF/Web-CAT.woa/Contents/Library," + "WEB-INF/Web-CAT.woa/configuration.properties," + "WEB-INF/pending-updates"; } if (preserveOnUpdateRaw != null) { preserveOnUpdate = new HashMap<String, String>(); for (String entry : preserveOnUpdateRaw.split(",\\s*")) { String key = FileUtilities.normalizeFileName( new File(unpackDir, entry)); preserveOnUpdate.put(key, key); } } } FileUtilities.deleteDirectory( isAppWrapper ? unpackDir : thisFrameworkDir, preserveOnUpdate); if (isAppWrapper) { // Examine configuration.properties to see if we just deleted // local copies of the static HTML resources File config = new File(unpackDir, "Web-INF/Web-CAT.woa/configuration.properties"); if (config.exists()) { try { InputStream is = new FileInputStream(config); Properties configProps = new Properties(); configProps.load(is); is.close(); if (configProps.getProperty("static.html.dir") == null) { // If this property is not set, then static resources // are stored in the root of the web app and they // were just deleted. Force re-copying of them. configProps.setProperty( "static.HTML.date", "00000000"); OutputStream out = new FileOutputStream(config); configProps.store( out, "Web-CAT configuration settings"); out.close(); } } catch (IOException e) { // We're not using log4j, since that may be within a // subsystem that needs updating System.out.println("WCServletAdaptor: ERROR: IO error " + "updating properties in " + config.getAbsolutePath() + ":" + e); } } } if (alsoContains != null) { for (String contains : alsoContains) { FileUtilities.deleteDirectory( new File(unpackDir, contains), preserveOnUpdate); } } if (removeUnused != null) { for (String unused : removeUnused) { FileUtilities.deleteDirectory( new File(unpackDir, unused), preserveOnUpdate); } } } // ---------------------------------------------------------- /** * Generate the classpath string from a list of framework directories. * @param subdirs * @param mainBundle */ private String classPathFrom(File[] subdirs, File mainBundle) { StringBuffer buffer = new StringBuffer(20 * subdirs.length); String woroot = properties.getProperty(INSTALLED_WOROOT); File installedWOFrameworkDir = null; if (woroot != null) { installedWOFrameworkDir = new File(woroot, "Library/Frameworks"); if (!installedWOFrameworkDir.exists()) { System.out.println("Cannot locate installed WO framework " + "directory at: " + installedWOFrameworkDir); installedWOFrameworkDir = null; } else if (!installedWOFrameworkDir.isDirectory()) { System.out.println("Installed WO framework location is not a " + "directory: " + installedWOFrameworkDir); installedWOFrameworkDir = null; } System.out.println("using WO root = " + installedWOFrameworkDir); } // First, handle all the subsystems for (File subdir : subdirs) { // Be sure to use the *local* version of JavaWOExtensions (from // project WONDER) rather than the default system version. if (installedWOFrameworkDir != null && !"JavaWOExtensions.framework".equals(subdir.getName())) { File localSubdir = new File(installedWOFrameworkDir, subdir.getName()); if (localSubdir.exists()) { // use the externally installed version instead subdir = localSubdir; } } getUpdaterFor(subdir).addToClasspath(buffer); } // Now handle the main bundle itself getUpdaterFor(mainBundle).addToClasspath(buffer); return buffer.toString(); } // ---------------------------------------------------------- /** * Attempt to load the properties settings for this adaptor. */ private void loadProperties() { properties = new Properties(); if (propertiesFile.exists()) { try { InputStream is = new FileInputStream(propertiesFile); properties.load(is); is.close(); } catch (IOException e) { // We're not using log4j, since that may be within a // subsystem that needs updating System.out.println("Error loading properties from " + propertiesFile.getAbsolutePath() + ":" + e); } } } // ---------------------------------------------------------- /** * Save the properties settings for this adaptor. */ private void commitProperties() { try { OutputStream out = new FileOutputStream(propertiesFile); properties.store(out, "WCServletAdaptor properties"); out.close(); } catch (IOException e) { System.out.println("Error saving WCServletAdaptor properties to " + propertiesFile.getAbsolutePath() + ":" + e); } } // ---------------------------------------------------------- /** * Get the {@link SubsystemUpdater} for the specified subsystem location. * Creates a new updater on demand, if necessary. * @param dir the subsystem location to look up * @return the corresponding updater */ private SubsystemUpdater getUpdaterFor(File dir) { return systemUpdater.getUpdaterFor(dir); } // ---------------------------------------------------------- /** * This comparator is used by {@link #applyNecessaryUpdates(File)} to * sort framework directories. It ensures that the high-priority * frameworks are first on the resulting classpath. */ private static class FrameworkComparator implements java.util.Comparator<File> { // ---------------------------------------------------------- public int compare(File o1, File o2) { String left = o1.getName(); String right = o2.getName(); if (left.equals(right)) { return 0; } else { for (int i = 0; i < PRIORITY_FRAMEWORKS.length; i++) { if (PRIORITY_FRAMEWORKS[i].equals(left)) { return -1; } else if (PRIORITY_FRAMEWORKS[i].equals(right)) { return 1; } } return left.compareTo(right); } } } //~ Instance/static variables ............................................. private WCUpdater systemUpdater; private javax.servlet.ServletContext innerContext = null; private javax.servlet.ServletContext wrappedContext = null; private String woClasspath = null; private Properties properties; private File propertiesFile; private File updateDir; private File frameworkDir; private Throwable initFailed; private Set<String> additionalHttpMethods; private static WCServletAdaptor instance; private static final String[] PRIORITY_FRAMEWORKS = { "EOJDBCPrototypes.framework", "ERJars.framework", "ERExtensions.framework" }; private static final String APP_JAR_PREFIX = "webcat"; private static final String INSTALLED_WOROOT = "installed.woroot"; private static final String VERSION = "1.5"; }