/* * #! * Ontopia Navigator * #- * Copyright (C) 2001 - 2013 The Ontopia Project * #- * 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.ontopia.topicmaps.nav2.impl.basic; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.InputStream; import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.servlet.ServletContext; import net.ontopia.topicmaps.core.TopicMapIF; import net.ontopia.topicmaps.core.TopicMapStoreIF; import net.ontopia.topicmaps.entry.SharedStoreRegistry; import net.ontopia.topicmaps.entry.TopicMapReferenceIF; import net.ontopia.topicmaps.entry.TopicMapRepositoryIF; import net.ontopia.topicmaps.entry.TopicMaps; import net.ontopia.topicmaps.entry.UserStoreRegistry; import net.ontopia.topicmaps.nav2.core.ModuleIF; import net.ontopia.topicmaps.nav2.core.NavigatorApplicationIF; import net.ontopia.topicmaps.nav2.core.NavigatorConfigurationIF; import net.ontopia.topicmaps.nav2.core.NavigatorRuntimeException; import net.ontopia.topicmaps.nav2.plugins.PluginConfigFactory; import net.ontopia.topicmaps.nav2.plugins.PluginIF; import net.ontopia.topicmaps.nav2.utils.NavigatorConfigFactory; import net.ontopia.utils.OntopiaRuntimeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; /** * INTERNAL: Basic Implementation of interface NavigatorApplicationIF to * store all handles to application-wide configuration holders needed * by the navigator framework. * <p> * Note: The default behaviour if no plug-ins dir is specified (in web.xml) * that there will be no plug-ins available. */ public final class NavigatorApplication implements NavigatorApplicationIF { // initialization of log facility private static Logger log = LoggerFactory.getLogger(NavigatorApplication.class.getName()); // the name of a plug-in specification file in a plug-in directory private static final String PLUGIN_SPEC = "plugin.xml"; // topic map repository private TopicMapRepositoryIF repository; private boolean localRepository; private String repositoryId; // members private NavigatorConfigurationIF navConfig; // key: shortcut (String), value: fqcn (String) private Map instances = new HashMap(); // key: filename (String), value: module (ModuleIF) private Map modules = new HashMap(); private ServletContext servlet_context; /** * INTERNAL: Default Constructor. * * @param context - The ServletContext object, which is * needed to retrieve the context and configuration information * about this web application. */ public NavigatorApplication(ServletContext context) { this.servlet_context = context; servlet_context.log("Start constructing NavigatorApplication object."); init(); log.info("NavigatorApplication initialized for '" + getName() + "'."); } private InputStream getInputStream(String name) throws IOException { // adapted from StreamUtils.getInputStream(String) InputStream istream; if (name.startsWith("classpath:")) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); String resourceName = name.substring("classpath:".length()); istream = cl.getResourceAsStream(resourceName); if (istream == null) throw new IOException("Resource '" + resourceName + "' not found through class loader."); log.debug("File loaded through class loader: " + name); } else if (name.startsWith("file:")) { File f = new File(name.substring("file:".length())); if (f.exists()) { log.debug("File loaded from file system: " + name); istream = new FileInputStream(f); } else throw new IOException("File '" + f + "' not found."); } else { String absname = makeAbsolute(name); File f = new File(absname); if (f.exists()) { log.debug("File loaded from file system: " + absname); istream = new FileInputStream(f); } else { ClassLoader cl = Thread.currentThread().getContextClassLoader(); istream = cl.getResourceAsStream(name); if (istream != null) log.debug("File loaded through class loader: " + name); } } return istream; } private String makeAbsolute(String filename) { if (filename != null && filename.length() > 0 && !(new java.io.File(filename).isAbsolute())) // Filename is relative to the webapp root directory return servlet_context.getRealPath("") + "/" + filename; else return filename; } // ------------------------------------------------------------ // Implementation of NavigatorApplicationIF // ------------------------------------------------------------ /** * @return Display name of the web application {@link * javax.servlet.ServletContext#getServletContextName()}. */ public String getName() { return JSPEngineWrapper.getServletContextName(servlet_context); } public NavigatorConfigurationIF getConfiguration() { return navConfig; } public TopicMapRepositoryIF getTopicMapRepository() { return repository; } @Deprecated public UserStoreRegistry getUserStoreRegistry() { throw new UnsupportedOperationException("Method is no longer supported."); } /** * INTERNAL: Reloads application.xml and plug-in configurations. * * @deprecated */ @Deprecated public void refreshAppConfig() { readInAppConfig(); // refresh also plug-ins because they are assigned to navigation configuration readAndSetPlugins(); } public TopicMapIF getTopicMapById(String topicmapId) throws NavigatorRuntimeException { return getTopicMapById(topicmapId, false); } public TopicMapIF getTopicMapById(String topicmapId, boolean readonly) throws NavigatorRuntimeException { // use local or jndi repository (for backwards compatibility); TopicMapReferenceIF ref = repository.getReferenceByKey(topicmapId); if (ref == null) throw new NavigatorRuntimeException("Topic map with id '" + topicmapId + "' not found."); TopicMapStoreIF store; try { store = ref.createStore(readonly); } catch (java.io.IOException e) { throw new NavigatorRuntimeException("Problems occurred when creating topic map store '" + topicmapId + "'", e); } if (readonly) log.debug("Got RO store: " + store); else log.debug("Got RW store: " + store); return store.getTopicMap(); } public void returnTopicMap(TopicMapIF topicmap) { TopicMapStoreIF store = topicmap.getStore(); log.debug("Returning store: " + store); store.close(); } public String getTopicMapRefId(TopicMapIF topicmap) { TopicMapStoreIF store = topicmap.getStore(); TopicMapReferenceIF ref = store.getReference(); return (ref == null ? null : getTopicMapRepository().getReferenceKey(ref)); } public Object getInstanceOf(String classname) throws NavigatorRuntimeException { String fqcn = null; // First try if this is an shortcut for a classname fqcn = navConfig.getClass(classname); // ...otherwise fallback to interpret parameter as full-qualified if (fqcn == null || fqcn.equals("")) fqcn = classname; // lookup if we have an instance available for this class Object instance = instances.get(fqcn); // ... otherwise try to create a new instance of it if (instance == null) { try { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); Class klass = Class.forName(fqcn, true, classLoader); instance = klass.newInstance(); instances.put(fqcn, instance); } catch (ClassNotFoundException e) { throw new NavigatorRuntimeException("Class '" + fqcn + "' not found.", e); } catch (LinkageError e) { String msg = "Unable to create instance of class '" + fqcn + "': " + e; throw new NavigatorRuntimeException(msg, e); } catch (InstantiationException e) { String msg = "Unable to create instance of class '" + fqcn + "': " + e; throw new NavigatorRuntimeException(msg, e); } catch (IllegalAccessException e) { String msg = "Unable to create instance of class '" + fqcn + "': " + e; throw new NavigatorRuntimeException(msg, e); } } return instance; } /** * Gets module from internal cache, if it is not in or should be * refreshed the module resource is loaded in again. */ public ModuleIF getModule(URL location) throws NavigatorRuntimeException { ModuleIF module = null; Object obj_module = modules.get(location); // --- no appropiate module found, create a new one if (obj_module == null) { String reader_type = navConfig .getProperty(NavigatorConfigurationIF.MODULE_READER); module = new Module(location, reader_type); module.readIn(); // put it into hashmap modules.put(location, module); } // --- reuse same module object instance else { module = (ModuleIF) obj_module; // check if modified in the meantime if (navConfig.getProperty(NavigatorConfigurationIF.CHECK_FOR_CHANGED_MODULES, "false").equals("true")) { if (module.hasResourceChanged()) { log.info("The module file has changed and is being reloaded."); module.readIn(); } } } return module; } public void close() { if (repository != null && localRepository) { try { // only close local repositories repository.close(); } catch (Throwable t) { log.error("Problems occurred when closing navigator application.", t); } } } // ------------------------------------------------------------ // internal helper methods // ------------------------------------------------------------ /** * INTERNAL: Sets up needed instances for constructors. */ private void init() { // --- set up logging with configuration from property file configureLogging(); // --- read in application configuration readInAppConfig(); // --- read in topicmap source configuration loadTopicMapRepository(); // --- scan for plug-in configuration files and read them in readAndSetPlugins(); } /** * INTERNAL: Configures the logging facility (Log4J) with the help * of the property file containing configuration issues.. */ private synchronized void configureLogging() { //! Does the removal of this code cause probelems? //! SLF4J is not meant to be configured /* // load in properties String filePathLog4J = servlet_context.getInitParameter(LOG4J_CONFIG_KEY); if (filePathLog4J == null) log.info("Logging not forced."); else { Properties props = new Properties(); servlet_context.log("start to load log4j configuration from " + filePathLog4J); try { props.load(getInputStream(filePathLog4J)); } catch (IOException ioe) { throw new OntopiaRuntimeException("Unable to load log configuration.", ioe); } // pre-cat the real directory to all file properties Enumeration pnames = props.propertyNames(); while (pnames.hasMoreElements()) { String pname = (String) pnames.nextElement(); String pval = props.getProperty(pname); if (pname.endsWith(".File")) { try { // check if directory is available File dir = (new File(pval)).getParentFile(); if (dir != null && !dir.exists()) { dir.mkdirs(); servlet_context.log("Created log directory " + dir); } props.setProperty(pname, pval); } catch (SecurityException ioe) { servlet_context.log("Not allowed to create log dir, " + ioe); } } } // finally configure Log4J with modified paths PropertyConfigurator.configure(props); log.info("Configured Logging with properties from " + filePathLog4J); } */ } /** * INTERNAL: Reads in one application configuration XML file. */ private synchronized void readInAppConfig() { try { String filePathAppConfig = servlet_context.getInitParameter(APP_CONFIG_KEY); if (filePathAppConfig == null) filePathAppConfig = APP_CONFIG_DEFAULT_VALUE; log.info("Start to load application configuration from " + filePathAppConfig); InputStream istream = getInputStream(filePathAppConfig); if (istream != null) { this.navConfig = NavigatorConfigFactory.getConfiguration(istream); return; } } catch (SAXParseException e) { log.error("Couldn't parse application configuration.", e); this.navConfig = new BrokenNavigatorConfiguration("Couldn't parse application configuration: " + e.getMessage() + " at: " + e.getSystemId() + ":" + e.getLineNumber() + ":" + e.getColumnNumber()); return; } catch (SAXException e) { log.error("Couldn't parse application configuration.", e); this.navConfig = new BrokenNavigatorConfiguration("Couldn't parse application configuration: " + e.getMessage()); return; } catch (IOException e) { log.error("Couldn't read in application configuration: " + e.getMessage()); } this.navConfig = new NavigatorConfiguration(); } /** * INTERNAL: Loads the topic map repository, which stores * information about which topicmaps should be made available to the * web application. */ private synchronized void loadTopicMapRepository() { // NOTE: if neither of the two properties are specified we'll use the default repository // get shared registry from JNDI String jndi_repository = servlet_context.getInitParameter(JNDI_REPOSITORY_KEY); if (jndi_repository != null) { SharedStoreRegistry ssr = lookupSharedStoreRegistry(jndi_repository); this.repository = ssr.getTopicMapRepository(); this.localRepository = false; log.warn("Navigator application '" + getName() + "' loaded topic maps repository: " + this.repository + " from JNDI '" + jndi_repository + "'. WARNING: you may not want to do this."); return; // we're done } // load topic map repository from specific file String source_config = servlet_context.getInitParameter(SOURCE_CONFIG_KEY); if (source_config != null) { Map<String, String> environ = new HashMap<String, String>(Collections.singletonMap("WEBAPP", servlet_context.getRealPath(""))); if (source_config.startsWith("classpath:")) { this.repository = TopicMaps.getRepository(source_config, environ); log.info("Navigator application '" + getName() + "' loaded topic maps repository: " + this.repository + " from file '" + source_config + "'"); } else { String source_config_file = makeAbsolute(source_config); this.repository = TopicMaps.getRepository("file:" + source_config_file, environ); log.info("Navigator application '" + getName() + "' loaded topic maps repository: " + this.repository + " from file '" + source_config_file + "'"); } this.localRepository = true; return; // we're done } // application can specify specific repository id this.repositoryId = servlet_context.getInitParameter(TOPICMAPS_REPOSITORY_ID); if (this.repositoryId != null) { this.repository = TopicMaps.getRepository(this.repositoryId); log.info("Navigator application '" + getName() + "' will use shared topic maps repository with id '" + this.repositoryId + "'"); return; // we're done } // fallback to default repository this.repository = TopicMaps.getRepository(); log.info("Navigator application '" + getName() + "' will use default shared topic maps repository"); } /** * INTERNAL: Reloads the topic map repository, which stores * information about which topicmaps should be made available to the * web application. * * @deprecated */ @Deprecated public synchronized void refreshTopicMapRegistry() { //! WARNING: the below is inherently unsafe, so it has been disabled } /** * INTERNAL: Looks up the SharedStoreRegistry in JNDI. */ public static SharedStoreRegistry lookupSharedStoreRegistry(String jndi_name) { log.info("Looking up shared store registry in JNDI '" + jndi_name + "'"); // lookup in jndi using Tomcat approach SharedStoreRegistry ssr = (SharedStoreRegistry)lookupJNDI("java:comp/env/" + jndi_name); if (ssr == null) // lookup in jndi using oc4j approach ssr = (SharedStoreRegistry)lookupJNDI("java:comp/resource/" + jndi_name + "/"); if (ssr == null) throw new OntopiaRuntimeException("Couldn't find shared repository " + jndi_name); return ssr; } private static SharedStoreRegistry lookupJNDI(String name) { try { log.info("Looking up JNDI name: " + name); Context ctx = new InitialContext(); SharedStoreRegistry ssr = (SharedStoreRegistry) ctx.lookup(name); if (ssr == null) throw new OntopiaRuntimeException("No JNDI shared repository named " + name + " found"); return ssr; } catch (NamingException e) { return null; } } /** * INTERNAL: Processes list of plug-in specification resources * starting from the given base path and add all therein contained * plug-ins to navigator configuration. */ private synchronized void readAndSetPlugins() { String pluginsRootURI = servlet_context.getInitParameter(PLUGINS_ROOTDIR_KEY); // it is allowed to specify no plug-in directory if (pluginsRootURI == null) return; Collection pluginspecs = getPluginSpecifications(makeAbsolute(pluginsRootURI)); Iterator it = pluginspecs.iterator(); while (it.hasNext()) { File specfile = (File) it.next(); String plugindir = specfile.getParent(); try { // Reads in one plug-in XML instance and generate PluginIF // instances from it. Collection plugins = PluginConfigFactory.getPlugins(new FileInputStream(specfile), plugindir, pluginsRootURI); // Loop over the plug-in inside one directory Iterator iter = plugins.iterator(); while (iter.hasNext()) { PluginIF plugin = (PluginIF) iter.next(); if (plugin != null) { log.info(" * found " + plugin); navConfig.addPlugin( plugin ); } } // while } catch (FileNotFoundException e) { throw new OntopiaRuntimeException("Could not find plugin directory: " + plugindir); } } // while it log.info("Loaded plug-in configuration."); } /** * INTERNAL: Scans directories for appropiate xml instances which * specify plug-in properties. * * @return Collection of File objects referencing the specification files. */ private Collection getPluginSpecifications(String startPath) { File directory = new File(startPath); if (!directory.exists()) { log.info("Plugins directory does not exist '" + startPath + "'"); return Collections.EMPTY_SET; } log.info("Scanning directory '" + startPath + "' for plug-ins."); File[] files = directory.listFiles(); // contains String with path to plugin.xml in plug-in dirs List pluginspecs = new ArrayList(); for (int i=0; i < files.length; i++) { File plugindir = files[i]; if (plugindir.isDirectory()) { File pluginspec = new File(plugindir, PLUGIN_SPEC); if (pluginspec.exists()) { pluginspecs.add(pluginspec); } } } return pluginspecs; } }