##### Coverage Report - org.apache.commons.configuration.tree.xpath.XPathExpressionEngine

Classes in this File | Line Coverage | Branch Coverage | Complexity

XPathExpressionEngine

100% | 70/70

100% | 20/20

5.333333333333333;5,333

1

     /*

2

      * Licensed to the Apache Software Foundation (ASF) under one or more

3

      * contributor license agreements.  See the NOTICE file distributed with

4

      * this work for additional information regarding copyright ownership.

5

      * The ASF licenses this file to You under the Apache License, Version 2.0

6

      * (the "License"); you may not use this file except in compliance with

7

      * the License.  You may obtain a copy of the License at

8

      *

9

      *     http://www.apache.org/licenses/LICENSE-2.0

10

      *

11

      * Unless required by applicable law or agreed to in writing, software

12

      * distributed under the License is distributed on an "AS IS" BASIS,

13

      * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

14

      * See the License for the specific language governing permissions and

15

      * limitations under the License.

16

      */

17

     package org.apache.commons.configuration.tree.xpath;

18

19

     import java.util.ArrayList;

20

     import java.util.Collections;

21

     import java.util.List;

22

     import java.util.StringTokenizer;

23

24

     import org.apache.commons.configuration.tree.ConfigurationNode;

25

     import org.apache.commons.configuration.tree.ExpressionEngine;

26

     import org.apache.commons.configuration.tree.NodeAddData;

27

     import org.apache.commons.jxpath.JXPathContext;

28

     import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;

29

     import org.apache.commons.lang.StringUtils;

30

31

     /**

32

      * <p>

33

      * A specialized implementation of the <code>ExpressionEngine</code> interface

34

      * that is able to evaluate XPATH expressions.

35

      * </p>

36

      * <p>

37

      * This class makes use of <a href="http://jakarta.apache.org/commons/jxpath/">

38

      * Commons JXPath</a> for handling XPath expressions and mapping them to the

39

      * nodes of a hierarchical configuration. This makes the rich and powerfull

40

      * XPATH syntax available for accessing properties from a configuration object.

41

      * </p>

42

      * <p>

43

      * For selecting properties arbitrary XPATH expressions can be used, which

44

      * select single or multiple configuration nodes. The associated

45

      * <code>Configuration</code> instance will directly pass the specified

46

      * property keys into this engine. If a key is not syntactically correct, an

47

      * exception will be thrown.

48

      * </p>

49

      * <p>

50

      * For adding new properties, this expression engine uses a specific syntax: the

51

      * &quot;key&quot; of a new property must consist of two parts that are

52

      * separated by whitespace:

53

      * <ol>

54

      * <li>An XPATH expression selecting a single node, to which the new element(s)

55

      * are to be added. This can be an arbitrary complex expression, but it must

56

      * select exactly one node, otherwise an exception will be thrown.</li>

57

      * <li>The name of the new element(s) to be added below this parent node. Here

58

      * either a single node name or a complete path of nodes (separated by the

59

      * &quot;/&quot; character) can be specified.</li>

60

      * </ol>

61

      * Some examples for valid keys that can be passed into the configuration's

62

      * <code>addProperty()</code> method follow:

63

      * </p>

64

      * <p>

65

      *

66

      * <pre>

67

      * &quot;/tables/table[1] type&quot;

68

      * </pre>

69

      *

70

      * </p>

71

      * <p>

72

      * This will add a new <code>type</code> node as a child of the first

73

      * <code>table</code> element.

74

      * </p>

75

      * <p>

76

      *

77

      * <pre>

78

      * &quot;/tables/table[1] @type&quot;

79

      * </pre>

80

      *

81

      * </p>

82

      * <p>

83

      * Similar to the example above, but this time a new attribute named

84

      * <code>type</code> will be added to the first <code>table</code> element.

85

      * </p>

86

      * <p>

87

      *

88

      * <pre>

89

      * &quot;/tables table/fields/field/name&quot;

90

      * </pre>

91

      *

92

      * </p>

93

      * <p>

94

      * This example shows how a complex path can be added. Parent node is the

95

      * <code>tables</code> element. Here a new branch consisting of the nodes

96

      * <code>table</code>, <code>fields</code>, <code>field</code>, and

97

      * <code>name</code> will be added.

98

      * </p>

99

      *

100

      * @since 1.3

101

      * @author Oliver Heger

102

      * @version $Id: XPathExpressionEngine.java 439648 2006-09-02 20:42:10Z oheger $

103

      */

104

31

     public class XPathExpressionEngine implements ExpressionEngine

105

     {

106

         /** Constant for the path delimiter. */

107

         static final String PATH_DELIMITER = "/";

108

109

         /** Constant for the attribute delimiter. */

110

         static final String ATTR_DELIMITER = "@";

111

112

         /** Constant for the delimiters for splitting node paths. */

113

         private static final String NODE_PATH_DELIMITERS = PATH_DELIMITER

114

                 + ATTR_DELIMITER;

115

116

         /**

117

          * Executes a query. The passed in property key is directly passed to a

118

          * JXPath context.

119

          *

120

          * @param root the configuration root node

121

          * @param key the query to be executed

122

          * @return a list with the nodes that are selected by the query

123

          */

124

         public List query(ConfigurationNode root, String key)

125

         {

126

249

             if (StringUtils.isEmpty(key))

127

             {

128

81

                 List result = new ArrayList(1);

129

81

                 result.add(root);

130

81

                 return result;

131

             }

132

             else

133

             {

134

168

                 JXPathContext context = createContext(root, key);

135

168

                 List result = context.selectNodes(key);

136

168

                 return (result != null) ? result : Collections.EMPTY_LIST;

137

             }

138

         }

139

140

         /**

141

          * Returns a (canonic) key for the given node based on the parent's key.

142

          * This implementation will create an XPATH expression that selects the

143

          * given node (under the assumption that the passed in parent key is valid).

144

          * As the <code>nodeKey()</code> implementation of

145

          * <code>{@link org.apache.commons.configuration.tree.DefaultExpressionEngine DefaultExpressionEngine}</code>

146

          * this method will not return indices for nodes. So all child nodes of a

147

          * given parent whith the same name will have the same key.

148

          *

149

          * @param node the node for which a key is to be constructed

150

          * @param parentKey the key of the parent node

151

          * @return the key for the given node

152

          */

153

         public String nodeKey(ConfigurationNode node, String parentKey)

154

         {

155

21

             if (parentKey == null)

156

             {

157

                 // name of the root node

158

2

                 return StringUtils.EMPTY;

159

             }

160

19

             else if (node.getName() == null)

161

             {

162

                 // paranoia check for undefined node names

163

1

                 return parentKey;

164

             }

165

166

             else

167

             {

168

18

                 StringBuffer buf = new StringBuffer(parentKey.length()

169

                         + node.getName().length() + PATH_DELIMITER.length());

170

18

                 if (parentKey.length() > 0)

171

                 {

172

14

                     buf.append(parentKey);

173

14

                     if (!node.isAttribute())

174

                     {

175

13

                         buf.append(PATH_DELIMITER);

176

                     }

177

                 }

178

18

                 if (node.isAttribute())

179

                 {

180

2

                     buf.append(ATTR_DELIMITER);

181

                 }

182

18

                 buf.append(node.getName());

183

18

                 return buf.toString();

184

             }

185

         }

186

187

         /**

188

          * Prepares an add operation for a configuration property. The expected

189

          * format of the passed in key is explained in the class comment.

190

          *

191

          * @param root the configuration's root node

192

          * @param key the key describing the target of the add operation and the

193

          * path of the new node

194

          * @return a data object to be evaluated by the calling configuration object

195

          */

196

         public NodeAddData prepareAdd(ConfigurationNode root, String key)

197

         {

198

28

             if (key == null)

199

             {

200

1

                 throw new IllegalArgumentException(

201

                         "prepareAdd: key must not be null!");

202

             }

203

204

27

             int index = key.length() - 1;

205

438

             while (index >= 0 && !Character.isWhitespace(key.charAt(index)))

206

             {

207

384

                 index--;

208

             }

209

27

             if (index < 0)

210

             {

211

2

                 throw new IllegalArgumentException(

212

                         "prepareAdd: Passed in key must contain a whitespace!");

213

             }

214

215

25

             List nodes = query(root, key.substring(0, index).trim());

216

25

             if (nodes.size() != 1)

217

             {

218

1

                 throw new IllegalArgumentException(

219

                         "prepareAdd: key must select exactly one target node!");

220

             }

221

222

24

             NodeAddData data = new NodeAddData();

223

24

             data.setParent((ConfigurationNode) nodes.get(0));

224

24

             initNodeAddData(data, key.substring(index).trim());

225

18

             return data;

226

         }

227

228

         /**

229

          * Creates the <code>JXPathContext</code> used for executing a query. This

230

          * method will create a new context and ensure that it is correctly

231

          * initialized.

232

          *

233

          * @param root the configuration root node

234

          * @param key the key to be queried

235

          * @return the new context

236

          */

237

         protected JXPathContext createContext(ConfigurationNode root, String key)

238

         {

239

169

             JXPathContext context = JXPathContext.newContext(root);

240

169

             context.setLenient(true);

241

169

             return context;

242

         }

243

244

         /**

245

          * Initializes most properties of a <code>NodeAddData</code> object. This

246

          * method is called by <code>prepareAdd()</code> after the parent node has

247

          * been found. Its task is to interprete the passed in path of the new node.

248

          *

249

          * @param data the data object to initialize

250

          * @param path the path of the new node

251

          */

252

         protected void initNodeAddData(NodeAddData data, String path)

253

         {

254

24

             String lastComponent = null;

255

24

             boolean attr = false;

256

24

             boolean first = true;

257

258

24

             StringTokenizer tok = new StringTokenizer(path, NODE_PATH_DELIMITERS,

259

                     true);

260

122

             while (tok.hasMoreTokens())

261

             {

262

79

                 String token = tok.nextToken();

263

79

                 if (PATH_DELIMITER.equals(token))

264

                 {

265

18

                     if (attr)

266

                     {

267

1

                         invalidPath(path, " contains an attribute"

268

                                 + " delimiter at an unallowed position.");

269

                     }

270

17

                     if (lastComponent == null)

271

                     {

272

2

                         invalidPath(path,

273

                                 " contains a '/' at an unallowed position.");

274

                     }

275

15

                     data.addPathNode(lastComponent);

276

15

                     lastComponent = null;

277

                 }

278

279

61

                 else if (ATTR_DELIMITER.equals(token))

280

                 {

281

18

                     if (attr)

282

                     {

283

1

                         invalidPath(path,

284

                                 " contains multiple attribute delimiters.");

285

                     }

286

17

                     if (lastComponent == null && !first)

287

                     {

288

1

                         invalidPath(path,

289

                                 " contains an attribute delimiter at an unallowed position.");

290

                     }

291

16

                     if (lastComponent != null)

292

                     {

293

8

                         data.addPathNode(lastComponent);

294

                     }

295

16

                     attr = true;

296

16

                     lastComponent = null;

297

                 }

298

299

                 else

300

                 {

301

43

                     lastComponent = token;

302

                 }

303

74

                 first = false;

304

             }

305

306

19

             if (lastComponent == null)

307

             {

308

1

                 invalidPath(path, "contains no components.");

309

             }

310

18

             data.setNewNodeName(lastComponent);

311

18

             data.setAttribute(attr);

312

18

         }

313

314

         /**

315

          * Helper method for throwing an exception about an invalid path.

316

          *

317

          * @param path the invalid path

318

          * @param msg the exception message

319

          */

320

         private void invalidPath(String path, String msg)

321

         {

322

6

             throw new IllegalArgumentException("Invalid node path: \"" + path

323

                     + "\" " + msg);

324

         }

325

326

         // static initializer: registers the configuration node pointer factory

327

         static

328

4

         {

329

4

             JXPathContextReferenceImpl

330

                     .addNodePointerFactory(new ConfigurationNodePointerFactory());

331

         }

332

     }