/* * Copyright 1998-2014 University Corporation for Atmospheric Research/Unidata * * Portions of this software were developed by the Unidata Program at the * University Corporation for Atmospheric Research. * * Access and use of this software shall impose the following obligations * and understandings on the user. The user is granted the right, without * any fee or cost, to use, copy, modify, alter, enhance and distribute * this software, and any derivative works thereof, and its supporting * documentation for any purpose whatsoever, provided that this entire * notice appears in all copies of the software, derivative works and * supporting documentation. Further, UCAR requests that the user credit * UCAR/Unidata in any publications that result from the use of this * software or in any product that includes this software. The names UCAR * and/or Unidata, however, may not be used in any advertising or publicity * to endorse or promote any products or commercial entity unless specific * written permission is obtained from UCAR/Unidata. The user also * understands that UCAR/Unidata is not obligated to provide the user with * any support, consulting, training or assistance of any kind with regard * to the use, operation and performance of this software nor to provide * the user with any updates, revisions, new versions or "bug fixes." * * THIS SOFTWARE IS PROVIDED BY UCAR/UNIDATA "AS IS" AND ANY EXPRESS 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 UCAR/UNIDATA BE LIABLE FOR ANY SPECIAL, * INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING * FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION * WITH THE ACCESS, USE OR PERFORMANCE OF THIS SOFTWARE. */ package thredds.server.config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.context.ServletContextAware; import org.springframework.web.util.Log4jWebConfigurer; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import thredds.catalog.InvDatasetFeatureCollection; import thredds.catalog.InvDatasetScan; import thredds.inventory.CollectionUpdater; import thredds.servlet.ServletUtil; import thredds.servlet.ThreddsConfig; import thredds.util.filesource.*; import ucar.httpservices.HTTPFactory; import ucar.httpservices.HTTPMethod; import ucar.httpservices.HTTPSession; import ucar.nc2.util.IO; import ucar.unidata.util.StringUtil2; import javax.servlet.RequestDispatcher; import javax.servlet.ServletContext; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.*; /** * TDS context implements ServletContextAware so it gets a ServletContext and performs most initial THREDDS set up: * - checks version * - check for latest stable and development release versions * - sets the content directory * - reads persistent user defined params and runs ThreddsConfig.init * - creates, if don't exist, log and public dirs in content directory * - Sets InvDatasetScan and InvDatasetFeatureCollection properties * - Get default and jsp dispatchers from servletContext * - Creates and initializes the TdsConfigMapper * * @author edavis * @since 4.0 */ @Component("tdsContext") public final class TdsContext implements ServletContextAware, InitializingBean, DisposableBean { private final Logger logServerStartup = LoggerFactory.getLogger("serverStartup"); private final Logger logCatalogInit = LoggerFactory.getLogger(TdsContext.class.getName() + ".catalogInit"); private String webappName; private String contextPath; // The values for these properties all come from tds/src/main/template/thredds/server/tds.properties except for // "tds.content.root.path", which must be defined on the command line. @Value("${tds.version}") private String webappVersion; @Value("${tds.version.builddate}") private String webappVersionBuildDate; @Value("${tds.content.root.path}") private String contentRootPath; @Value("${tds.content.path}") private String contentPath; @Value("${tds.config.file}") private String tdsConfigFileName; @Value("${tds.content.startup.path}") private String startupContentPath; //////////////////////////////////// private String webinfPath; private File rootDirectory; private File contentDirectory; private File publicContentDirectory; private File tomcatLogDir; private File startupContentDirectory; //private File iddContentDirectory; //private File motherlodeContentDirectory; private DescendantFileSource rootDirSource; private DescendantFileSource contentDirSource; private DescendantFileSource publicContentDirSource; private DescendantFileSource startupContentDirSource; //private DescendantFileSource iddContentPublicDirSource; //private DescendantFileSource motherlodeContentPublicDirSource; private FileSource configSource; private FileSource publicDocSource; private RequestDispatcher defaultRequestDispatcher; private RequestDispatcher jspRequestDispatcher; @Autowired private HtmlConfig htmlConfig; @Autowired private TdsServerInfo serverInfo; @Autowired private WmsConfig wmsConfig; @Autowired private CorsConfig corsConfig; @Autowired private TdsUpdateConfig tdsUpdateConfig; private ServletContext servletContext; private TdsContext() { } public void setWebappVersion(String verFull) { this.webappVersion = verFull; } public void setWebappVersionBuildDate(String buildDateString) { this.webappVersionBuildDate = buildDateString; } public void setContentRootPath(String contentRootPath) { this.contentRootPath = contentRootPath; } public String getContentRootPath() { return this.contentRootPath; } public String getContentRootPathAbsolute() { File abs = new File(this.contentRootPath); return abs.getAbsolutePath(); } public void setContentPath(String contentPath) { this.contentPath = contentPath; } public void setStartupContentPath(String startupContentPath) { this.startupContentPath = startupContentPath; } public void setTdsConfigFileName(String filename) { this.tdsConfigFileName = filename; } public String getTdsConfigFileName() { return this.tdsConfigFileName; } public void setServerInfo(TdsServerInfo serverInfo) { this.serverInfo = serverInfo; } public TdsServerInfo getServerInfo() { return serverInfo; } public void setHtmlConfig(HtmlConfig htmlConfig) { this.htmlConfig = htmlConfig; } public HtmlConfig getHtmlConfig() { return this.htmlConfig; } public WmsConfig getWmsConfig() { return wmsConfig; } public void setWmsConfig(WmsConfig wmsConfig) { this.wmsConfig = wmsConfig; } public CorsConfig getCorsConfig() { return corsConfig; } public void setCorsConfig(CorsConfig corsConfig) { this.corsConfig = corsConfig; } public TdsUpdateConfig getTdsUpdateConfig() { return tdsUpdateConfig; } public void setTdsUpdateConfig(TdsUpdateConfig tdsUpdateConfig) { this.tdsUpdateConfig = tdsUpdateConfig; } /* * Release tdsContext resources * (non-Javadoc) * @see org.springframework.beans.factory.DisposableBean#destroy() */ public void destroy() { logServerStartup.info("TdsContext: releasing resources"); logServerStartup.info("TdsContext: Shutting down collection manager"); CollectionUpdater.INSTANCE.shutdown(); logServerStartup.info("TdsContext: shutdownLogging()"); Log4jWebConfigurer.shutdownLogging(servletContext); } public void afterPropertiesSet() { // ToDo Instead of stdout, use servletContext.log( "...") [NOTE: it writes to localhost.*.log rather than catalina.out]. if (servletContext == null) throw new IllegalArgumentException("ServletContext must not be null."); // ToDo LOOK - Are we still using this. ServletUtil.initDebugging(servletContext); // Set the webapp name. this.webappName = servletContext.getServletContextName(); // Set the context path. // Servlet 2.5 allows the following. //contextPath = servletContext.getContextPath(); String tmpContextPath = servletContext.getInitParameter("ContextPath"); // cannot be overridden in the ThreddsConfig file if (tmpContextPath == null) tmpContextPath = "thredds"; contextPath = "/" + tmpContextPath; // ToDo LOOK - Get rid of need for setting contextPath in ServletUtil. ServletUtil.setContextPath(contextPath); // Set the root directory and source. String rootPath = servletContext.getRealPath("/"); if (rootPath == null) { String msg = "Webapp [" + this.webappName + "] must run with exploded deployment directory (not from .war)."; logServerStartup.error("TdsContext.init(): " + msg); throw new IllegalStateException(msg); } this.rootDirectory = new File(rootPath); this.rootDirSource = new BasicDescendantFileSource(this.rootDirectory); this.rootDirectory = this.rootDirSource.getRootDirectory(); // ToDo LOOK - Get rid of need for setting rootPath in ServletUtil. ServletUtil.setRootPath(this.rootDirSource.getRootDirectoryPath()); // Set the startup (initial install) content directory and source. this.startupContentDirectory = new File(this.rootDirectory, this.startupContentPath); this.startupContentDirSource = new BasicDescendantFileSource(this.startupContentDirectory); this.startupContentDirectory = this.startupContentDirSource.getRootDirectory(); this.webinfPath = this.rootDirectory + "/WEB-INF"; // set the tomcat logging directory try { String base = System.getProperty("catalina.base"); if (base != null) { this.tomcatLogDir = new File(base, "logs").getCanonicalFile(); if (!this.tomcatLogDir.exists()) { String msg = "'catalina.base' directory not found: " + this.tomcatLogDir; logServerStartup.error("TdsContext.init(): " + msg); } } else { String msg = "'catalina.base' property not found - probably not a tomcat server"; logServerStartup.warn("TdsContext.init(): " + msg); } } catch (IOException e) { String msg = "tomcatLogDir could not be created"; logServerStartup.error("TdsContext.init(): " + msg); } String contentRootPathKey = "tds.content.root.path"; // In applicationContext-tdsConfig.xml, we have ignoreUnresolvablePlaceholders set to "true". // As a result, when properties aren't defined, they will keep their placeholder String. // In this case, that's "${tds.content.root.path}". if (this.contentRootPath.equals("${tds.content.root.path}")) { String message = String.format("\"%s\" property isn't defined.", contentRootPathKey); logServerStartup.error(message); throw new IllegalStateException(message); } File contentRootDir = new File(this.contentRootPath); if (!contentRootDir.isAbsolute()) { contentRootDir = new File(this.rootDirectory, this.contentRootPath); } if (contentRootDir.isDirectory()) { this.contentDirectory = new File(contentRootDir, this.contentPath); } else { String message = String.format("\"%s\" property doesn't define a directory: %s", contentRootPathKey, contentRootPath); logServerStartup.error(message); throw new IllegalStateException(message); } // If content directory exists, make sure it is a directory. if (this.contentDirectory.isDirectory()) { this.contentDirSource = new BasicDescendantFileSource(StringUtils.cleanPath(this.contentDirectory.getAbsolutePath())); this.contentDirectory = this.contentDirSource.getRootDirectory(); } else { String message = String.format( "TdsContext.init(): Content directory is not a directory: %s", this.contentDirectory.getAbsolutePath()); logServerStartup.error(message); throw new IllegalStateException(message); } ServletUtil.setContentPath(this.contentDirSource.getRootDirectoryPath()); //////////////////////////////////// Copy default startup files, if necessary //////////////////////////////////// try { File catalogFile = new File(contentDirectory, "catalog.xml"); if (!catalogFile.exists()) { File defaultCatalogFile = new File(startupContentDirectory, "catalog.xml"); logServerStartup.info("TdsContext.init(): Copying default catalog file from {}.", defaultCatalogFile); IO.copyFile(defaultCatalogFile, catalogFile); File enhancedCatalogFile = new File(contentDirectory, "enhancedCatalog.xml"); File defaultEnhancedCatalogFile = new File(startupContentDirectory, "enhancedCatalog.xml"); logServerStartup.info("TdsContext.init(): Copying default enhanced catalog file from {}.", defaultEnhancedCatalogFile); IO.copyFile(defaultEnhancedCatalogFile, enhancedCatalogFile); File dataDir = new File(new File(contentDirectory, "public"), "testdata"); File defaultDataDir = new File(new File(startupContentDirectory, "public"), "testdata"); logServerStartup.info("TdsContext.init(): Copying default testdata directory from {}.", defaultDataDir); IO.copyDirTree(defaultDataDir.getCanonicalPath(), dataDir.getCanonicalPath()); } File threddsConfigFile = new File(contentDirectory, "threddsConfig.xml"); if (!threddsConfigFile.exists()) { File defaultThreddsConfigFile = new File(startupContentDirectory, "threddsConfig.xml"); logServerStartup.info("TdsContext.init(): Copying default THREDDS config file from {}.", defaultThreddsConfigFile); IO.copyFile(defaultThreddsConfigFile, threddsConfigFile); } File wmsConfigXmlFile = new File(contentDirectory, "wmsConfig.xml"); if (!wmsConfigXmlFile.exists()) { File defaultWmsConfigXmlFile = new File(startupContentDirectory, "wmsConfig.xml"); logServerStartup.info("TdsContext.init(): Copying default WMS config file from {}.", defaultWmsConfigXmlFile); IO.copyFile(defaultWmsConfigXmlFile, wmsConfigXmlFile); } } catch (IOException e) { String message = String.format("Could not copy default startup files to %s.", contentDirectory); logServerStartup.error("TdsContext.init(): " + message); throw new IllegalStateException(message, e); } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// File logDir = new File(this.contentDirectory, "logs"); if (!logDir.exists()) { if (!logDir.mkdirs()) { String msg = "Couldn't create TDS log directory [" + logDir.getPath() + "]."; logServerStartup.error("TdsContext.init(): " + msg); throw new IllegalStateException(msg); } } String loggingDirectory = StringUtil2.substitute(logDir.getPath(), "\\", "/"); System.setProperty("tds.log.dir", loggingDirectory); // variable substitution // LOOK Remove log4j init JC 6/13/2012 // which is used in log4j.xml file loaded here. // LOOK Remove Log4jWebConfigurer,initLogging - depends on log4g v1, we are using v2 JC 9/2/2013 // Log4jWebConfigurer.initLogging( servletContext ); logServerStartup.info("TdsContext version= " + getVersionInfo()); logServerStartup.info("TdsContext intialized logging in " + logDir.getPath()); // read in persistent user-defined params from threddsConfig.xml File tdsConfigFile = this.contentDirSource.getFile(this.getTdsConfigFileName()); if (tdsConfigFile == null) { tdsConfigFile = new File(this.contentDirSource.getRootDirectory(), this.getTdsConfigFileName()); String msg = "TDS configuration file doesn't exist: " + tdsConfigFile; logServerStartup.error("TdsContext.init(): " + msg); throw new IllegalStateException(msg); } ThreddsConfig.init(tdsConfigFile.getPath()); this.publicContentDirectory = new File(this.contentDirectory, "public"); if (!publicContentDirectory.exists()) { if (!publicContentDirectory.mkdirs()) { String msg = "Couldn't create TDS public directory [" + publicContentDirectory.getPath() + "]."; logServerStartup.error("TdsContext.init(): " + msg); throw new IllegalStateException(msg); } } this.publicContentDirSource = new BasicDescendantFileSource(this.publicContentDirectory); List<DescendantFileSource> chain = new ArrayList<>(); DescendantFileSource contentMinusPublicSource = new BasicWithExclusionsDescendantFileSource(this.contentDirectory, Collections.singletonList("public")); chain.add(contentMinusPublicSource); this.configSource = new ChainedFileSource(chain); this.publicDocSource = this.publicContentDirSource; // ToDo LOOK Find a better way once thredds.catalog2 is used. InvDatasetScan.setContext(contextPath); InvDatasetScan.setCatalogServletName("/catalog"); InvDatasetFeatureCollection.setContext(contextPath); // GridServlet.setContextPath( contextPath ); // Won't need when switch GridServlet to use Swing MVC and TdsContext jspRequestDispatcher = servletContext.getNamedDispatcher("jsp"); defaultRequestDispatcher = servletContext.getNamedDispatcher("default"); TdsConfigMapper tdsConfigMapper = new TdsConfigMapper(); tdsConfigMapper.setTdsServerInfo(this.serverInfo); tdsConfigMapper.setHtmlConfig(this.htmlConfig); tdsConfigMapper.setWmsConfig(this.wmsConfig); tdsConfigMapper.setCorsConfig(this.corsConfig); tdsConfigMapper.setTdsUpdateConfig(this.tdsUpdateConfig); tdsConfigMapper.init(this); // log current server version in catalogInit, where it is // most likely to be seen by the user String message = "You are currently running TDS version " + this.getVersionInfo(); logCatalogInit.info(message); // check and log the latest stable and development version information // only if it is OK according to the threddsConfig file. if (this.tdsUpdateConfig.isLogVersionInfo()) { Map<String, String> latestVersionInfo = getLatestVersionInfo(); if (!latestVersionInfo.isEmpty()) { logCatalogInit.info("Latest Available TDS Version Info:"); for (Map.Entry entry : latestVersionInfo.entrySet()) { message = "latest " + entry.getKey() + " version = " + entry.getValue(); logServerStartup.info("TdsContext: " + message); logCatalogInit.info(" " + message); } logCatalogInit.info(""); } } } /** * Return the name of the webapp as given by the display-name element in web.xml. * * @return the name of the webapp as given by the display-name element in web.xml. */ public String getWebappName() { return this.webappName; } /** * Return the context path under which this web app is running (e.g., "/thredds"). * * @return the context path. */ public String getContextPath() { return contextPath; } /** * Return the context path under which this web app is running (e.g., "/thredds"). * * @return the context path. */ public String getWebinfPath() { return webinfPath; } /** * Return the full version string (<major>.<minor>.<bug>.<build>) * for this web application. * * @return the full version string. */ public String getWebappVersion() { return this.webappVersion; } public String getWebappVersionBuildDate() { return this.webappVersionBuildDate; } public String getVersionInfo() { StringBuilder sb = new StringBuilder(); sb.append(getWebappVersion()); if (getWebappVersionBuildDate() != null) { sb.append(" - "); sb.append(getWebappVersionBuildDate()); } return sb.toString(); } /** * Retrieve the latest stable and development versions * available from Unidata. Needs to connect to * http://www.unidata.ucar.edu in order to get the * latest version numbers. The propose is to easily let users * know if the version of TDS they are running is out of * date, as this information is recorded in the * serverStartup.log file. * * @return A hashmap containing versionTypes as key (i.e. * "stable", "development") and their corresponding * version numbers (i.e. 4.5.2) */ private Map<String, String> getLatestVersionInfo() { int socTimeout = 1; // http socket timeout in seconds int connectionTimeout = 3; // http connection timeout in seconds Map<String, String> latestVersionInfo = new HashMap<>(); String versionUrl = "http://www.unidata.ucar.edu/software/thredds/latest.xml"; try { try (HTTPMethod method = HTTPFactory.Get(versionUrl)) { HTTPSession httpClient = method.getSession(); httpClient.setSoTimeout(socTimeout * 1000); httpClient.setConnectionTimeout(connectionTimeout * 1000); httpClient.setUserAgent("TDS_" + getVersionInfo().replace(" ", "")); method.execute(); InputStream responseIs = method.getResponseBodyAsStream(); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); Document dom = db.parse(responseIs); Element docEle = dom.getDocumentElement(); NodeList versionElements = docEle.getElementsByTagName("version"); if(versionElements != null && versionElements.getLength() > 0) { for(int i = 0;i < versionElements.getLength();i++) { //get the version element Element versionElement = (Element) versionElements.item(i); String verType = versionElement.getAttribute("name"); String verStr = versionElement.getAttribute("value"); latestVersionInfo.put(verType, verStr); } } } } catch (IOException e) { logServerStartup.warn("TdsContext - Could not get latest version information from Unidata."); } catch (ParserConfigurationException e) { logServerStartup.error("TdsContext - Error configuring latest version xml parser" + e.getMessage() + "."); } catch (SAXException e) { logServerStartup.error("TdsContext - Could not parse latest version information."); } return latestVersionInfo; } /** * Return the web apps root directory (i.e., getRealPath( "/")). * * @return the root directory for the web app. */ public File getRootDirectory() { return rootDirectory; } /** * Return the tomcat logging directory * * @return the tomcat logging directory. */ public File getTomcatLogDirectory() { return tomcatLogDir; } /** * Return File for content directory (exists() may be false). * * @return a File to the content directory. */ public File getContentDirectory() { return contentDirectory; } /** * Return File for the initial content directory. I.e., the directory * that contains default content for the content directory, copied * there when TDS is first installed. * * @return a File to the initial content directory. */ public File getStartupContentDirectory() { return startupContentDirectory; } /* public File getIddContentDirectory() { return iddContentDirectory; } public File getMotherlodeContentDirectory() { return motherlodeContentDirectory; } */ public FileSource getConfigFileSource() { return this.configSource; } public FileSource getPublicDocFileSource() { return this.publicDocSource; } public RequestDispatcher getDefaultRequestDispatcher() { return this.defaultRequestDispatcher; } public RequestDispatcher getJspRequestDispatcher() { return this.jspRequestDispatcher; } @Override public void setServletContext(ServletContext servletContext) { this.servletContext = servletContext; } public ServletContext getServletContext() { return this.servletContext; } @Override public String toString() { final StringBuilder sb = new StringBuilder("TdsContext{"); sb.append("webappName='").append(webappName).append('\''); sb.append("\n contextPath='").append(contextPath).append('\''); sb.append("\n webappVersion='").append(webappVersion).append('\''); sb.append("\n webappVersionBuildDate='").append(webappVersionBuildDate).append('\''); sb.append("\n contentRootPath='").append(contentRootPath).append('\''); sb.append("\n contentPath='").append(contentPath).append('\''); sb.append("\n tdsConfigFileName='").append(tdsConfigFileName).append('\''); sb.append("\n startupContentPath='").append(startupContentPath).append('\''); sb.append("\n webinfPath='").append(webinfPath).append('\''); sb.append("\n rootDirectory=").append(rootDirectory); sb.append("\n contentDirectory=").append(contentDirectory); sb.append("\n publicContentDirectory=").append(publicContentDirectory); sb.append("\n tomcatLogDir=").append(tomcatLogDir); sb.append("\n startupContentDirectory=").append(startupContentDirectory); sb.append("\n rootDirSource=").append(rootDirSource); sb.append("\n contentDirSource=").append(contentDirSource); sb.append("\n publicContentDirSource=").append(publicContentDirSource); sb.append("\n startupContentDirSource=").append(startupContentDirSource); sb.append("\n configSource=").append(configSource); sb.append("\n publicDocSource=").append(publicDocSource); sb.append('}'); return sb.toString(); } }