/*
* Copyright 2004, 2005, 2006 Odysseus Software GmbH
*
* 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 de.odysseus.calyxo.base.conf.impl;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.apache.commons.digester.Digester;
import org.apache.commons.digester.Rule;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.xml.sax.Attributes;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;
import de.odysseus.calyxo.base.ModuleContext;
import de.odysseus.calyxo.base.conf.ConfigException;
import de.odysseus.calyxo.base.conf.ImportConfig;
import de.odysseus.calyxo.base.util.PropertyUtils;
/**
* A parser base class.
* <p/>
* The parser parses from xml configuration files using the
* commons digester. The files are merged together into the final
* configuration. Variables are resolved before merging trees, so
* local scope is limited to a file.
* <p/>
* Subclasses have to implement methods for root and digester creation.
* The parser parses from several inputs.
*
* @author Christoph Beck
*/
public abstract class RootConfigImplParser {
private static final Log log = LogFactory.getLog(RootConfigImplParser.class);
/**
* Resolve schema location.
* The schema definition systemId is matched against the
* file part of the resolver's path. That is, a schema resolver
* with path <code>/org/my/app/myschema.xsd</code> will match
* all systemIds ending with <code>myschema.xsd</code>.
*/
public static class SchemaResolver implements EntityResolver {
private static final String basePath =
"/de/odysseus/calyxo/base/conf/impl/calyxo-base-config.xsd";
private static final String baseSuffix =
basePath.substring(basePath.lastIndexOf('/') + 1);
private final String suffix;
private final String uri;
public SchemaResolver(String path) {
suffix = path.substring(path.lastIndexOf('/') + 1);
uri = getClass().getResource(path).toString();
}
public InputSource resolveEntity(String publicId, String systemId)
throws SAXException, IOException {
if (publicId == null && systemId != null) {
if (systemId.endsWith(suffix)) {
return new InputSource(uri);
}
if (systemId.endsWith(baseSuffix)) {
return new InputSource(getClass().getResource(basePath).toString());
}
}
return null;
}
public String getURI() {
return uri;
}
}
/**
* Resolve schema location by associating a public id with an uri.
* If the passed publicId is equal to the resolver's publicId,
* resolve it to the uri.
*/
public static class DTDResolver implements EntityResolver {
private static final String baseDTDPublicId =
"-//Odysseus Software GmbH//DTD Calyxo Base 0.9//EN";
private static final String baseDTDPath =
"/de/odysseus/calyxo/base/conf/impl/calyxo-base-config.dtd";
private final String publicId;
private final String path;
public DTDResolver(String publicId, String path) {
this.publicId = publicId;
this.path = path;
}
public InputSource resolveEntity(String publicId, String systemId)
throws SAXException, IOException {
if (publicId != null) {
if (publicId.equals(this.publicId)) {
return new InputSource(getClass().getResource(path).toString());
}
if (publicId.equals(baseDTDPublicId)) {
return new InputSource(getClass().getResource(baseDTDPath).toString());
}
}
return null;
}
}
/**
* This rule assigns a generic unique id to the top object.
*/
protected class UniqueIdRule extends Rule {
private String prefix;
private String property;
private int index = 1;
/**
* Constructor
*/
public UniqueIdRule(String prefix, String property) {
super();
this.prefix = prefix;
this.property = property;
}
/**
* Assign new id to top object.
*/
public void begin(String namespace, String name, Attributes attributes) throws Exception {
String id = prefix + index++;
if (digester.getLogger().isDebugEnabled()) {
digester.getLogger().debug(
"[UniqueIdRule]{" + digester.getMatch() +
"} Set " + property + " to " + id);
}
PropertyUtils.setProperty(digester.peek(), property, id);
}
}
private SAXParserFactory factory;
private ModuleContext context;
private DTDResolver dtdResolver;
private SchemaResolver schemaResolver;
/**
* Constructor.
* Since no context is provided, context-relative files cannot
* be imported using absolute paths like <code>/WEB-INF/foo.xml</code>
* when using this constructor.
*/
public RootConfigImplParser() {
this(null);
}
/**
* Constructor.
* @param context Used to lookup absolute import paths.
*/
public RootConfigImplParser(ModuleContext context) {
this.context = context;
dtdResolver = getDtdResolver();
schemaResolver = getSchemaResolver();
factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true);
factory.setValidating(dtdResolver != null || schemaResolver != null);
}
protected void addBaseRuleInstances(Digester digester) {
digester.setRuleNamespaceURI("http://calyxo.odysseus.de/xml/ns/base");
// Handle import elements
digester.addObjectCreate("*/import", ImportConfigImpl.class.getName());
digester.addSetProperties("*/import");
digester.addSetNext("*/import", "add", ImportConfigImpl.class.getName());
// Handle functions elements
digester.addObjectCreate("*/functions", FunctionsConfigImpl.class.getName());
digester.addSetProperties("*/functions", "class", "className");
digester.addSetNext("*/functions", "add", FunctionsConfigImpl.class.getName());
// Handle set elements
digester.addObjectCreate("*/set", SetConfigImpl.class.getName());
digester.addSetProperties("*/set", "scope", "scopeName");
digester.addSetNext("*/set", "add", SetConfigImpl.class.getName());
// Handle use elements
digester.addObjectCreate("*/use", UseConfigImpl.class.getName());
digester.addSetProperties("*/use");
digester.addSetNext("*/use", "add", UseConfigImpl.class.getName());
// Handle property elements
digester.addObjectCreate("*/property", PropertyConfigImpl.class.getName());
digester.addSetProperties("*/property");
digester.addSetNext("*/property", "add", PropertyConfigImpl.class.getName());
// Handle method elements
digester.addObjectCreate("*/use/method", MethodConfigImpl.class.getName());
digester.addSetNext("*/use/method", "add", MethodConfigImpl.class.getName());
digester.addSetProperties("*/use/method");
digester.addObjectCreate("*/object/method", MethodConfigImpl.class.getName());
digester.addSetNext("*/object/method", "add", MethodConfigImpl.class.getName());
digester.addSetProperties("*/object/method");
digester.addObjectCreate("*/member/method", MethodConfigImpl.class.getName());
digester.addSetNext("*/member/method", "setMethodConfig", MethodConfigImpl.class.getName());
digester.addSetProperties("*/member/method");
// Handle field elements
digester.addObjectCreate("*/member/field", FieldConfigImpl.class.getName());
digester.addSetNext("*/member/field", "setFieldConfig", FieldConfigImpl.class.getName());
digester.addSetProperties("*/member/field");
// Handle object elements
digester.addObjectCreate("*/object", ObjectConfigImpl.class.getName());
digester.addSetProperties("*/object", "class", "className");
digester.addSetNext("*/object", "setObjectConfig", ObjectConfigImpl.class.getName());
// Handle constructor elements
digester.addObjectCreate("*/constructor", ConstructorConfigImpl.class.getName());
digester.addSetNext("*/constructor", "setConstructorConfig", ConstructorConfigImpl.class.getName());
digester.addSetProperties("*/constructor");
// Handle member elements
digester.addObjectCreate("*/member", MemberConfigImpl.class.getName());
digester.addSetProperties("*/member", "class", "className");
digester.addSetNext("*/member", "setMemberConfig", MemberConfigImpl.class.getName());
// Handle arg elements
digester.addObjectCreate("*/arg", ArgConfigImpl.class.getName());
digester.addSetProperties("*/arg", "class", "className");
digester.addSetNext("*/arg", "add", ArgConfigImpl.class.getName());
}
/**
* Create digester with validating, namespace aware parser.
* This method has been added, since configuration for
* schema validation seems to be broken in digester 1.6.
* Therefore, we directly set the JAXP properties.
*/
protected Digester createDigester() {
SAXParser parser = null;
try {
parser = factory.newSAXParser();
} catch (Exception e) {
// should not happen...
}
if (schemaResolver != null) {
try {
parser.setProperty(
"http://java.sun.com/xml/jaxp/properties/schemaLanguage",
"http://www.w3.org/2001/XMLSchema"
);
parser.setProperty(
"http://java.sun.com/xml/jaxp/properties/schemaSource",
getSchemaResolver().getURI()
);
} catch (Exception e) {
log.info("Parser doesn't support XML Schema validation, using DTD...");
log.debug(e);
}
}
final Digester digester = new Digester(parser);
digester.setLogger(getLog());
digester.setClassLoader(context.getClassLoader());
digester.setErrorHandler(new DefaultHandler() {
public void error(SAXParseException e) throws SAXException {
throw e;
}
public void warning(SAXParseException e) throws SAXException {
getLog().warn(e.getMessage());
}
});
digester.setEntityResolver(new EntityResolver() {
public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
InputSource result = null;
if (schemaResolver != null) {
result = schemaResolver.resolveEntity(publicId, systemId);
}
if (result == null && dtdResolver != null) {
result = dtdResolver.resolveEntity(publicId, systemId);
}
if (result != null) {
return result;
}
return digester.resolveEntity(publicId, systemId);
}
});
addBaseRuleInstances(digester);
addRuleInstances(digester);
return digester;
}
/**
* Create root element
*/
protected abstract RootConfigImpl createRoot();
/**
* Get the dtd resolver. If there's no dtd support, return null.
* To be implemented by subclasses.
*/
protected abstract DTDResolver getDtdResolver();
/**
* Get the schema resolver. If there's no schema support, return null.
* To be implemented by subclasses.
*/
protected abstract SchemaResolver getSchemaResolver();
/**
* Add rules
*/
protected abstract void addRuleInstances(Digester digester);
/**
* Get the log instance to be passed to the digester.
*/
protected Log getLog() {
return log;
}
private RootConfigImpl parse(URL url) throws ConfigException {
Digester digester = createDigester();
RootConfigImpl root = createRoot();
digester.push(root);
log.debug("Parsing: " + url);
try {
InputSource input = new InputSource(url.openStream());
input.setSystemId(url.toString());
digester.parse(input);
} catch (Exception e) {
throw new ConfigException("Error parsing " + url, e);
}
// Parse imported files
Iterator imports = root.getImportConfigs();
ArrayList children = null;
if (imports.hasNext()) {
children = new ArrayList();
while (imports.hasNext()) {
String file = ((ImportConfig)imports.next()).getFile();
URL childURL = null;
try {
if (file.startsWith("/")) {
if (context != null) {
childURL = context.getServletContext().getResource(file);
}
if (childURL == null) {
childURL = getClass().getResource(file);
}
} else {
childURL = new URL(url, file);
}
if (childURL == null) {
throw new ConfigException("Could not find file: '" + file + "'");
}
} catch (MalformedURLException e) {
throw new ConfigException("Bad file path: '" + file + "'", e);
}
children.add(parse(childURL));
}
}
root.resolve(context);
if (children != null) {
for (int i = 0; i < children.size(); i++) {
root.merge((RootConfigImpl)children.get(i));
}
}
return root;
}
/**
* Parse specified urls merge and initialize resulting trees
* @param urls pointing to xml configuration files
* @throws ConfigException on error during parse or initialization
*/
protected RootConfigImpl parseAll(URL[] urls) throws ConfigException {
RootConfigImpl root = parse(urls[0]);
for (int i = 1; i < urls.length; i++) {
root.merge(parse(urls[i]));
}
root.initialize();
if (log.isDebugEnabled()) {
log.debug(root.toXML());
}
return root;
}
}