Coverage report

| %line | %branch
--- | --- | ---
org.apache.commons.configuration.XMLConfiguration |
87% |
100% |

1 |  |
     /*
2 |  |
      * Copyright 2004 The Apache Software Foundation.
3 |  |
      *
4 |  |
      * Licensed under the Apache License, Version 2.0 (the "License")
5 |  |
      * you may not use this file except in compliance with the License.
6 |  |
      * You may obtain a copy of the License at
7 |  |
      *
8 |  |
      *     http://www.apache.org/licenses/LICENSE-2.0
9 |  |
      *
10 |  |
      * Unless required by applicable law or agreed to in writing, software
11 |  |
      * distributed under the License is distributed on an "AS IS" BASIS,
12 |  |
      * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  |
      * See the License for the specific language governing permissions and
14 |  |
      * limitations under the License.
15 |  |
      */
16 |  |
17 |  |
     package org.apache.commons.configuration;
18 |  |
19 |  |
     import java.io.File;
20 |  |
     import java.io.Reader;
21 |  |
     import java.io.StringWriter;
22 |  |
     import java.io.Writer;
23 |  |
     import java.net.URL;
24 |  |
     import java.util.ArrayList;
25 |  |
     import java.util.Iterator;
26 |  |
     import java.util.List;
27 |  |
     import javax.xml.parsers.DocumentBuilder;
28 |  |
     import javax.xml.parsers.DocumentBuilderFactory;
29 |  |
     import javax.xml.parsers.ParserConfigurationException;
30 |  |
     import javax.xml.transform.Result;
31 |  |
     import javax.xml.transform.Source;
32 |  |
     import javax.xml.transform.Transformer;
33 |  |
     import javax.xml.transform.TransformerException;
34 |  |
     import javax.xml.transform.TransformerFactory;
35 |  |
     import javax.xml.transform.dom.DOMSource;
36 |  |
     import javax.xml.transform.stream.StreamResult;
37 |  |
38 |  |
     import org.apache.commons.lang.StringUtils;
39 |  |
     import org.w3c.dom.Attr;
40 |  |
     import org.w3c.dom.CDATASection;
41 |  |
     import org.w3c.dom.CharacterData;
42 |  |
     import org.w3c.dom.Document;
43 |  |
     import org.w3c.dom.Element;
44 |  |
     import org.w3c.dom.NamedNodeMap;
45 |  |
     import org.w3c.dom.Node;
46 |  |
     import org.w3c.dom.NodeList;
47 |  |
     import org.w3c.dom.Text;
48 |  |
     import org.xml.sax.InputSource;
49 |  |
50 |  |
     /**
51 |  |
      * Reads a XML configuration file.
52 |  |
      *
53 |  |
      * To retrieve the value of an attribute of an element, use
54 |  |
      * <code>X.Y.Z[@attribute]</code>. The '@' symbol was chosen for consistency
55 |  |
      * with XPath.
56 |  |
      *
57 |  |
      * Setting property values will <b>NOT </b> automatically persist changes to
58 |  |
      * disk, unless <code>autoSave=true</code>.
59 |  |
      *
60 |  |
      * @since commons-configuration 1.0
61 |  |
      *
62 |  |
      * @author J�rg Schaible
63 |  |
      * @author <a href="mailto:kelvint@apache.org">Kelvin Tan </a>
64 |  |
      * @author <a href="mailto:dlr@apache.org">Daniel Rall </a>
65 |  |
      * @author Emmanuel Bourg
66 |  |
      * @version $Revision: 1.17 $, $Date: 2004/10/04 19:35:45 $
67 |  |
      */
68 |  |
     public class XMLConfiguration extends AbstractFileConfiguration
69 |  |
     {
70 |  |
         // For conformance with xpath
71 |  |
         private static final String ATTRIBUTE_START = "[@";
72 |  |
73 |  |
         private static final String ATTRIBUTE_END = "]";
74 |  |
75 |  |
         /**
76 |  |
          * For consistency with properties files. Access nodes via an "A.B.C"
77 |  |
          * notation.
78 |  |
          */
79 |  |
         private static final String NODE_DELIMITER = ".";
80 |  |
81 |  |
         /**
82 |  |
          * The XML document from our data source.
83 |  |
          */
84 |  |
         private Document document;
85 |  |
86 |  |
         /**
87 |  |
          * If true, modifications are immediately persisted.
88 |  |
          */
89 | 85 |
         private boolean autoSave = false;
90 |  |
91 |  |
         /**
92 |  |
          * Creates an empty XML configuration.
93 |  |
          */
94 |  |
         public XMLConfiguration()
95 | 12 |
         {
96 |  |
             // build an empty document.
97 | 12 |
             DocumentBuilder builder = null;
98 |  |
             try
99 |  |
             {
100 | 12 |
                 builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
101 |  |
             }
102 | 0 |
             catch (ParserConfigurationException e)
103 |  |
             {
104 | 0 |
                 throw new ConfigurationRuntimeException(e.getMessage(), e);
105 | 12 |
             }
106 |  |
107 | 12 |
             document = builder.newDocument();
108 | 12 |
             document.appendChild(document.createElement("configuration"));
109 | 12 |
         }
110 |  |
111 |  |
         /**
112 |  |
          * Creates and loads the XML configuration from the specified resource.
113 |  |
          *
114 |  |
          * @param resource The name of the resource to load.
115 |  |
          *
116 |  |
          * @throws ConfigurationException Error while loading the XML file
117 |  |
          */
118 |  |
         public XMLConfiguration(String resource) throws ConfigurationException
119 | 0 |
         {
120 | 0 |
             this.fileName = resource;
121 | 0 |
             url = ConfigurationUtils.locate(resource);
122 | 0 |
             load();
123 | 0 |
         }
124 |  |
125 |  |
         /**
126 |  |
          * Creates and loads the XML configuration from the specified file.
127 |  |
          *
128 |  |
          * @param file The XML file to load.
129 |  |
          * @throws ConfigurationException Error while loading the XML file
130 |  |
          */
131 |  |
         public XMLConfiguration(File file) throws ConfigurationException
132 | 73 |
         {
133 | 73 |
             setFile(file);
134 | 73 |
             load();
135 | 73 |
         }
136 |  |
137 |  |
         /**
138 |  |
          * Creates and loads the XML configuration from the specified URL.
139 |  |
          *
140 |  |
          * @param url The location of the XML file to load.
141 |  |
          * @throws ConfigurationException Error while loading the XML file
142 |  |
          */
143 |  |
         public XMLConfiguration(URL url) throws ConfigurationException
144 | 0 |
         {
145 | 0 |
             setURL(url);
146 | 0 |
             load();
147 | 0 |
         }
148 |  |
149 |  |
         /**
150 |  |
          * {@inheritDoc}
151 |  |
          */
152 |  |
         public void load(Reader in) throws ConfigurationException
153 |  |
         {
154 |  |
             try
155 |  |
             {
156 | 91 |
                 DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
157 | 91 |
                 document = builder.parse(new InputSource(in));
158 |  |
             }
159 | 0 |
             catch (Exception e)
160 |  |
             {
161 | 0 |
                 throw new ConfigurationException(e.getMessage(), e);
162 | 91 |
             }
163 |  |
164 | 91 |
             initProperties(document.getDocumentElement(), new StringBuffer());
165 | 91 |
         }
166 |  |
167 |  |
         /**
168 |  |
          * Loads and initializes from the XML file.
169 |  |
          *
170 |  |
          * @param element The element to start processing from. Callers should supply the root element of the document.
171 |  |
          * @param hierarchy
172 |  |
          */
173 |  |
         private void initProperties(Element element, StringBuffer hierarchy)
174 |  |
         {
175 | 3288 |
             StringBuffer buffer = new StringBuffer();
176 | 3288 |
             NodeList list = element.getChildNodes();
177 | 13514 |
             for (int i = 0; i < list.getLength(); i++)
178 |  |
             {
179 | 10226 |
                 Node node = list.item(i);
180 | 10226 |
                 if (node instanceof Element)
181 |  |
                 {
182 | 3197 |
                     Element child = (Element) node;
183 |  |
184 | 3197 |
                     StringBuffer subhierarchy = new StringBuffer(hierarchy.toString());
185 | 3197 |
                     subhierarchy.append(child.getTagName());
186 | 3197 |
                     processAttributes(subhierarchy.toString(), child);
187 | 3197 |
                     initProperties(child, subhierarchy.append(NODE_DELIMITER));
188 |  |
                 }
189 | 7029 |
                 else if (node instanceof CDATASection || node instanceof Text)
190 |  |
                 {
191 | 6579 |
                     CharacterData data = (CharacterData) node;
192 | 6579 |
                     buffer.append(data.getData());
193 |  |
                 }
194 |  |
             }
195 |  |
196 | 3288 |
             String text = buffer.toString().trim();
197 | 3288 |
             if (text.length() > 0 && hierarchy.length() > 0)
198 |  |
             {
199 | 2098 |
                 super.addProperty(hierarchy.substring(0, hierarchy.length() - 1), text);
200 |  |
             }
201 | 3288 |
         }
202 |  |
203 |  |
         /**
204 |  |
          * Helper method for constructing properties for the attributes of the given
205 |  |
          * XML element.
206 |  |
          *
207 |  |
          * @param hierarchy the actual hierarchy
208 |  |
          * @param element   the actual XML element
209 |  |
          */
210 |  |
         private void processAttributes(String hierarchy, Element element)
211 |  |
         {
212 |  |
             // Add attributes as x.y{ATTRIBUTE_START}att{ATTRIBUTE_END}
213 | 3197 |
             NamedNodeMap attributes = element.getAttributes();
214 | 4013 |
             for (int i = 0; i < attributes.getLength(); ++i)
215 |  |
             {
216 | 816 |
                 Attr attr = (Attr) attributes.item(i);
217 | 816 |
                 String attrName = hierarchy + ATTRIBUTE_START + attr.getName() + ATTRIBUTE_END;
218 | 816 |
                 super.addProperty(attrName, attr.getValue());
219 |  |
             }
220 | 3197 |
         }
221 |  |
222 |  |
         /**
223 |  |
          * Calls super method, and also ensures the underlying {@linkDocument} is
224 |  |
          * modified so changes are persisted when saved.
225 |  |
          *
226 |  |
          * @param name
227 |  |
          * @param value
228 |  |
          */
229 |  |
         public void addProperty(String name, Object value)
230 |  |
         {
231 | 23 |
             super.addProperty(name, value);
232 | 23 |
             addXmlProperty(name, value);
233 | 23 |
             possiblySave();
234 | 23 |
         }
235 |  |
236 |  |
         Object getXmlProperty(String name)
237 |  |
         {
238 |  |
             // parse the key
239 | 27 |
             String[] nodes = parseElementNames(name);
240 | 27 |
             String attName = parseAttributeName(name);
241 |  |
242 |  |
             // get all the matching elements
243 | 27 |
             List children = findElementsForPropertyNodes(nodes);
244 |  |
245 | 27 |
             List properties = new ArrayList();
246 | 27 |
             if (attName == null)
247 |  |
             {
248 |  |
                 // return text contents of elements
249 | 18 |
                 Iterator cIter = children.iterator();
250 | 46 |
                 while (cIter.hasNext())
251 |  |
                 {
252 | 28 |
                     Element child = (Element) cIter.next();
253 |  |
                     // add non-empty strings
254 | 28 |
                     String text = getChildText(child);
255 | 28 |
                     if (StringUtils.isNotEmpty(text))
256 |  |
                     {
257 | 19 |
                         properties.add(text);
258 |  |
                     }
259 |  |
                 }
260 |  |
             }
261 |  |
             else
262 |  |
             {
263 |  |
                 // return text contents of attributes
264 | 9 |
                 Iterator cIter = children.iterator();
265 | 31 |
                 while (cIter.hasNext())
266 |  |
                 {
267 | 22 |
                     Element child = (Element) cIter.next();
268 | 22 |
                     if (child.hasAttribute(attName))
269 |  |
                     {
270 | 9 |
                         properties.add(child.getAttribute(attName));
271 |  |
                     }
272 |  |
                 }
273 |  |
             }
274 |  |
275 | 27 |
             switch (properties.size())
276 |  |
             {
277 |  |
                 case 0:
278 | 14 |
                     return null;
279 |  |
                 case 1:
280 | 8 |
                     return properties.get(0);
281 |  |
                 default:
282 | 5 |
                     return properties;
283 |  |
             }
284 |  |
         }
285 |  |
286 |  |
         /**
287 |  |
          * TODO Add comment.
288 |  |
          *
289 |  |
          * @param nodes
290 |  |
          * @return
291 |  |
          */
292 |  |
         private List findElementsForPropertyNodes(String[] nodes)
293 |  |
         {
294 | 48 |
             List children = new ArrayList();
295 | 48 |
             List elements = new ArrayList();
296 |  |
297 | 48 |
             children.add(document.getDocumentElement());
298 | 140 |
             for (int i = 0; i < nodes.length; i++)
299 |  |
             {
300 | 92 |
                 elements.clear();
301 | 92 |
                 elements.addAll(children);
302 | 92 |
                 children.clear();
303 |  |
304 | 92 |
                 String eName = nodes[i];
305 | 92 |
                 Iterator eIter = elements.iterator();
306 | 197 |
                 while (eIter.hasNext())
307 |  |
                 {
308 | 105 |
                     Element element = (Element) eIter.next();
309 | 105 |
                     NodeList list = element.getChildNodes();
310 | 1695 |
                     for (int j = 0; j < list.getLength(); j++)
311 |  |
                     {
312 | 1590 |
                         Node node = list.item(j);
313 | 1590 |
                         if (node instanceof Element)
314 |  |
                         {
315 | 594 |
                             Element child = (Element) node;
316 | 594 |
                             if (eName.equals(child.getTagName()))
317 |  |
                             {
318 | 138 |
                                 children.add(child);
319 |  |
                             }
320 |  |
                         }
321 |  |
                     }
322 |  |
                 }
323 |  |
             }
324 |  |
325 | 48 |
             return children;
326 |  |
         }
327 |  |
328 |  |
         private static String getChildText(Node node)
329 |  |
         {
330 |  |
             // is there anything to do?
331 | 28 |
             if (node == null)
332 |  |
             {
333 | 0 |
                 return null;
334 |  |
             }
335 |  |
336 |  |
             // concatenate children text
337 | 28 |
             StringBuffer str = new StringBuffer();
338 | 28 |
             Node child = node.getFirstChild();
339 | 61 |
             while (child != null)
340 |  |
             {
341 | 33 |
                 short type = child.getNodeType();
342 | 33 |
                 if (type == Node.TEXT_NODE)
343 |  |
                 {
344 | 25 |
                     str.append(child.getNodeValue());
345 |  |
                 }
346 | 8 |
                 else if (type == Node.CDATA_SECTION_NODE)
347 |  |
                 {
348 | 1 |
                     str.append(child.getNodeValue());
349 |  |
                 }
350 | 33 |
                 child = child.getNextSibling();
351 |  |
             }
352 |  |
353 |  |
             // return text value
354 | 28 |
             return StringUtils.trimToNull(str.toString());
355 |  |
356 |  |
         }
357 |  |
358 |  |
         private Element getChildElementWithName(String eName, Element element)
359 |  |
         {
360 | 38 |
             Element child = null;
361 |  |
362 | 38 |
             NodeList list = element.getChildNodes();
363 | 362 |
             for (int j = 0; j < list.getLength(); j++)
364 |  |
             {
365 | 350 |
                 Node node = list.item(j);
366 | 350 |
                 if (node instanceof Element)
367 |  |
                 {
368 | 167 |
                     child = (Element) node;
369 | 167 |
                     if (eName.equals(child.getTagName()))
370 |  |
                     {
371 | 26 |
                         break;
372 |  |
                     }
373 | 141 |
                     child = null;
374 |  |
                 }
375 |  |
             }
376 | 38 |
             return child;
377 |  |
         }
378 |  |
379 |  |
         /**
380 |  |
          * Adds the property value in our document tree.
381 |  |
          *
382 |  |
          * @param name  The name of the element to set a value for.
383 |  |
          * @param value The value to set.
384 |  |
          */
385 |  |
         private void addXmlProperty(String name, Object value)
386 |  |
         {
387 |  |
             // parse the key
388 | 23 |
             String[] nodes = parseElementNames(name);
389 | 23 |
             String attName = parseAttributeName(name);
390 |  |
391 | 23 |
             Element element = document.getDocumentElement();
392 | 23 |
             Element parent = element;
393 |  |
394 | 61 |
             for (int i = 0; i < nodes.length; i++)
395 |  |
             {
396 | 39 |
                 if (element == null)
397 |  |
                 {
398 | 1 |
                     break;
399 |  |
                 }
400 | 38 |
                 parent = element;
401 | 38 |
                 String eName = nodes[i];
402 | 38 |
                 Element child = getChildElementWithName(eName, element);
403 |  |
404 | 38 |
                 element = child;
405 |  |
             }
406 |  |
407 | 23 |
             Element child = document.createElement(nodes[nodes.length - 1]);
408 | 23 |
             parent.appendChild(child);
409 | 23 |
             if (attName == null)
410 |  |
             {
411 | 14 |
                 CharacterData data = document.createTextNode(String.valueOf(value));
412 | 14 |
                 child.appendChild(data);
413 |  |
             }
414 |  |
             else
415 |  |
             {
416 | 9 |
                 child.setAttribute(attName, String.valueOf(value));
417 |  |
             }
418 | 23 |
         }
419 |  |
420 |  |
         /**
421 |  |
          * Calls super method, and also ensures the underlying {@link Document}is
422 |  |
          * modified so changes are persisted when saved.
423 |  |
          *
424 |  |
          * @param name The name of the property to clear.
425 |  |
          */
426 |  |
         public void clearProperty(String name)
427 |  |
         {
428 | 21 |
             super.clearProperty(name);
429 | 21 |
             clearXmlProperty(name);
430 | 21 |
             possiblySave();
431 | 21 |
         }
432 |  |
433 |  |
         private void clearXmlProperty(String name)
434 |  |
         {
435 |  |
             // parse the key
436 | 21 |
             String[] nodes = parseElementNames(name);
437 | 21 |
             String attName = parseAttributeName(name);
438 |  |
439 |  |
             // get all the matching elements
440 | 21 |
             List children = findElementsForPropertyNodes(nodes);
441 |  |
442 | 21 |
             if (attName == null)
443 |  |
             {
444 |  |
                 // remove children with no subelements
445 | 16 |
                 Iterator cIter = children.iterator();
446 | 36 |
                 while (cIter.hasNext())
447 |  |
                 {
448 | 20 |
                     Element child = (Element) cIter.next();
449 |  |
450 |  |
                     // determine if child has subelments
451 | 20 |
                     boolean hasSubelements = false;
452 | 20 |
                     Node subchild = child.getFirstChild();
453 | 40 |
                     while (subchild != null)
454 |  |
                     {
455 | 20 |
                         if (subchild.getNodeType() == Node.ELEMENT_NODE)
456 |  |
                         {
457 | 0 |
                             hasSubelements = true;
458 | 0 |
                             break;
459 |  |
                         }
460 | 20 |
                         subchild = subchild.getNextSibling();
461 |  |
                     }
462 |  |
463 | 20 |
                     if (!hasSubelements)
464 |  |
                     {
465 |  |
                         // safe to remove
466 | 20 |
                         if (!child.hasAttributes())
467 |  |
                         {
468 |  |
                             // remove entire node
469 | 13 |
                             Node parent = child.getParentNode();
470 | 13 |
                             parent.removeChild(child);
471 |  |
                         }
472 |  |
                         else
473 |  |
                         {
474 |  |
                             // only remove node contents
475 | 7 |
                             subchild = child.getLastChild();
476 | 14 |
                             while (subchild != null)
477 |  |
                             {
478 | 7 |
                                 child.removeChild(subchild);
479 | 7 |
                                 subchild = child.getLastChild();
480 |  |
                             }
481 |  |
                         }
482 |  |
                     }
483 |  |
                 }
484 |  |
             }
485 |  |
             else
486 |  |
             {
487 |  |
                 // remove attributes from children
488 | 5 |
                 Iterator cIter = children.iterator();
489 | 16 |
                 while (cIter.hasNext())
490 |  |
                 {
491 | 11 |
                     Element child = (Element) cIter.next();
492 | 11 |
                     child.removeAttribute(attName);
493 |  |
                 }
494 |  |
             }
495 | 21 |
         }
496 |  |
497 |  |
         /**
498 |  |
          * Save the configuration if the automatic persistence is enabled and a file
499 |  |
          * is specified.
500 |  |
          */
501 |  |
         private void possiblySave()
502 |  |
         {
503 | 44 |
             if (autoSave && fileName != null)
504 |  |
             {
505 |  |
                 try
506 |  |
                 {
507 | 2 |
                     save();
508 |  |
                 }
509 | 0 |
                 catch (ConfigurationException ce)
510 |  |
                 {
511 | 0 |
                     throw new ConfigurationRuntimeException("Failed to auto-save", ce);
512 | 2 |
                 }
513 |  |
             }
514 | 44 |
         }
515 |  |
516 |  |
         /**
517 |  |
          * If true, changes are automatically persisted.
518 |  |
          *
519 |  |
          * @param autoSave
520 |  |
          */
521 |  |
         public void setAutoSave(boolean autoSave)
522 |  |
         {
523 | 1 |
             this.autoSave = autoSave;
524 | 1 |
         }
525 |  |
526 |  |
         /**
527 |  |
          * {@inheritDoc}
528 |  |
          */
529 |  |
         public void save(Writer writer) throws ConfigurationException
530 |  |
         {
531 |  |
             try
532 |  |
             {
533 | 3 |
                 Transformer transformer = TransformerFactory.newInstance().newTransformer();
534 | 3 |
                 Source source = new DOMSource(document);
535 | 3 |
                 Result result = new StreamResult(writer);
536 |  |
537 | 3 |
                 transformer.setOutputProperty("indent", "yes");
538 | 3 |
                 transformer.transform(source, result);
539 |  |
             }
540 | 0 |
             catch (TransformerException e)
541 |  |
             {
542 | 0 |
                 throw new ConfigurationException(e.getMessage(), e);
543 | 3 |
             }
544 | 3 |
         }
545 |  |
546 |  |
         public String toString()
547 |  |
         {
548 | 0 |
             StringWriter writer = new StringWriter();
549 |  |
             try
550 |  |
             {
551 | 0 |
                 save(writer);
552 |  |
             }
553 | 0 |
             catch (ConfigurationException e)
554 |  |
             {
555 | 0 |
                 e.printStackTrace();
556 | 0 |
             }
557 | 0 |
             return writer.toString();
558 |  |
         }
559 |  |
560 |  |
         /**
561 |  |
          * Parse a property key and return an array of the element hierarchy it
562 |  |
          * specifies. For example the key "x.y.z[@abc]" will result in [x, y, z].
563 |  |
          *
564 |  |
          * @param key the key to parse
565 |  |
          *
566 |  |
          * @return the elements in the key
567 |  |
          */
568 |  |
         protected static String[] parseElementNames(String key)
569 |  |
         {
570 | 74 |
             if (key == null)
571 |  |
             {
572 | 1 |
                 return new String[]{};
573 |  |
             }
574 |  |
             else
575 |  |
             {
576 |  |
                 // find the beginning of the attribute name
577 | 73 |
                 int attStart = key.indexOf(ATTRIBUTE_START);
578 |  |
579 | 73 |
                 if (attStart > -1)
580 |  |
                 {
581 |  |
                     // remove the attribute part of the key
582 | 24 |
                     key = key.substring(0, attStart);
583 |  |
                 }
584 |  |
585 | 73 |
                 return StringUtils.split(key, NODE_DELIMITER);
586 |  |
             }
587 |  |
         }
588 |  |
589 |  |
         /**
590 |  |
          * Parse a property key and return the attribute name if it existst.
591 |  |
          *
592 |  |
          * @param key the key to parse
593 |  |
          *
594 |  |
          * @return the attribute name, or null if the key doesn't contain one
595 |  |
          */
596 |  |
         protected static String parseAttributeName(String key)
597 |  |
         {
598 | 75 |
             String name = null;
599 |  |
600 | 75 |
             if (key != null)
601 |  |
             {
602 |  |
                 // find the beginning of the attribute name
603 | 74 |
                 int attStart = key.indexOf(ATTRIBUTE_START);
604 |  |
605 | 74 |
                 if (attStart > -1)
606 |  |
                 {
607 |  |
                     // find the end of the attribute name
608 | 25 |
                     int attEnd = key.indexOf(ATTRIBUTE_END);
609 | 25 |
                     attEnd = attEnd > -1 ? attEnd : key.length();
610 |  |
611 | 25 |
                     name = key.substring(attStart + ATTRIBUTE_START.length(), attEnd);
612 |  |
                 }
613 |  |
             }
614 |  |
615 | 75 |
             return name;
616 |  |
         }
617 |  |
     }

This report is generated by jcoverage, Maven and Maven JCoverage Plugin.