/*
* 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.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import javax.servlet.jsp.el.ELException;
import javax.servlet.jsp.el.ExpressionEvaluator;
import javax.servlet.jsp.el.FunctionMapper;
import javax.servlet.jsp.el.VariableResolver;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import de.odysseus.calyxo.base.ModuleContext;
import de.odysseus.calyxo.base.conf.Config;
import de.odysseus.calyxo.base.conf.ConfigException;
import de.odysseus.calyxo.base.util.PropertyUtils;
/**
* Base configuration element.
*
* @author Oliver Stuhr
* @author Christoph Beck
*/
public abstract class ConfigImpl implements Config {
protected static final Log log = LogFactory.getLog(ConfigImpl.class);
private static final String[] EMPTY_STRING_ARRAY = new String[0];
interface ConfigImplInitializer {
public void init(ConfigImpl node) throws ConfigException;
}
private static ConfigImplInitializer initCaller = new ConfigImplInitializer() {
public void init(ConfigImpl node) throws ConfigException {
node._init();
if (!node.initCalled) {
log.warn("Node " + node.toInlineString() + " may not have been fully initialized! Check super._init() calls...");
}
}
};
private static ConfigImplInitializer init2Caller = new ConfigImplInitializer() {
public void init(ConfigImpl node) throws ConfigException {
node._init2();
if (!node.init2Called) {
log.warn("Node " + node.toInlineString() + " may not have been fully initialized! Check super._init2() calls...");
}
}
};
/**
* Collect attributes from an element
*/
protected static class Attributes implements Serializable {
private StringBuffer buffer = new StringBuffer();
private boolean closed;
/**
* Add attribute key/value pair
*/
public void put(String key, Object value) {
if (value != null) {
if (!isEmpty()) {
buffer.append(" ");
}
buffer.append(key);
buffer.append("=\"");
buffer.append(value);
buffer.append('"');
}
}
/**
* Append attribute list to specified buffer.
* This will append a string of the form
* <code>key1="value1" key2="value2"</code> ...
*/
void appendTo(StringBuffer s) {
s.append(buffer.toString());
}
/**
* Answer true iff no attributes have been added yet
*/
public boolean isEmpty() {
return buffer.length() == 0;
}
/**
* Answer string representation
*/
public String toString() {
return buffer.toString();
}
/**
* Indicate that all additions have been done.
*/
void close() {
closed = true;
}
/**
* Answer <code>true</code> iff <code>close()</code> has been called.
*/
boolean isClosed() {
return closed;
}
}
/**
* Collect elements (used for children list)
*/
protected static class Elements implements Serializable {
private ArrayList list;
private boolean closed;
/**
* Answer true iff no elements have been added yet
*/
public boolean isEmpty() {
return list == null || list.isEmpty();
}
/**
* Add an element
*/
public void add(Config element) {
if (element != null) {
ConfigImpl config = (ConfigImpl)element;
if (list == null)
list = new ArrayList();
list.add(config);
}
}
/**
* Add elements
*/
public void add(Iterator elements) {
while (elements.hasNext()) {
add((ConfigImpl)elements.next());
}
}
/**
* Answer elements (instanceof <code>ConfigImpl)
*/
Iterator iterator() {
return isEmpty() ? Collections.EMPTY_LIST.iterator() : list.iterator();
}
/**
* Indicate that all additions have been done.
*/
void close() {
closed = true;
}
/**
* Answer <code>true</code> iff <code>close()</code> has been called.
*/
boolean isClosed() {
return closed;
}
}
private static final String TAB = " ";
private ConfigImpl previous;
private ConfigImpl parent;
private Elements children = new Elements();
private Attributes attributes = new Attributes();
private FunctionMapper _parentFunctionMapper;
private boolean resolveCalled = false;
private boolean initCalled = false;
private boolean init2Called = false;
/**
* Answer the element's name.
*/
protected String _getElementName() {
return "unknown";
}
/**
* Add the element's attributes to specifies <code>Attributes</code>.
*/
protected void _putAttributes(Attributes attributes) {
attributes.close();
}
/**
* Add the elements children to specified <code>Elements</code>
*/
protected void _addChildren(Elements elements) {
elements.close();
}
/**
* Answer an array of the element's dynamic property names.
* Dynamic properties may contain variable references in their
* property values, as in <code>/WEB-INF/${layout}/page.jspx</code>.
* These references will be resolved automatically during initialization.
*/
protected String[] _getDynamicAttributes() {
return EMPTY_STRING_ARRAY;
}
/**
* Get the parent element
*/
protected final ConfigImpl _getParent() {
return parent;
}
/**
* Get the previous sibling element
*/
protected final ConfigImpl _getPrevious() {
return previous;
}
/**
* Get the children elements
*/
protected Iterator _getChildren() {
return children.iterator();
}
/**
* Search nearest element on the path towards the root element
* (including the element itself) assignable to specified type.
* @param type element type to find
* @return nearest ancestor element or the element itself
* assignable to specified type
*/
protected ConfigImpl _getNearestAncestorOrSelf(Class type) {
if (type.isAssignableFrom(getClass())) {
return this;
}
return _getNearestAncestor(type);
}
/**
* Search nearest element on the path towards the root element
* (excluding the element itself) assignable to specified type.
* @param type element type to find
* @return nearest ancestor node assignable to specified type
*/
protected ConfigImpl _getNearestAncestor(Class type) {
return parent == null ? null : parent._getNearestAncestorOrSelf(type);
}
/**
* Initialize element.
* When this method is called, <code>_init</code> has been called for
* all nodes before the receiver (according to preorder).
* To be overwritten by subclasses.
* @throws ConfigException
*/
protected void _init() throws ConfigException {
initCalled = true;
}
/**
* Initialize element.
* When this method is called, <code>_init2</code> has been called for
* all nodes before the receiver (according to preorder).
* Also, <code>_init</code> has been called for all nodes.
* To be overwritten by subclasses.
* @throws ConfigException
*/
protected void _init2() throws ConfigException {
init2Called = true;
}
/**
* Log warning if base implementation of <code>_init</code> or
* <code>_init2</code> has not been called (missing call to super...)
*
*/
void checkInitCalled() {
if (!initCalled) {
log.warn("Node " + toInlineString() + " may not have been fully initialized! Check super._init() calls...");
}
if (!init2Called) {
log.warn("Node " + toInlineString() + " may not have been fully initialized! Check super._init2() calls...");
}
}
/**
* Delegate to parent
* @see javax.servlet.jsp.el.FunctionMapper#resolveFunction(java.lang.String, java.lang.String)
*/
public Method resolveFunction(String prefix, String name) {
return _parentFunctionMapper == null ? null : _parentFunctionMapper.resolveFunction(prefix, name);
}
/**
* Evaluate other than dynamic attributes.
* This method is called after <code>_resolve()</code> has been
* called for the subtree rooted at this node.
* @throws ConfigException
*/
protected void _evaluate(ModuleContext context) throws ConfigException {
}
/**
* Resolve values of dynamic properties. The properties are accessed
* using reflection.
* @throws ConfigException if a variable could not been resolved on
* property access error.
*/
protected final void _resolveTree(ModuleContext context) throws ConfigException {
linkSubtree();
resolveSubtree(context);
}
/**
* Initialize tree.
* Call <code>_init()</code> for the elements in the subtree
* rooted at this element.
* @throws ConfigException from an element's <code>_init()</code> method.
*/
protected final void _initTree() throws ConfigException {
linkSubtree();
initSubtree(initCaller);
initSubtree(init2Caller);
}
private void linkSubtree() throws ConfigException {
_addChildren(children = new Elements());
if (!children.isClosed()) {
log.warn("Node " + toInlineString() + " may not have been fully initialized! Check super._addChildren() calls...");
}
_putAttributes(attributes = new Attributes());
if (!attributes.isClosed()) {
log.warn("Node " + toInlineString() + " may not have been fully initialized! Check super._putAttributes() calls...");
}
ConfigImpl previous = null;
Iterator iterator = children.iterator();
while (iterator.hasNext()) {
ConfigImpl child = (ConfigImpl)iterator.next();
child.previous = previous;
child.parent = this;
previous = child;
child.linkSubtree();
}
}
private void resolveSubtree(ModuleContext context) throws ConfigException {
VariableResolver variables = new ConfigImplVariableResolver(context, this);
_parentFunctionMapper = parent; // save file parent (may change after merge)
_resolve(context.getExpressionEvaluator(), variables);
if (!resolveCalled) {
log.warn("Node " + toInlineString() + " may not have been fully initialized! Check super._resolve() calls...");
}
Iterator iterator = children.iterator();
while (iterator.hasNext()) {
ConfigImpl child = (ConfigImpl)iterator.next();
child.resolveSubtree(context);
}
_evaluate(context);
}
/**
* Resolve dynamic attribute expressions.
* This implementation uses <code>_getDynamicAttributes()</code>
* to get a list of attributes whose values may have expressions.
* It reads the corresponding property values, evaluates them
* and finally writes them back.
* <p/>
* Subclasses may override this method to do any special handling
* or to use the evaluator, variable resolver and function mapper
* to do further expression evaluations.
* @param evaluator expression evaluator
* @param variables the variables resolver
* @throws ConfigException
*/
protected void _resolve(ExpressionEvaluator evaluator, VariableResolver variables) throws ConfigException {
String[] dynamicAttributes = _getDynamicAttributes();
for (int i = 0; i < dynamicAttributes.length; i++) {
String attribute = dynamicAttributes[i];
String value = null;
try {
value = (String)PropertyUtils.getProperty(this, attribute);
if (value != null && value.indexOf("${") >= 0) {
Object object = evaluator.evaluate(value, Object.class, variables, this);
Class type = PropertyUtils.getPropertyDescriptor(getClass(), attribute).getPropertyType();
if (type == String.class) {
if (object != null) {
object = object.toString();
}
}
PropertyUtils.setProperty(this, attribute, object);
}
} catch (ELException e) {
throw new ConfigException("Could not evaluate " + toInlineString() + "/@" + attribute + " '" + value + "'", e);
} catch (Exception e) {
throw new ConfigException("Could not access " + toInlineString() + "/@" + attribute, e);
}
}
resolveCalled = true;
}
private void initSubtree(ConfigImplInitializer initializer) throws ConfigException {
if (log.isTraceEnabled()) {
log.trace("Initializing node " + toInlineString() + " begin");
}
initializer.init(this);
Iterator iterator = children.iterator();
while (iterator.hasNext()) {
ConfigImpl child = (ConfigImpl)iterator.next();
child.initSubtree(initializer);
}
if (log.isTraceEnabled()) {
log.trace("Initializing node " + toInlineString() + " end");
}
}
/**
* Append a short string representation to the specified string buffer.
*/
protected void appendInlineString(StringBuffer s) {
if (parent != null) {
parent.appendInlineString(s);
}
s.append("/");
s.append(_getElementName());
Attributes attrs = new Attributes();
_putAttributes(attrs);
if (!attrs.isEmpty()) {
s.append("[");
attrs.appendTo(s);
s.append("]");
}
}
/**
* Answer a short string representation.
*/
public final String toInlineString() {
StringBuffer s = new StringBuffer();
appendInlineString(s);
return s.toString();
}
/**
* Lookup variable.
* Delegate request to previous sibling (if available) or parent element
*/
Object lookupVariable(String name) {
if (previous != null) {
return previous.lookupVariable(name);
} else if (parent != null) {
return parent.lookupVariable(name);
} else {
return null;
}
}
private String toString(String prefix, int depth) {
StringBuffer result = new StringBuffer();
result.append(prefix);
result.append("<");
result.append(_getElementName());
if (!attributes.isEmpty()) {
result.append(" ");
attributes.appendTo(result);
}
if (children.isEmpty())
result.append("/>\n");
else {
result.append(">\n");
if (depth == 0) {
result.append(prefix + TAB + "...\n");
} else {
Iterator elements = children.iterator();
while (elements.hasNext()) {
ConfigImpl element = (ConfigImpl)elements.next();
result.append(element.toString(prefix + TAB, depth - 1));
}
}
result.append(prefix);
result.append("</");
result.append(_getElementName());
result.append(">\n");
}
return result.toString();
}
/**
* Answer string representation
*/
public String toString() {
return toString(1);
}
/**
* Answer xml string representation, prune to specified depth.
* @param depth maximal tree depth (0 indicates no children,
* -1 indicates unlimited depth)
*/
public String toString(int depth) {
return toString("", depth);
}
/**
* Answer xml string representation.
*/
public String toXML() {
return toString(-1);
}
}