/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.commons.configuration.tree;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import org.apache.commons.lang.StringUtils;
/**
* <p>
* A default implementation of the <code>ExpressionEngine</code> interface
* providing the "native"e; expression language for hierarchical
* configurations.
* </p>
* <p>
* This class implements a rather simple expression language for navigating
* through a hierarchy of configuration nodes. It supports the following
* operations:
* </p>
* <p>
* <ul>
* <li>Navigating from a node to one of its children using the child node
* delimiter, which is by the default a dot (".").</li>
* <li>Navigating from a node to one of its attributes using the attribute node
* delimiter, which by default follows the XPATH like syntax
* <code>[@<attributeName>]</code>.</li>
* <li>If there are multiple child or attribute nodes with the same name, a
* specific node can be selected using a numerical index. By default indices are
* written in paranthesis.</li>
* </ul>
* </p>
* <p>
* As an example consider the following XML document:
* </p>
*
* <pre>
* <database>
* <tables>
* <table type="system">
* <name>users</name>
* <fields>
* <field>
* <name>lid</name>
* <type>long</name>
* </field>
* <field>
* <name>usrName</name>
* <type>java.lang.String</type>
* </field>
* ...
* </fields>
* </table>
* <table>
* <name>documents</name>
* <fields>
* <field>
* <name>docid</name>
* <type>long</type>
* </field>
* ...
* </fields>
* </table>
* ...
* </tables>
* </database>
* </pre>
*
* </p>
* <p>
* If this document is parsed and stored in a hierarchical configuration object,
* for instance the key <code>tables.table(0).name</code> can be used to find
* out the name of the first table. In opposite <code>tables.table.name</code>
* would return a collection with the names of all available tables. Similarily
* the key <code>tables.table(1).fields.field.name</code> returns a collection
* with the names of all fields of the second table. If another index is added
* after the <code>field</code> element, a single field can be accessed:
* <code>tables.table(1).fields.field(0).name</code>. The key
* <code>tables.table(0)[@type]</code> would select the type attribute of the
* first table.
* </p>
* <p>
* This example works with the default values for delimiters and index markers.
* It is also possible to set custom values for these properties so that you can
* adapt a <code>DefaultExpressionEngine</code> to your personal needs.
* </p>
*
* @since 1.3
* @author Oliver Heger
* @version $Id: DefaultExpressionEngine.java 439648 2006-09-02 20:42:10Z oheger $
*/
public class DefaultExpressionEngine implements ExpressionEngine
{
/** Constant for the default property delimiter. */
public static final String DEFAULT_PROPERTY_DELIMITER = ".";
/** Constant for the default escaped property delimiter. */
public static final String DEFAULT_ESCAPED_DELIMITER = DEFAULT_PROPERTY_DELIMITER
+ DEFAULT_PROPERTY_DELIMITER;
/** Constant for the default attribute start marker. */
public static final String DEFAULT_ATTRIBUTE_START = "[@";
/** Constant for the default attribute end marker. */
public static final String DEFAULT_ATTRIBUTE_END = "]";
/** Constant for the default index start marker. */
public static final String DEFAULT_INDEX_START = "(";
/** Constant for the default index end marker. */
public static final String DEFAULT_INDEX_END = ")";
/** Stores the property delimiter. */
private String propertyDelimiter = DEFAULT_PROPERTY_DELIMITER;
/** Stores the escaped property delimiter. */
private String escapedDelimiter = DEFAULT_ESCAPED_DELIMITER;
/** Stores the attribute start marker. */
private String attributeStart = DEFAULT_ATTRIBUTE_START;
/** Stores the attribute end marker. */
private String attributeEnd = DEFAULT_ATTRIBUTE_END;
/** Stores the index start marker. */
private String indexStart = DEFAULT_INDEX_START;
/** stores the index end marker. */
private String indexEnd = DEFAULT_INDEX_END;
/**
* Sets the attribute end marker.
*
* @return the attribute end marker
*/
public String getAttributeEnd()
{
return attributeEnd;
}
/**
* Sets the attribute end marker.
*
* @param attributeEnd the attribute end marker; can be <b>null</b> if no
* end marker is needed
*/
public void setAttributeEnd(String attributeEnd)
{
this.attributeEnd = attributeEnd;
}
/**
* Returns the attribute start marker.
*
* @return the attribute start marker
*/
public String getAttributeStart()
{
return attributeStart;
}
/**
* Sets the attribute start marker. Attribute start and end marker are used
* together to detect attributes in a property key.
*
* @param attributeStart the attribute start marker
*/
public void setAttributeStart(String attributeStart)
{
this.attributeStart = attributeStart;
}
/**
* Returns the escaped property delimiter string.
*
* @return the escaped property delimiter
*/
public String getEscapedDelimiter()
{
return escapedDelimiter;
}
/**
* Sets the escaped property delimiter string. With this string a delimiter
* that belongs to the key of a property can be escaped. If for instance
* "." is used as property delimiter, you can set the escaped
* delimiter to "\." and can then escape the delimiter with a back
* slash.
*
* @param escapedDelimiter the escaped delimiter string
*/
public void setEscapedDelimiter(String escapedDelimiter)
{
this.escapedDelimiter = escapedDelimiter;
}
/**
* Returns the index end marker.
*
* @return the index end marker
*/
public String getIndexEnd()
{
return indexEnd;
}
/**
* Sets the index end marker.
*
* @param indexEnd the index end marker
*/
public void setIndexEnd(String indexEnd)
{
this.indexEnd = indexEnd;
}
/**
* Returns the index start marker.
*
* @return the index start marker
*/
public String getIndexStart()
{
return indexStart;
}
/**
* Sets the index start marker. Index start and end marker are used together
* to detect indices in a property key.
*
* @param indexStart the index start marker
*/
public void setIndexStart(String indexStart)
{
this.indexStart = indexStart;
}
/**
* Returns the property delimiter.
*
* @return the property delimiter
*/
public String getPropertyDelimiter()
{
return propertyDelimiter;
}
/**
* Sets the property delmiter. This string is used to split the parts of a
* property key.
*
* @param propertyDelimiter the property delimiter
*/
public void setPropertyDelimiter(String propertyDelimiter)
{
this.propertyDelimiter = propertyDelimiter;
}
/**
* Evaluates the given key and returns all matching nodes. This method
* supports the syntax as described in the class comment.
*
* @param root the root node
* @param key the key
* @return a list with the matching nodes
*/
public List query(ConfigurationNode root, String key)
{
List nodes = new LinkedList();
findNodesForKey(new DefaultConfigurationKey(this, key).iterator(),
root, nodes);
return nodes;
}
/**
* Determines the key of the passed in node. This implementation takes the
* given parent key, adds a property delimiter, and then adds the node's
* name. (For attribute nodes the attribute delimiters are used instead.)
* The name of the root node is a blanc string. Note that no indices will be
* returned.
*
* @param node the node whose key is to be determined
* @param parentKey the key of this node's parent
* @return the key for the given node
*/
public String nodeKey(ConfigurationNode node, String parentKey)
{
if (parentKey == null)
{
// this is the root node
return StringUtils.EMPTY;
}
else
{
DefaultConfigurationKey key = new DefaultConfigurationKey(this,
parentKey);
if (node.isAttribute())
{
key.appendAttribute(node.getName());
}
else
{
key.append(node.getName(), true);
}
return key.toString();
}
}
/**
* <p>
* Prepares Adding the property with the specified key.
* </p>
* <p>
* To be able to deal with the structure supported by hierarchical
* configuration implementations the passed in key is of importance,
* especially the indices it might contain. The following example should
* clearify this: Suppose the actual node structure looks like the
* following:
* </p>
* <p>
* <pre>
* tables
* +-- table
* +-- name = user
* +-- fields
* +-- field
* +-- name = uid
* +-- field
* +-- name = firstName
* ...
* +-- table
* +-- name = documents
* +-- fields
* ...
* </pre>
* </p>
* <p>
* In this example a database structure is defined, e.g. all fields of the
* first table could be accessed using the key
* <code>tables.table(0).fields.field.name</code>. If now properties are
* to be added, it must be exactly specified at which position in the
* hierarchy the new property is to be inserted. So to add a new field name
* to a table it is not enough to say just
* </p>
* <p>
* <pre>
* config.addProperty("tables.table.fields.field.name", "newField");
* </pre>
* </p>
* <p>
* The statement given above contains some ambiguity. For instance it is not
* clear, to which table the new field should be added. If this method finds
* such an ambiguity, it is resolved by following the last valid path. Here
* this would be the last table. The same is true for the <code>field</code>;
* because there are multiple fields and no explicit index is provided, a
* new <code>name</code> property would be added to the last field - which
* is propably not what was desired.
* </p>
* <p>
* To make things clear explicit indices should be provided whenever
* possible. In the example above the exact table could be specified by
* providing an index for the <code>table</code> element as in
* <code>tables.table(1).fields</code>. By specifying an index it can
* also be expressed that at a given position in the configuration tree a
* new branch should be added. In the example above we did not want to add
* an additional <code>name</code> element to the last field of the table,
* but we want a complete new <code>field</code> element. This can be
* achieved by specifying an invalid index (like -1) after the element where
* a new branch should be created. Given this our example would run:
* </p>
* <p>
* <pre>
* config.addProperty("tables.table(1).fields.field(-1).name", "newField");
* </pre>
* </p>
* <p>
* With this notation it is possible to add new branches everywhere. We
* could for instance create a new <code>table</code> element by
* specifying
* </p>
* <p>
* <pre>
* config.addProperty("tables.table(-1).fields.field.name", "newField2");
* </pre>
* </p>
* <p>
* (Note that because after the <code>table</code> element a new branch is
* created indices in following elements are not relevant; the branch is new
* so there cannot be any ambiguities.)
* </p>
*
* @param root the root node of the nodes hierarchy
* @param key the key of the new property
* @return a data object with information needed for the add operation
*/
public NodeAddData prepareAdd(ConfigurationNode root, String key)
{
DefaultConfigurationKey.KeyIterator it = new DefaultConfigurationKey(
this, key).iterator();
if (!it.hasNext())
{
throw new IllegalArgumentException(
"Key for add operation must be defined!");
}
NodeAddData result = new NodeAddData();
result.setParent(findLastPathNode(it, root));
while (it.hasNext())
{
if (!it.isPropertyKey())
{
throw new IllegalArgumentException(
"Invalid key for add operation: " + key
+ " (Attribute key in the middle.)");
}
result.addPathNode(it.currentKey());
it.next();
}
result.setNewNodeName(it.currentKey());
result.setAttribute(!it.isPropertyKey());
return result;
}
/**
* Recursive helper method for evaluating a key. This method processes all
* facets of a configuration key, traverses the tree of properties and
* fetches the the nodes of all matching properties.
*
* @param keyPart the configuration key iterator
* @param node the actual node
* @param nodes here the found nodes are stored
*/
protected void findNodesForKey(DefaultConfigurationKey.KeyIterator keyPart,
ConfigurationNode node, Collection nodes)
{
if (!keyPart.hasNext())
{
nodes.add(node);
}
else
{
String key = keyPart.nextKey(false);
if (keyPart.isPropertyKey())
{
processSubNodes(keyPart, node.getChildren(key), nodes);
}
if (keyPart.isAttribute())
{
processSubNodes(keyPart, node.getAttributes(key), nodes);
}
}
}
/**
* Finds the last existing node for an add operation. This method traverses
* the configuration node tree along the specified key. The last existing
* node on this path is returned.
*
* @param keyIt the key iterator
* @param node the actual node
* @return the last existing node on the given path
*/
protected ConfigurationNode findLastPathNode(
DefaultConfigurationKey.KeyIterator keyIt, ConfigurationNode node)
{
String keyPart = keyIt.nextKey(false);
if (keyIt.hasNext())
{
if (!keyIt.isPropertyKey())
{
// Attribute keys can only appear as last elements of the path
throw new IllegalArgumentException(
"Invalid path for add operation: "
+ "Attribute key in the middle!");
}
int idx = keyIt.hasIndex() ? keyIt.getIndex() : node
.getChildrenCount(keyPart) - 1;
if (idx < 0 || idx >= node.getChildrenCount(keyPart))
{
return node;
}
else
{
return findLastPathNode(keyIt, (ConfigurationNode) node
.getChildren(keyPart).get(idx));
}
}
else
{
return node;
}
}
/**
* Called by <code>findNodesForKey()</code> to process the sub nodes of
* the current node depending on the type of the current key part (children,
* attributes, or both).
*
* @param keyPart the key part
* @param subNodes a list with the sub nodes to process
* @param nodes the target collection
*/
private void processSubNodes(DefaultConfigurationKey.KeyIterator keyPart,
List subNodes, Collection nodes)
{
if (keyPart.hasIndex())
{
if (keyPart.getIndex() >= 0 && keyPart.getIndex() < subNodes.size())
{
findNodesForKey((DefaultConfigurationKey.KeyIterator) keyPart
.clone(), (ConfigurationNode) subNodes.get(keyPart
.getIndex()), nodes);
}
}
else
{
for (Iterator it = subNodes.iterator(); it.hasNext();)
{
findNodesForKey((DefaultConfigurationKey.KeyIterator) keyPart
.clone(), (ConfigurationNode) it.next(), nodes);
}
}
}
}
|