/** * This Source Code Form is subject to the terms of the Mozilla Public License, * v. 2.0. If a copy of the MPL was not distributed with this file, You can * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under * the terms of the Healthcare Disclaimer located at http://openmrs.org/license. * * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS * graphic logo is a trademark of OpenMRS Inc. */ package org.openmrs.web; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.StringReader; import java.sql.Driver; import java.sql.DriverManager; import java.util.ArrayList; import java.util.Collection; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.ServletException; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.apache.log4j.Level; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.openmrs.api.context.Context; import org.openmrs.module.MandatoryModuleException; import org.openmrs.module.Module; import org.openmrs.module.ModuleFactory; import org.openmrs.module.ModuleMustStartException; import org.openmrs.module.OpenmrsCoreModuleException; import org.openmrs.module.web.WebModuleUtil; import org.openmrs.scheduler.SchedulerUtil; import org.openmrs.util.DatabaseUpdateException; import org.openmrs.util.DatabaseUpdater; import org.openmrs.util.InputRequiredException; import org.openmrs.util.MemoryLeakUtil; import org.openmrs.util.OpenmrsClassLoader; import org.openmrs.util.OpenmrsConstants; import org.openmrs.util.OpenmrsUtil; import org.openmrs.web.filter.initialization.InitializationFilter; import org.openmrs.web.filter.update.UpdateFilter; import org.slf4j.LoggerFactory; import org.slf4j.MarkerFactory; import org.springframework.beans.factory.BeanCreationException; import org.springframework.util.StringUtils; import org.springframework.web.context.ContextLoader; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.XmlWebApplicationContext; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.xml.sax.EntityResolver; import org.xml.sax.InputSource; import org.xml.sax.SAXException; /** * Our Listener class performs the basic starting functions for our webapp. Basic needs for starting * the API: 1) Get the runtime properties 2) Start Spring 3) Start the OpenMRS APi (via * Context.startup) Basic startup needs specific to the web layer: 1) Do the web startup of the * modules 2) Copy the custom look/images/messages over into the web layer */ public final class Listener extends ContextLoader implements ServletContextListener { // extends ContextLoaderListener { protected final org.slf4j.Logger log = LoggerFactory.getLogger(getClass()); private static boolean runtimePropertiesFound = false; private static Throwable errorAtStartup = null; private static boolean setupNeeded = false; /** * Boolean flag set on webapp startup marking whether there is a runtime properties file or not. * If there is not, then the {@link InitializationFilter} takes over any openmrs url and * redirects to the {@link WebConstants#SETUP_PAGE_URL} * * @return true/false whether an openmrs runtime properties file is defined */ public static boolean runtimePropertiesFound() { return runtimePropertiesFound; } /** * Boolean flag set by the {@link #contextInitialized(ServletContextEvent)} method if an error * occurred when trying to start up. The StartupErrorFilter displays the error to the admin * * @return true/false if an error occurred when starting up */ public static boolean errorOccurredAtStartup() { return errorAtStartup != null; } /** * Boolean flag that tells if we need to run the database setup wizard. * * @return true if setup is needed, else false. */ public static boolean isSetupNeeded() { return setupNeeded; } /** * Get the error thrown at startup * * @return get the error thrown at startup */ public static Throwable getErrorAtStartup() { return errorAtStartup; } public static void setRuntimePropertiesFound(boolean runtimePropertiesFound) { Listener.runtimePropertiesFound = runtimePropertiesFound; } public static void setErrorAtStartup(Throwable errorAtStartup) { Listener.errorAtStartup = errorAtStartup; } /** * This method is called when the servlet context is initialized(when the Web Application is * deployed). You can initialize servlet context related data here. * * @param event */ @Override public void contextInitialized(ServletContextEvent event) { final org.slf4j.Logger log = LoggerFactory.getLogger(Listener.class); log.debug("Starting the OpenMRS webapp"); try { // validate the current JVM version OpenmrsUtil.validateJavaVersion(); ServletContext servletContext = event.getServletContext(); // pulled from web.xml. loadConstants(servletContext); // erase things in the dwr file clearDWRFile(servletContext); setApplicationDataDirectory(servletContext); // Try to get the runtime properties Properties props = getRuntimeProperties(); if (props != null) { // the user has defined a runtime properties file setRuntimePropertiesFound(true); // set props to the context so that they can be // used during sessionFactory creation Context.setRuntimeProperties(props); String appDataRuntimeProperty = props .getProperty(OpenmrsConstants.APPLICATION_DATA_DIRECTORY_RUNTIME_PROPERTY, null); if (StringUtils.hasLength(appDataRuntimeProperty)) { OpenmrsUtil.setApplicationDataDirectory(null); } //ensure that we always log the runtime properties file that we are using //since openmrs is just booting, the log levels are not yet set. TRUNK-4835 Logger.getLogger(getClass()).setLevel(Level.INFO); log.info("Using runtime properties file: " + OpenmrsUtil.getRuntimePropertiesFilePathName(WebConstants.WEBAPP_NAME)); } Thread.currentThread().setContextClassLoader(OpenmrsClassLoader.getInstance()); if (!setupNeeded()) { // must be done after the runtime properties are // found but before the database update is done copyCustomizationIntoWebapp(servletContext, props); //super.contextInitialized(event); // also see commented out line in contextDestroyed /** * This logic is from ContextLoader.initWebApplicationContext. Copied here instead * of calling that so that the context is not cached and hence not garbage collected */ XmlWebApplicationContext context = (XmlWebApplicationContext) createWebApplicationContext(servletContext); configureAndRefreshWebApplicationContext(context, servletContext); servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, context); WebDaemon.startOpenmrs(event.getServletContext()); } else { setupNeeded = true; } } catch (Exception e) { setErrorAtStartup(e); log.error(MarkerFactory.getMarker("FATAL"), "Failed to obtain JDBC connection", e); } } /** * This method knows about all the filters that openmrs uses for setup. Currently those are the * {@link InitializationFilter} and the {@link UpdateFilter}. If either of these have to do * something, openmrs won't start in this Listener. * * @return true if one of the filters needs to take some action */ private boolean setupNeeded() throws Exception { if (!runtimePropertiesFound) { return true; } return DatabaseUpdater.updatesRequired() && !DatabaseUpdater.allowAutoUpdate(); } /** * Do the work of starting openmrs. * * @param servletContext * @throws ServletException */ public static void startOpenmrs(ServletContext servletContext) throws ServletException { //Ensure that we are being called from WebDaemon //TODO this did not work because callerClass was org.openmrs.web.WebDaemon$1 instead of org.openmrs.web.WebDaemon /*Class<?> callerClass = new OpenmrsSecurityManager().getCallerClass(0); if (!WebDaemon.class.isAssignableFrom(callerClass)) throw new APIException("This method can only be called from the WebDaemon class, not " + callerClass.getName());*/ // start openmrs try { // load bundled modules that are packaged into the webapp Listener.loadBundledModules(servletContext); Context.startup(getRuntimeProperties()); } catch (DatabaseUpdateException updateEx) { throw new ServletException("Should not be here because updates were run previously", updateEx); } catch (InputRequiredException inputRequiredEx) { throw new ServletException("Should not be here because updates were run previously", inputRequiredEx); } catch (MandatoryModuleException mandatoryModEx) { throw new ServletException(mandatoryModEx); } catch (OpenmrsCoreModuleException coreModEx) { // don't wrap this error in a ServletException because we want to deal with it differently // in the StartupErrorFilter class throw coreModEx; } // TODO catch openmrs errors here and drop the user back out to the setup screen try { // web load modules Listener.performWebStartOfModules(servletContext); // start the scheduled tasks SchedulerUtil.startup(getRuntimeProperties()); } catch (Exception t) { Context.shutdown(); WebModuleUtil.shutdownModules(servletContext); throw new ServletException(t); } finally { Context.closeSession(); } } /** * Load the openmrs constants with values from web.xml init parameters * * @param servletContext startup context (web.xml) */ private void loadConstants(ServletContext servletContext) { WebConstants.BUILD_TIMESTAMP = servletContext.getInitParameter("build.timestamp"); WebConstants.WEBAPP_NAME = getContextPath(servletContext); WebConstants.MODULE_REPOSITORY_URL = servletContext.getInitParameter("module.repository.url"); if (!"openmrs".equalsIgnoreCase(WebConstants.WEBAPP_NAME)) { OpenmrsConstants.KEY_OPENMRS_APPLICATION_DATA_DIRECTORY = WebConstants.WEBAPP_NAME + "_APPLICATION_DATA_DIRECTORY"; } } private void setApplicationDataDirectory(ServletContext servletContext) { // note: the below value will be overridden after reading the runtime properties if the // "application_data_directory" runtime property is set String appDataDir = servletContext.getInitParameter("application.data.directory"); if (StringUtils.hasLength(appDataDir)) { OpenmrsUtil.setApplicationDataDirectory(appDataDir); } else if (!"openmrs".equalsIgnoreCase(WebConstants.WEBAPP_NAME)) { OpenmrsUtil.setApplicationDataDirectory( OpenmrsUtil.getApplicationDataDirectory() + File.separator + WebConstants.WEBAPP_NAME); } } /** * Hacky way to get the current contextPath. This will usually be "openmrs". This method will be * obsolete when servlet api ~2.6 comes out...at which point a call like * servletContext.getContextRoot() would be sufficient * * @return current contextPath of this webapp without initial slash */ private String getContextPath(ServletContext servletContext) { // Get the context path without the request. String contextPath = ""; try { contextPath = servletContext.getContextPath(); } catch (NoSuchMethodError ex) { // ServletContext.getContextPath() was added in version 2.5 of the Servlet API. Tomcat 5.5 // has version 2.4 of the servlet API, so we fall back to the hacky code we were previously // using try { String path = servletContext.getResource("/").getPath(); contextPath = path.substring(0, path.lastIndexOf("/")); contextPath = contextPath.substring(contextPath.lastIndexOf("/")); } catch (Exception e) { log.error("Failed to get construct context path", e); } } catch (Exception e) { log.error("Failed to get context path", e); } // trim off initial slash if it exists if (contextPath.indexOf("/") != -1) { contextPath = contextPath.substring(1); } return contextPath; } /** * Convenience method to empty out the dwr-modules.xml file to fix any errors that might have * occurred in it when loading or unloading modules. * * @param servletContext */ private void clearDWRFile(ServletContext servletContext) { final org.slf4j.Logger log = LoggerFactory.getLogger(Listener.class); String realPath = servletContext.getRealPath(""); String absPath = realPath + "/WEB-INF/dwr-modules.xml"; File dwrFile = new File(absPath.replace("/", File.separator)); try { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); db.setEntityResolver(new EntityResolver() { @Override public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException { // When asked to resolve external entities (such as a DTD) we return an InputSource // with no data at the end, causing the parser to ignore the DTD. return new InputSource(new StringReader("")); } }); Document doc = db.parse(dwrFile); Element elem = doc.getDocumentElement(); elem.setTextContent(""); OpenmrsUtil.saveDocument(doc, dwrFile); } catch (Exception e) { // got here because the dwr-modules.xml file is empty for some reason. This might // happen because the servlet container (i.e. tomcat) crashes when first loading this file log.debug("Error clearing dwr-modules.xml", e); dwrFile.delete(); FileWriter writer = null; try { writer = new FileWriter(dwrFile); writer.write( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE dwr PUBLIC \"-//GetAhead Limited//DTD Direct Web Remoting 2.0//EN\" \"http://directwebremoting.org/schema/dwr20.dtd\">\n<dwr></dwr>"); } catch (IOException io) { log.error( "Unable to clear out the " + dwrFile.getAbsolutePath() + " file. Please redeploy the openmrs war file", io); } finally { if (writer != null) { try { writer.close(); } catch (IOException io) { log.warn("Couldn't close Writer: " + io); } } } } } /** * Copy the customization scripts over into the webapp * * @param servletContext */ private void copyCustomizationIntoWebapp(ServletContext servletContext, Properties props) { final org.slf4j.Logger log = LoggerFactory.getLogger(Listener.class); String realPath = servletContext.getRealPath(""); // TODO centralize map to WebConstants? Map<String, String> custom = new HashMap<String, String>(); custom.put("custom.template.dir", "/WEB-INF/template"); custom.put("custom.index.jsp.file", "/WEB-INF/view/index.jsp"); custom.put("custom.login.jsp.file", "/WEB-INF/view/login.jsp"); custom.put("custom.patientDashboardForm.jsp.file", "/WEB-INF/view/patientDashboardForm.jsp"); custom.put("custom.images.dir", "/images"); custom.put("custom.style.css.file", "/style.css"); custom.put("custom.messages", "/WEB-INF/custom_messages.properties"); custom.put("custom.messages_fr", "/WEB-INF/custom_messages_fr.properties"); custom.put("custom.messages_es", "/WEB-INF/custom_messages_es.properties"); custom.put("custom.messages_de", "/WEB-INF/custom_messages_de.properties"); for (Map.Entry<String, String> entry : custom.entrySet()) { String prop = entry.getKey(); String webappPath = entry.getValue(); String userOverridePath = props.getProperty(prop); // if they defined the variable if (userOverridePath != null) { String absolutePath = realPath + webappPath; File file = new File(userOverridePath); // if they got the path correct // also, if file does not start with a "." (hidden files, like SVN files) if (file.exists() && !userOverridePath.startsWith(".")) { log.debug("Overriding file: " + absolutePath); log.debug("Overriding file with: " + userOverridePath); if (file.isDirectory()) { for (File f : file.listFiles()) { userOverridePath = f.getAbsolutePath(); if (!f.getName().startsWith(".")) { String tmpAbsolutePath = absolutePath + "/" + f.getName(); if (!copyFile(userOverridePath, tmpAbsolutePath)) { log.warn("Unable to copy file in folder defined by runtime property: " + prop); log.warn("Your source directory (or a file in it) '" + userOverridePath + " cannot be loaded or destination '" + tmpAbsolutePath + "' cannot be found"); } } } } else { // file is not a directory if (!copyFile(userOverridePath, absolutePath)) { log.warn("Unable to copy file defined by runtime property: " + prop); log.warn("Your source file '" + userOverridePath + " cannot be loaded or destination '" + absolutePath + "' cannot be found"); } } } } } } /** * Copies file pointed to by <code>fromPath</code> to <code>toPath</code> * * @param fromPath * @param toPath * @return true/false whether the copy was a success */ private boolean copyFile(String fromPath, String toPath) { final org.slf4j.Logger log = LoggerFactory.getLogger(Listener.class); FileInputStream inputStream = null; FileOutputStream outputStream = null; try { inputStream = new FileInputStream(fromPath); outputStream = new FileOutputStream(toPath); OpenmrsUtil.copyFile(inputStream, outputStream); } catch (IOException io) { return false; } finally { try { if (inputStream != null) { inputStream.close(); } } catch (IOException io) { log.warn("Unable to close input stream", io); } try { if (outputStream != null) { outputStream.close(); } } catch (IOException io) { log.warn("Unable to close input stream", io); } } return true; } /** * Load the pre-packaged modules from web/WEB-INF/bundledModules. <br> * <br> * This method assumes that the api startup() and WebModuleUtil.startup() will be called later * for modules that loaded here * * @param servletContext the current servlet context for the webapp */ public static void loadBundledModules(ServletContext servletContext) { final org.slf4j.Logger log = LoggerFactory.getLogger(Listener.class); String path = servletContext.getRealPath(""); path += File.separator + "WEB-INF" + File.separator + "bundledModules"; File folder = new File(path); if (!folder.exists()) { log.warn("Bundled module folder doesn't exist: " + folder.getAbsolutePath()); return; } if (!folder.isDirectory()) { log.warn("Bundled module folder isn't really a directory: " + folder.getAbsolutePath()); return; } // loop over the modules and load the modules that we can for (File f : folder.listFiles()) { if (!f.getName().startsWith(".")) { // ignore .svn folder and the like try { Module mod = ModuleFactory.loadModule(f); log.debug("Loaded bundled module: " + mod + " successfully"); } catch (Exception e) { log.warn("Error while trying to load bundled module " + f.getName() + "", e); } } } } /** * Called when the webapp is shut down properly Must call Context.shutdown() and then shutdown * all the web layers of the modules * * @see org.springframework.web.context.ContextLoaderListener#contextDestroyed(javax.servlet.ServletContextEvent) */ @SuppressWarnings("squid:S1215") @Override public void contextDestroyed(ServletContextEvent event) { try { Context.openSession(); Context.shutdown(); WebModuleUtil.shutdownModules(event.getServletContext()); } catch (Exception e) { // don't print the unhelpful "contextDAO is null" message if (!"contextDAO is null".equals(e.getMessage())) { // not using log.error here so it can be garbage collected System.out.println("Listener.contextDestroyed: Error while shutting down openmrs: "); log.error("Listener.contextDestroyed: Error while shutting down openmrs: ", e); } } finally { if ("true".equalsIgnoreCase(System.getProperty("FUNCTIONAL_TEST_MODE"))) { //Delete the temporary file created for functional testing and shutdown the mysql daemon String filename = WebConstants.WEBAPP_NAME + "-test-runtime.properties"; File file = new File(OpenmrsUtil.getApplicationDataDirectory(), filename); System.out.println(filename + " delete=" + file.delete()); //new com.mysql.management.MysqldResource(new File("../openmrs/target/database")).shutdown(); } // remove the user context that we set earlier Context.closeSession(); } // commented out because we are not init'ing it in the contextInitialization anymore // super.contextDestroyed(event); try { for (Enumeration<Driver> e = DriverManager.getDrivers(); e.hasMoreElements();) { Driver driver = e.nextElement(); ClassLoader classLoader = driver.getClass().getClassLoader(); // only unload drivers for this webapp if (classLoader == null || classLoader == getClass().getClassLoader()) { DriverManager.deregisterDriver(driver); } else { System.err.println("Didn't remove driver class: " + driver.getClass() + " with classloader of: " + driver.getClass().getClassLoader()); } } } catch (Exception e) { System.err.println("Listener.contextDestroyed: Failed to cleanup drivers in webapp"); log.error("Listener.contextDestroyed: Failed to cleanup drivers in webapp", e); } MemoryLeakUtil.shutdownMysqlCancellationTimer(); MemoryLeakUtil.shutdownKeepAliveTimer(); OpenmrsClassLoader.onShutdown(); LogManager.shutdown(); // just to make things nice and clean. // Suppressing sonar issue squid:S1215 System.gc(); System.gc(); } /** * Finds and loads the runtime properties * * @return Properties * @see OpenmrsUtil#getRuntimeProperties(String) */ public static Properties getRuntimeProperties() { return OpenmrsUtil.getRuntimeProperties(WebConstants.WEBAPP_NAME); } /** * Call WebModuleUtil.startModule on each started module * * @param servletContext * @throws ModuleMustStartException if the context cannot restart due to a * {@link MandatoryModuleException} or {@link OpenmrsCoreModuleException} */ public static void performWebStartOfModules(ServletContext servletContext) throws ModuleMustStartException, Exception { List<Module> startedModules = new ArrayList<Module>(); startedModules.addAll(ModuleFactory.getStartedModules()); performWebStartOfModules(startedModules, servletContext); } public static void performWebStartOfModules(Collection<Module> startedModules, ServletContext servletContext) throws ModuleMustStartException, Exception { final org.slf4j.Logger log = LoggerFactory.getLogger(Listener.class); boolean someModuleNeedsARefresh = false; for (Module mod : startedModules) { try { boolean thisModuleCausesRefresh = WebModuleUtil.startModule(mod, servletContext, /* delayContextRefresh */true); someModuleNeedsARefresh = someModuleNeedsARefresh || thisModuleCausesRefresh; } catch (Exception e) { mod.setStartupErrorMessage("Unable to start module", e); } } if (someModuleNeedsARefresh) { try { WebModuleUtil.refreshWAC(servletContext, true, null); } catch (ModuleMustStartException ex) { // pass this up to the calling method so that openmrs loading stops throw ex; } catch (BeanCreationException ex) { // pass this up to the calling method so that openmrs loading stops throw ex; } catch (Exception e) { Throwable rootCause = getActualRootCause(e, true); if (rootCause != null) { log.error(MarkerFactory.getMarker("FATAL"), "Unable to refresh the spring application context. Root Cause was:", rootCause); } else { log.error(MarkerFactory.getMarker("FATAL"), "nable to refresh the spring application context. Unloading all modules, Error was:", e); } try { WebModuleUtil.shutdownModules(servletContext); for (Module mod : ModuleFactory.getLoadedModules()) {// use loadedModules to avoid a concurrentmodificationexception if (!mod.isCoreModule() && !mod.isMandatory()) { try { ModuleFactory.stopModule(mod, true, true); } catch (Exception t3) { // just keep going if we get an error shutting down. was probably caused by the module // that actually got us to this point! log.trace("Unable to shutdown module:" + mod, t3); } } } WebModuleUtil.refreshWAC(servletContext, true, null); } catch (MandatoryModuleException ex) { // pass this up to the calling method so that openmrs loading stops throw new MandatoryModuleException(ex.getModuleId(), "Got an error while starting a mandatory module: " + e.getMessage() + ". Check the server logs for more information"); } catch (Exception t2) { // a mandatory or core module is causing spring to fail to start up. We don't want those // stopped so we must report this error to the higher authorities log.warn("caught another error: ", t2); throw t2; } } } // because we delayed the refresh, we need to load+start all servlets and filters now // (this is to protect servlets/filters that depend on their module's spring xml config being available) for (Module mod : ModuleFactory.getStartedModules()) { WebModuleUtil.loadServlets(mod, servletContext); WebModuleUtil.loadFilters(mod, servletContext); } } /** * Convenience method that recursively attempts to pull the root case from a Throwable * * @param t the Throwable object * @param isOriginalError specifies if the passed in Throwable is the original Exception that * was thrown * @return the root cause if any was found */ private static Throwable getActualRootCause(Throwable t, boolean isOriginalError) { if (t.getCause() != null) { return getActualRootCause(t.getCause(), false); } if (!isOriginalError) { return t; } return null; } }