001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.commons.configuration.tree.xpath;
018:
019: import java.util.ArrayList;
020: import java.util.Collections;
021: import java.util.List;
022: import java.util.StringTokenizer;
023:
024: import org.apache.commons.configuration.tree.ConfigurationNode;
025: import org.apache.commons.configuration.tree.ExpressionEngine;
026: import org.apache.commons.configuration.tree.NodeAddData;
027: import org.apache.commons.jxpath.JXPathContext;
028: import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;
029: import org.apache.commons.lang.StringUtils;
030:
031: /**
032: * <p>
033: * A specialized implementation of the <code>ExpressionEngine</code> interface
034: * that is able to evaluate XPATH expressions.
035: * </p>
036: * <p>
037: * This class makes use of <a href="http://jakarta.apache.org/commons/jxpath/">
038: * Commons JXPath</a> for handling XPath expressions and mapping them to the
039: * nodes of a hierarchical configuration. This makes the rich and powerfull
040: * XPATH syntax available for accessing properties from a configuration object.
041: * </p>
042: * <p>
043: * For selecting properties arbitrary XPATH expressions can be used, which
044: * select single or multiple configuration nodes. The associated
045: * <code>Configuration</code> instance will directly pass the specified
046: * property keys into this engine. If a key is not syntactically correct, an
047: * exception will be thrown.
048: * </p>
049: * <p>
050: * For adding new properties, this expression engine uses a specific syntax: the
051: * "key" of a new property must consist of two parts that are
052: * separated by whitespace:
053: * <ol>
054: * <li>An XPATH expression selecting a single node, to which the new element(s)
055: * are to be added. This can be an arbitrary complex expression, but it must
056: * select exactly one node, otherwise an exception will be thrown.</li>
057: * <li>The name of the new element(s) to be added below this parent node. Here
058: * either a single node name or a complete path of nodes (separated by the
059: * "/" character) can be specified.</li>
060: * </ol>
061: * Some examples for valid keys that can be passed into the configuration's
062: * <code>addProperty()</code> method follow:
063: * </p>
064: * <p>
065: *
066: * <pre>
067: * "/tables/table[1] type"
068: * </pre>
069: *
070: * </p>
071: * <p>
072: * This will add a new <code>type</code> node as a child of the first
073: * <code>table</code> element.
074: * </p>
075: * <p>
076: *
077: * <pre>
078: * "/tables/table[1] @type"
079: * </pre>
080: *
081: * </p>
082: * <p>
083: * Similar to the example above, but this time a new attribute named
084: * <code>type</code> will be added to the first <code>table</code> element.
085: * </p>
086: * <p>
087: *
088: * <pre>
089: * "/tables table/fields/field/name"
090: * </pre>
091: *
092: * </p>
093: * <p>
094: * This example shows how a complex path can be added. Parent node is the
095: * <code>tables</code> element. Here a new branch consisting of the nodes
096: * <code>table</code>, <code>fields</code>, <code>field</code>, and
097: * <code>name</code> will be added.
098: * </p>
099: *
100: * @since 1.3
101: * @author Oliver Heger
102: * @version $Id: XPathExpressionEngine.java 466413 2006-10-21 15:23:45Z oheger $
103: */
104: public class XPathExpressionEngine implements ExpressionEngine {
105: /** Constant for the path delimiter. */
106: static final String PATH_DELIMITER = "/";
107:
108: /** Constant for the attribute delimiter. */
109: static final String ATTR_DELIMITER = "@";
110:
111: /** Constant for the delimiters for splitting node paths. */
112: private static final String NODE_PATH_DELIMITERS = PATH_DELIMITER
113: + ATTR_DELIMITER;
114:
115: /**
116: * Executes a query. The passed in property key is directly passed to a
117: * JXPath context.
118: *
119: * @param root the configuration root node
120: * @param key the query to be executed
121: * @return a list with the nodes that are selected by the query
122: */
123: public List query(ConfigurationNode root, String key) {
124: if (StringUtils.isEmpty(key)) {
125: List result = new ArrayList(1);
126: result.add(root);
127: return result;
128: } else {
129: JXPathContext context = createContext(root, key);
130: List result = context.selectNodes(key);
131: return (result != null) ? result : Collections.EMPTY_LIST;
132: }
133: }
134:
135: /**
136: * Returns a (canonic) key for the given node based on the parent's key.
137: * This implementation will create an XPATH expression that selects the
138: * given node (under the assumption that the passed in parent key is valid).
139: * As the <code>nodeKey()</code> implementation of
140: * <code>{@link org.apache.commons.configuration.tree.DefaultExpressionEngine DefaultExpressionEngine}</code>
141: * this method will not return indices for nodes. So all child nodes of a
142: * given parent whith the same name will have the same key.
143: *
144: * @param node the node for which a key is to be constructed
145: * @param parentKey the key of the parent node
146: * @return the key for the given node
147: */
148: public String nodeKey(ConfigurationNode node, String parentKey) {
149: if (parentKey == null) {
150: // name of the root node
151: return StringUtils.EMPTY;
152: } else if (node.getName() == null) {
153: // paranoia check for undefined node names
154: return parentKey;
155: }
156:
157: else {
158: StringBuffer buf = new StringBuffer(parentKey.length()
159: + node.getName().length() + PATH_DELIMITER.length());
160: if (parentKey.length() > 0) {
161: buf.append(parentKey);
162: buf.append(PATH_DELIMITER);
163: }
164: if (node.isAttribute()) {
165: buf.append(ATTR_DELIMITER);
166: }
167: buf.append(node.getName());
168: return buf.toString();
169: }
170: }
171:
172: /**
173: * Prepares an add operation for a configuration property. The expected
174: * format of the passed in key is explained in the class comment.
175: *
176: * @param root the configuration's root node
177: * @param key the key describing the target of the add operation and the
178: * path of the new node
179: * @return a data object to be evaluated by the calling configuration object
180: */
181: public NodeAddData prepareAdd(ConfigurationNode root, String key) {
182: if (key == null) {
183: throw new IllegalArgumentException(
184: "prepareAdd: key must not be null!");
185: }
186:
187: int index = key.length() - 1;
188: while (index >= 0 && !Character.isWhitespace(key.charAt(index))) {
189: index--;
190: }
191: if (index < 0) {
192: throw new IllegalArgumentException(
193: "prepareAdd: Passed in key must contain a whitespace!");
194: }
195:
196: List nodes = query(root, key.substring(0, index).trim());
197: if (nodes.size() != 1) {
198: throw new IllegalArgumentException(
199: "prepareAdd: key must select exactly one target node!");
200: }
201:
202: NodeAddData data = new NodeAddData();
203: data.setParent((ConfigurationNode) nodes.get(0));
204: initNodeAddData(data, key.substring(index).trim());
205: return data;
206: }
207:
208: /**
209: * Creates the <code>JXPathContext</code> used for executing a query. This
210: * method will create a new context and ensure that it is correctly
211: * initialized.
212: *
213: * @param root the configuration root node
214: * @param key the key to be queried
215: * @return the new context
216: */
217: protected JXPathContext createContext(ConfigurationNode root,
218: String key) {
219: JXPathContext context = JXPathContext.newContext(root);
220: context.setLenient(true);
221: return context;
222: }
223:
224: /**
225: * Initializes most properties of a <code>NodeAddData</code> object. This
226: * method is called by <code>prepareAdd()</code> after the parent node has
227: * been found. Its task is to interprete the passed in path of the new node.
228: *
229: * @param data the data object to initialize
230: * @param path the path of the new node
231: */
232: protected void initNodeAddData(NodeAddData data, String path) {
233: String lastComponent = null;
234: boolean attr = false;
235: boolean first = true;
236:
237: StringTokenizer tok = new StringTokenizer(path,
238: NODE_PATH_DELIMITERS, true);
239: while (tok.hasMoreTokens()) {
240: String token = tok.nextToken();
241: if (PATH_DELIMITER.equals(token)) {
242: if (attr) {
243: invalidPath(path, " contains an attribute"
244: + " delimiter at an unallowed position.");
245: }
246: if (lastComponent == null) {
247: invalidPath(path,
248: " contains a '/' at an unallowed position.");
249: }
250: data.addPathNode(lastComponent);
251: lastComponent = null;
252: }
253:
254: else if (ATTR_DELIMITER.equals(token)) {
255: if (attr) {
256: invalidPath(path,
257: " contains multiple attribute delimiters.");
258: }
259: if (lastComponent == null && !first) {
260: invalidPath(path,
261: " contains an attribute delimiter at an unallowed position.");
262: }
263: if (lastComponent != null) {
264: data.addPathNode(lastComponent);
265: }
266: attr = true;
267: lastComponent = null;
268: }
269:
270: else {
271: lastComponent = token;
272: }
273: first = false;
274: }
275:
276: if (lastComponent == null) {
277: invalidPath(path, "contains no components.");
278: }
279: data.setNewNodeName(lastComponent);
280: data.setAttribute(attr);
281: }
282:
283: /**
284: * Helper method for throwing an exception about an invalid path.
285: *
286: * @param path the invalid path
287: * @param msg the exception message
288: */
289: private void invalidPath(String path, String msg) {
290: throw new IllegalArgumentException("Invalid node path: \""
291: + path + "\" " + msg);
292: }
293:
294: // static initializer: registers the configuration node pointer factory
295: static {
296: JXPathContextReferenceImpl
297: .addNodePointerFactory(new ConfigurationNodePointerFactory());
298: }
299: }
|