/*
* #!
* Ontopia Engine
* #-
* 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.entry;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import net.ontopia.utils.OntopiaRuntimeException;
import net.ontopia.utils.PropertyUtils;
import net.ontopia.utils.StringUtils;
import net.ontopia.utils.URIUtils;
import net.ontopia.xml.DefaultXMLReaderFactory;
import net.ontopia.xml.Slf4jSaxErrorHandler;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* PUBLIC: Reads store configuration parameters from an XML
* file. The config source is able to handle multiple sources at the
* same time by using an instance of the {@link TopicMapRepositoryIF}
* class.<p>
*
* The class understands XML documents using the following DTD:</p>
*
* <pre>
* <!ELEMENT repository (source+) >
* <!ELEMENT source (param*) >
* <!ATTLIST source id ID #IMPLIED >
* <!ATTLIST source class CDATA #REQUIRED >
* <!ELEMENT param EMPTY >
* <!ATTLIST param name CDATA #REQUIRED
* value CDATA #REQUIRED >
* </pre>
*
* <b>Example:</b></p>
*
* <pre>
* <repository>
* <!-- source that references all .xtm files in a directory -->
* <source class="net.ontopia.topicmaps.xml.XTMPathTopicMapSource">
* <param name="path" value="/ontopia/topicmaps"/>
* <param name="suffix" value=".xtm"/>
* </source>
* <!-- source that references a topic map in a relational database -->
* <source class="net.ontopia.topicmaps.impl.rdbms.RDBMSSingleTopicMapSource">
* <param name="topicMapId" value="5001"/>
* <param name="title" value="My RDBMS Topic Map"/>
* <param name="referenceId" value="mytm"/>
* <param name="propertyFile" value="${CWD}/db.postgresql.props"/>
* </source>
* </repository>
* </pre>
*
* This example makes XMLConfigSource use a TopicMapRepositoryIF that
* contains two other topic maps sources, namely instances of the two
* specifed in the class attributes of the source elements. Note that
* the classes must have empty constructors for them to be used with
* this class.</p>
*
* The two sources would locate all XTM files with the .xtm extension
* in the /ontopia/topicmaps directory in which the config file is
* located.</p>
*
* The <code>param</code> element is used to set bean properties on
* the source instance. This way the config source can be configured.</p>
*
* <p><b>Environment variables:</b></p>
*
* <p>XMLConfigSource is able to replace environment variables in the
* param value attribute. The only environment variable available at
* this time is ${CWD}, which contains the full path of the directory
* in which the config file is located.</p>
*
* <p>NOTE: Topic map sources with supportsCreate set to true will get
* ids assigned automatically. This is done so that the sources can be
* referred to from the outside.</p>
*
*/
public class XMLConfigSource {
// Define a logging category.
static Logger log = LoggerFactory.getLogger(XMLConfigSource.class.getName());
/**
* INTERNAL: Don't call constructor directly. Instead used static
* factory methods.
*/
private XMLConfigSource() {
}
/**
* PUBLIC: Get the topic map repository that is created by loading
* the 'tm-sources.xml' configuration file from the classpath.<p>
*
* @since 3.0
*/
public static TopicMapRepositoryIF getRepositoryFromClassPath() {
return getRepositoryFromClassPath("tm-sources.xml");
}
/**
* PUBLIC: Get the topic map repository that is created by loading
* the named resource from the classpath.<p>
*
* @since 3.0
*/
public static TopicMapRepositoryIF getRepositoryFromClassPath(String resourceName) {
return getRepositoryFromClassPath(resourceName, null);
}
/**
* INTERNAL:
*/
public static TopicMapRepositoryIF getRepositoryFromClassPath(Map<String, String> environ) {
return getRepositoryFromClassPath("tm-sources.xml", environ);
}
/**
* INTERNAL:
*/
public static TopicMapRepositoryIF getRepositoryFromClassPath(String resourceName, Map<String, String> environ) {
// look up configuration via classpath
ClassLoader cl = Thread.currentThread().getContextClassLoader();
URL url = cl.getResource(resourceName);
if (url == null)
throw new OntopiaRuntimeException("Could not find resource '" + resourceName + "' on CLASSPATH.");
// build configuration environment
if (environ == null)
environ = new HashMap<String, String>(1);
if (url.getProtocol().equals("file")) {
String file = url.getFile();
environ.put("CWD", file.substring(0, file.lastIndexOf('/')));
} else
environ.put("CWD", ".");
// read configuration and create the repository instance
try {
return createRepository(readSources(new InputSource(url.openStream()),
environ));
} catch (IOException e) {
throw new OntopiaRuntimeException(e);
}
}
/**
* PUBLIC: Gets the topic map repository that is created by reading
* the configuration file.<p>
*/
public static TopicMapRepositoryIF getRepository(String config_file) {
return createRepository(readSources(config_file));
}
/**
* INTERNAL: Gets the topic map repository that is created by
* reading the configuration file with the given environment.<p>
*/
public static TopicMapRepositoryIF getRepository(String config_file, Map<String, String> environ) {
return createRepository(readSources(config_file, environ));
}
/**
* PUBLIC: Gets the topic map repository that is created by reading
* the configuration file from the reader.<p>
*
* @since 3.0
*/
public static TopicMapRepositoryIF getRepository(Reader config_file) {
return createRepository(readSources(new InputSource(config_file), null));
}
/**
* INTERNAL: Gets the topic map repository that is created by
* reading the configuration file from the reader with the given
* environment.<p>
*
* @since 3.0
*/
public static TopicMapRepositoryIF getRepository(Reader config_file, Map<String, String> environ) {
return createRepository(readSources(new InputSource(config_file), environ));
}
private static TopicMapSourceManager createRepository(Collection<TopicMapSourceIF> sources) {
// assign default source ids and titles
int counter = 1;
Iterator<TopicMapSourceIF> iter = sources.iterator();
while (iter.hasNext()) {
TopicMapSourceIF source = iter.next();
if (source.getId() == null && source.supportsCreate()) {
String newId = source.getClass().getName() + "-" + (counter++);
source.setId(newId);
if (source.getTitle() == null)
source.setTitle(newId);
}
}
return new TopicMapSourceManager(sources);
}
/**
* INTERNAL: Returns a collection containing the topic map sources
* created by reading the configuration file.
*/
public static List<TopicMapSourceIF> readSources(String config_file) {
return readSources(config_file, new HashMap<String, String>(1));
}
/**
* INTERNAL: Returns a collection containing the topic map sources
* created by reading the configuration file.
*/
public static List<TopicMapSourceIF> readSources(String config_file, Map<String, String> environ) {
if (environ == null) environ = new HashMap<String, String>(1);
// add CWD entry
if (!environ.containsKey("CWD")) {
File file = new File(config_file);
if (!file.exists())
throw new OntopiaRuntimeException("Config file '" + config_file +
"' does not exist.");
environ.put("CWD", file.getParent());
}
String url = URIUtils.getURI(config_file).getAddress();
return readSources(new InputSource(url), environ);
}
// ------------------------------------------------------------
// internal helper method(s)
// ------------------------------------------------------------
private static List<TopicMapSourceIF> readSources(InputSource inp_source, Map<String, String> environ) {
ConfigHandler handler = new ConfigHandler(environ);
try {
XMLReader parser = DefaultXMLReaderFactory.createXMLReader();
parser.setContentHandler(handler);
parser.setErrorHandler(new Slf4jSaxErrorHandler(log));
parser.parse(inp_source);
} catch (SAXParseException e) {
String msg = "" + e.getSystemId() + ":" + e.getLineNumber() + ":" +
e.getColumnNumber() + ": " + e.getMessage();
throw new OntopiaRuntimeException(msg, e);
} catch (Exception e) {
throw new OntopiaRuntimeException(e);
}
return handler.sources;
}
// ------------------------------------------------------------
// internal ContentHandler class
// ------------------------------------------------------------
static class ConfigHandler extends DefaultHandler {
Map<String, String> environ;
//Map params = new HashMap();
List<TopicMapSourceIF> sources = new ArrayList<TopicMapSourceIF>();
TopicMapSourceIF source;
ConfigHandler(Map<String, String> environ) {
this.environ = environ;
}
public void startElement(String uri, String name, String qName,
Attributes atts) throws SAXException {
if (qName.equals("source")) {
// Clear source member
source = null;
try {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
source = (TopicMapSourceIF) Class.forName(atts.getValue("class"), true, classLoader).newInstance();
String id = atts.getValue("id");
if (id != null)
source.setId(id);
sources.add(source);
//log.debug("Added source " + source + ".");
} catch (ClassNotFoundException e) {
log.error("Cannot find class " + e.getMessage());
} catch (Exception e) {
log.error("Exception: " + e.getClass().getName() + ": " + e.getMessage());
}
}
else if (qName.equals("param") && source != null) {
String param_name = atts.getValue("name");
String param_value = atts.getValue("value");
Iterator<String> iter = environ.keySet().iterator();
while (iter.hasNext()) {
String environ_key = iter.next();
param_value = StringUtils.replace(param_value, "${" + environ_key + "}", environ.get(environ_key));
}
try {
BeanInfo bean_info = Introspector.getBeanInfo(source.getClass());
PropertyDescriptor[] props = bean_info.getPropertyDescriptors();
boolean found_property = false;
for (int i = 0; i < props.length; i++) {
//log.debug("property: " + props[i].getName());
//System.out.println("P: " + props[i].getName() + " T: " + props[i].getPropertyType());
if (props[i].getName().equals(param_name)) {
Method setter = props[i].getWriteMethod();
if (props[i].getPropertyType().equals(String.class)) {
setter.invoke(source, new Object[] {param_value});
found_property = true;
break;
}
else if (props[i].getPropertyType().equals(boolean.class)) {
setter.invoke(source, new Object[] {new Boolean(PropertyUtils.isTrue(param_value))});
found_property = true;
break;
}
}
}
if (!found_property)
throw new SAXException("Cannot find property '" + param_name + "' " +
"on source " + source);
} catch (IntrospectionException e) {
throw new SAXException(e);
} catch (InvocationTargetException e) {
throw new SAXException(e);
} catch (IllegalAccessException e) {
throw new SAXException(e);
}
}
}
public void endElement(String uri, String name, String qName) throws SAXException {
if (qName.equals("source")) {
source = null;
}
}
}
}