[Aprog]
[aprog.git] / Aprog / src / net / sourceforge / aprog / xml / XMLTools.java
blob6723b7ddb58beb75ef755d22229b1592601150d0
1 /*
2 * The MIT License
3 *
4 * Copyright 2010 Codist Monk.
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a copy
7 * of this software and associated documentation files (the "Software"), to deal
8 * in the Software without restriction, including without limitation the rights
9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 * copies of the Software, and to permit persons to whom the Software is
11 * furnished to do so, subject to the following conditions:
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 * THE SOFTWARE.
25 package net.sourceforge.aprog.xml;
27 import static java.util.Arrays.asList;
28 import static java.util.Collections.unmodifiableList;
30 import static net.sourceforge.aprog.tools.Tools.*;
32 import java.io.ByteArrayInputStream;
33 import java.io.ByteArrayOutputStream;
34 import java.io.File;
35 import java.io.InputStream;
36 import java.io.OutputStream;
37 import java.io.OutputStreamWriter;
38 import java.util.ArrayList;
39 import java.util.LinkedHashMap;
40 import java.util.List;
41 import java.util.Locale;
42 import java.util.Map;
43 import java.util.logging.Level;
45 import javax.xml.XMLConstants;
46 import javax.xml.namespace.QName;
47 import javax.xml.parsers.DocumentBuilderFactory;
48 import javax.xml.parsers.ParserConfigurationException;
49 import javax.xml.parsers.SAXParserFactory;
50 import javax.xml.transform.OutputKeys;
51 import javax.xml.transform.Result;
52 import javax.xml.transform.Source;
53 import javax.xml.transform.Transformer;
54 import javax.xml.transform.TransformerConfigurationException;
55 import javax.xml.transform.TransformerException;
56 import javax.xml.transform.TransformerFactory;
57 import javax.xml.transform.dom.DOMSource;
58 import javax.xml.transform.stream.StreamResult;
59 import javax.xml.transform.stream.StreamSource;
60 import javax.xml.validation.SchemaFactory;
61 import javax.xml.validation.Validator;
62 import javax.xml.xpath.XPathConstants;
63 import javax.xml.xpath.XPathFactory;
65 import net.sourceforge.aprog.tools.IllegalInstantiationException;
67 import org.w3c.dom.Attr;
68 import org.w3c.dom.Document;
69 import org.w3c.dom.Element;
70 import org.w3c.dom.Node;
71 import org.w3c.dom.NodeList;
72 import org.w3c.dom.events.DocumentEvent;
73 import org.w3c.dom.events.EventListener;
74 import org.w3c.dom.events.EventTarget;
75 import org.w3c.dom.events.MutationEvent;
76 import org.xml.sax.InputSource;
77 import org.xml.sax.SAXException;
78 import org.xml.sax.SAXParseException;
79 import org.xml.sax.helpers.DefaultHandler;
81 /**
83 * @author codistmonk (creation 2010-07-02)
85 public final class XMLTools {
87 /**
88 * @throws IllegalInstantiationException To prevent instantiation
90 private XMLTools() {
91 throw new IllegalInstantiationException();
94 /**
95 * {@value}.
97 public static final String XML_1_0_UTF8 =
98 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
101 * {@value}.
103 public static final String XML_1_0_UTF8_STANDALONE_NO =
104 "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>";
107 * {@value}.
109 public static final String XML_1_0_UTF8_STANDALONE_YES =
110 "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>";
113 * {@value}.
115 public static final String DOM_EVENT_SUBTREE_MODIFIED = "DOMSubtreeModified";
118 * {@value}.
120 public static final String DOM_EVENT_NODE_INSERTED = "DOMNodeInserted";
123 * {@value}.
125 public static final String DOM_EVENT_NODE_REMOVED = "DOMNodeRemoved";
128 * {@value}.
130 public static final String DOM_EVENT_NODE_REMOVED_FROM_DOCUMENT = "DOMNodeRemovedFromDocument";
133 * {@value}.
135 public static final String DOM_EVENT_NODE_INSERTED_INTO_DOCUMENT = "DOMNodeInsertedIntoDocument";
138 * {@value}.
140 public static final String DOM_EVENT_ATTRIBUTE_MODIFIED = "DOMAttrModified";
143 * {@value}.
145 public static final String DOM_EVENT_CHARACTER_DATA_MODIFIED = "DOMCharacterDataModified";
147 public static final List<String> DOM_EVENT_TYPES = unmodifiableList(asList(
148 DOM_EVENT_ATTRIBUTE_MODIFIED,
149 DOM_EVENT_CHARACTER_DATA_MODIFIED,
150 DOM_EVENT_NODE_INSERTED,
151 DOM_EVENT_NODE_INSERTED_INTO_DOCUMENT,
152 DOM_EVENT_NODE_REMOVED,
153 DOM_EVENT_NODE_REMOVED_FROM_DOCUMENT,
154 DOM_EVENT_SUBTREE_MODIFIED
159 * @param node
160 * <br>Not null
161 * <br>Input-output
162 * @param listener
163 * <br>Not null
164 * <br>Shared
165 * @throws IllegalArgumentException If {@code node} doesn't have the "events 2.0" feature
167 public static final void addDOMEventListener(final Node node, final EventListener listener) {
168 checkHasEventsFeature(node);
170 for (final String eventType : DOM_EVENT_TYPES) {
171 ((EventTarget) node).addEventListener(eventType, listener, false);
177 * @param node
178 * <br>Not null
179 * <br>Input-output
180 * @param listener
181 * <br>Not null
182 * <br>Shared
183 * @throws IllegalArgumentException If {@code node} doesn't have the "events 2.0" feature
185 public static final void removeDOMEventListener(final Node node, final EventListener listener) {
186 checkHasEventsFeature(node);
188 for (final String eventType : DOM_EVENT_TYPES) {
189 ((EventTarget) node).removeEventListener(eventType, listener, false);
195 * @param node
196 * <br>Not null
197 * @param namespaceURI
198 * <br>Maybe null
199 * @param qualifiedName
200 * <br>Not null
201 * @return
202 * <br>Not null
203 * <br>Maybe new
205 public static final Node rename(final Node node, final String namespaceURI, final String qualifiedName) {
206 final String oldQualifiedName = getQualifiedName(node);
207 final Document document = getOwnerDocument(node);
208 final Node result = document.renameNode(node, namespaceURI, qualifiedName);
210 if (hasEventsFeature(node) && node.getNodeType() == Node.ELEMENT_NODE) {
211 final MutationEvent event = (MutationEvent) ((DocumentEvent) document).createEvent("MutationEvent");
213 event.initMutationEvent(DOM_EVENT_SUBTREE_MODIFIED, true, true,
214 node, oldQualifiedName, qualifiedName,
215 null, (short) 0);
217 ((EventTarget) result).dispatchEvent(event);
220 return result;
225 * @param node
226 * <br>Not null
227 * @throws IllegalArgumentException If {@code node} doesn't have the "events 2.0" feature
229 public static final void checkHasEventsFeature(final Node node) {
230 if (!hasEventsFeature(node)) {
231 throw new IllegalArgumentException("Events 2.0 feature unavailable for node " + node);
237 * @param node
238 * <br>Not null
239 * @return {@code true} if {@code node} has the "events 2.0" feature
241 public static final boolean hasEventsFeature(final Node node) {
242 return XMLTools.getOwnerDocument(node).getImplementation().hasFeature("events", "2.0");
247 * @param node
248 * <br>Not null
249 * @return
250 * <br>Not null
252 public static final String getQualifiedName(final Node node) {
253 return node.getPrefix() == null ? node.getNodeName() : node.getPrefix() + ":" + node.getLocalName();
258 * @param xmlInput
259 * <br>Not null
260 * @return
261 * <br>Not null
262 * <br>New
263 * @throws RuntimeException if an error occurs
265 public static final Document parse(final String xmlInput) {
266 return parse(new ByteArrayInputStream(xmlInput.getBytes()));
271 * @param xmlInputStream
272 * <br>Not null
273 * <br>Input-output
274 * @return
275 * <br>Not null
276 * <br>New
277 * @throws RuntimeException if an error occurs
279 public static final Document parse(final InputStream xmlInputStream) {
280 return parse(new InputSource(xmlInputStream));
285 * @param inputSource
286 * <br>Not null
287 * <br>Input-output
288 * @return
289 * <br>Not null
290 * <br>New
291 * @throws RuntimeException if an error occurs
293 public static final Document parse(final InputSource inputSource) {
294 try {
295 return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputSource);
296 } catch (final Exception exception) {
297 throw unchecked(exception);
303 * @return
304 * <br>Not null
305 * <br>New
307 public static final Document newDocument() {
308 try {
309 final Document result = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
311 result.setXmlStandalone(true);
313 return result;
314 } catch (final ParserConfigurationException exception) {
315 throw unchecked(exception);
321 * @param document
322 * <br>Not null
323 * <br>Input-output
324 * @return {@code document}
325 * <br>Not null
327 public static final Document normalize(final Document document) {
328 document.normalize();
330 return document;
335 * @param document
336 * <br>Not null
337 * <br>Input-output
338 * @return {@code document}
339 * <br>Not null
341 public static final Document standalone(final Document document) {
342 document.setXmlStandalone(true);
344 return document;
349 * @param node
350 * <br>Not null
351 * @param outputFile
352 * <br>Not null
353 * <br>Input-output
354 * @param indent
355 * <br>Range: {@code [0 .. Integer.MAX_VALUE]}
357 public static final void write(final Node node, final File outputFile, final int indent) {
358 write(node, new StreamResult(outputFile.getAbsolutePath()), indent);
363 * @param node
364 * <br>Not null
365 * @param output
366 * <br>Not null
367 * <br>Input-output
368 * @param indent
369 * <br>Range: {@code [0 .. Integer.MAX_VALUE]}
371 public static final void write(final Node node, final OutputStream output, final int indent) {
372 write(node, new StreamResult(new OutputStreamWriter(output)), indent);
377 * @param node
378 * <br>Not null
379 * @param output
380 * <br>Not null
381 * <br>Input-output
382 * @param indent
383 * <br>Range: {@code [0 .. Integer.MAX_VALUE]}
385 public static final void write(final Node node, final Result output, final int indent) {
386 try {
387 final TransformerFactory transformerFactory = TransformerFactory.newInstance();
389 transformerFactory.setAttribute("indent-number", indent);
391 final Transformer transformer = transformerFactory.newTransformer();
393 transformer.setOutputProperty(OutputKeys.METHOD, "xml");
394 transformer.setOutputProperty(OutputKeys.INDENT, indent != 0 ? "yes" : "no");
395 transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION,
396 node instanceof Document ? "no" : "yes");
397 transformer.setOutputProperty(OutputKeys.STANDALONE, "yes");
399 transformer.transform(new DOMSource(node), output);
400 } catch (final Exception exception) {
401 throw unchecked(exception);
407 * @param resourceName
408 * <br>Not null
409 * @return
410 * <br>Not null
411 * <br>New
413 public static final Source getResourceAsSource(final String resourceName) {
414 final ClassLoader classLoader = getCallerClass().getClassLoader();
415 final Source result = new StreamSource(classLoader.getResourceAsStream(resourceName));
417 result.setSystemId(classLoader.getResource(resourceName).toString());
419 return result;
424 * @param nodeList
425 * <br>Not null
426 * @return
427 * <br>Not null
428 * <br>New
430 public static final List<Node> toList(final NodeList nodeList) {
431 final List<Node> result = new ArrayList<Node>();
433 for (int i = 0; i < nodeList.getLength(); ++i) {
434 result.add(nodeList.item(i));
437 return result;
441 * Validates the XML input against the specified DTD or schema.
443 * @param xmlInputStream
444 * <br>Not null
445 * @param dtdOrSchema
446 * <br>Not null
447 * @return An empty list if validation succeeds
448 * <br>Not null
449 * <br>New
451 public static final List<Throwable> validate(final InputStream xmlInputStream, final Source dtdOrSchema) {
452 final List<Throwable> exceptions = new ArrayList<Throwable>();
453 final String schemaLanguage = getSchemaLanguage(dtdOrSchema);
455 try {
456 if (schemaLanguage != null) {
457 final Validator validator = SchemaFactory.newInstance(schemaLanguage)
458 .newSchema(dtdOrSchema).newValidator();
460 validator.validate(new StreamSource(xmlInputStream));
461 } else {
462 final Transformer addDoctypeInformation = TransformerFactory.newInstance().newTransformer();
464 addDoctypeInformation.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, dtdOrSchema.getSystemId());
466 final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
468 addDoctypeInformation.transform(new StreamSource(xmlInputStream), new StreamResult(buffer));
470 final SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
472 saxParserFactory.setValidating(true);
473 saxParserFactory.newSAXParser().parse(
474 new ByteArrayInputStream(buffer.toByteArray()), new DefaultHandler() {
476 @Override
477 public final void error(final SAXParseException exception) throws SAXException {
478 exceptions.add(exception);
480 getLoggerForThisMethod().log(Level.WARNING, "", exceptions);
483 @Override
484 public final void fatalError(final SAXParseException exception) throws SAXException {
485 exceptions.add(exception);
487 getLoggerForThisMethod().log(Level.WARNING, "", exceptions);
489 throw exception;
492 @Override
493 public final void warning(final SAXParseException exception) throws SAXException {
494 getLoggerForThisMethod().log(Level.WARNING, "", exceptions);
499 } catch (final TransformerConfigurationException exception) {
500 throw unchecked(exception);
501 } catch (final TransformerException exception) {
502 exceptions.add(exception);
503 } catch (final SAXException exception) {
504 exceptions.add(exception);
505 } catch (final Exception exception) {
506 throw unchecked(exception);
509 return exceptions;
513 * Determines the schema language from the argument's system id.
515 * @param dtdOrSchema
516 * <br>Not null
517 * @return {@code null} for a DTD
518 * <br>Maybe null
519 * <br>Range: { {@code null},
520 * {@link XMLConstants#W3C_XML_SCHEMA_NS_URI}, {@link XMLConstants#RELAXNG_NS_URI} }
521 * @throws IllegalArgumentException If {@code dtdOrSchema}'s system id does not indicate
522 * a DTD or a schema (XSD or RNG)
524 public static final String getSchemaLanguage(final Source dtdOrSchema) {
525 final String dtdOrSchemaId = dtdOrSchema.getSystemId().toLowerCase(Locale.ENGLISH);
527 if (dtdOrSchemaId.endsWith(".xsd")) {
528 return XMLConstants.W3C_XML_SCHEMA_NS_URI;
529 } else if (dtdOrSchemaId.endsWith(".rng") || dtdOrSchemaId.endsWith(".rnc")) {
530 return XMLConstants.RELAXNG_NS_URI;
531 } else if (dtdOrSchemaId.endsWith(".dtd")) {
532 return null;
535 throw new IllegalArgumentException("Unsupported extension for " + dtdOrSchema +
536 " (the name must end with .xsd, .rng, .rnc or .dtd (case-insensitive))");
540 * Calls {@link #getNode(java.lang.Object, java.lang.String)}.
542 * @param context
543 * <br>Maybe null
544 * @param xPath
545 * <br>Not null
546 * @return
547 * <br>Maybe null
548 * <br>Not New
550 public static final Node getNode(final Node context, final String xPath) {
551 return getNode((Object) context, xPath);
555 * Calls {@link #getNodeSet(java.lang.Object, java.lang.String)}.
557 * @param context
558 * <br>Maybe null
559 * @param xPath
560 * <br>Not null
561 * @return
562 * <br>Maybe null
563 * <br>Not New
565 public static final NodeList getNodeList(final Node context, final String xPath) {
566 return getNodeSet((Object) context, xPath);
570 * Calls {@link #toList(NodeList)} with the value returned by {@link #getNodeList(Node, String)}.
572 * @param context
573 * <br>Maybe null
574 * @param xPath
575 * <br>Not null
576 * @return
577 * <br>Maybe null
578 * <br>Not New
580 public static final List<Node> getNodes(final Node context, final String xPath) {
581 return toList(getNodeList(context, xPath));
585 * Calls {@link #get(java.lang.Object, java.lang.String, javax.xml.namespace.QName)}
586 * with {@code returnType == XPathConstants.NODE}.
588 * @param <N> The expected node type
589 * @param context
590 * <br>Maybe null
591 * @param xPath
592 * <br>Not null
593 * @return
594 * <br>Maybe null
595 * <br>Not New
597 @SuppressWarnings("unchecked")
598 public static final <N> N getNode(final Object context, final String xPath) {
599 return (N) get(context, xPath, XPathConstants.NODE);
603 * Calls {@link #get(java.lang.Object, java.lang.String, javax.xml.namespace.QName)}
604 * with {@code returnType == XPathConstants.NODESET}.
606 * @param <S> The expected node set type
607 * @param context
608 * <br>Maybe null
609 * @param xPath
610 * <br>Not null
611 * @return
612 * <br>Maybe null
613 * <br>Not New
615 @SuppressWarnings("unchecked")
616 public static final <S> S getNodeSet(final Object context, final String xPath) {
617 return (S) get(context, xPath, XPathConstants.NODESET);
621 * Calls {@link #get(java.lang.Object, java.lang.String, javax.xml.namespace.QName)}
622 * with {@code returnType == XPathConstants.BOOLEAN}.
624 * @param context
625 * <br>Maybe null
626 * @param xPath
627 * <br>Not null
628 * @return
629 * <br>Maybe null
630 * <br>Not New
632 public static final Boolean getBoolean(final Object context, final String xPath) {
633 return (Boolean) get(context, xPath, XPathConstants.BOOLEAN);
637 * Calls {@link #get(java.lang.Object, java.lang.String, javax.xml.namespace.QName)}
638 * with {@code returnType == XPathConstants.NUMBER}.
640 * @param context
641 * <br>Maybe null
642 * @param xPath
643 * <br>Not null
644 * @return
645 * <br>Maybe null
646 * <br>Not New
648 public static final Number getNumber(final Object context, final String xPath) {
649 return (Number) get(context, xPath, XPathConstants.NUMBER);
653 * Calls {@link #get(java.lang.Object, java.lang.String, javax.xml.namespace.QName)}
654 * with {@code returnType == XPathConstants.STRING}.
656 * @param context
657 * <br>Maybe null
658 * @param xPath
659 * <br>Not null
660 * @return
661 * <br>Maybe null
662 * <br>Not New
664 public static final String getString(final Object context, final String xPath) {
665 return (String) get(context, xPath, XPathConstants.STRING);
669 * Evaluates the compiled XPath expression in the specified context
670 * and returns the result as the specified type.
672 * @param <T> The expected return type
673 * @param context
674 * <br>Maybe null
675 * @param xPath
676 * <br>Not null
677 * @param returnType
678 * <br>Not null
679 * <br>Range: {
680 * {@link XPathConstants#BOOLEAN},
681 * {@link XPathConstants#NODE},
682 * {@link XPathConstants#NODESET},
683 * {@link XPathConstants#NUMBER},
684 * {@link XPathConstants#STRING}
686 * @return
687 * <br>Maybe null
688 * @throws RuntimeException If an error occurs
690 @SuppressWarnings("unchecked")
691 public static final <T> T get(final Object context, final String xPath, final QName returnType) {
692 try {
693 return (T) XPathFactory.newInstance().newXPath().compile(xPath)
694 .evaluate(context, returnType);
695 } catch (final Exception exception) {
696 throw unchecked(exception);
701 * Evaluates the quasi-XPath expression in the specified context, and returns the corresponding node,
702 * creating it if necessary.
703 * <br>The second argument is called "quasi-XPath" because it allows non-XPath expressions
704 * like {@code "a/b[]"} which means "add an element b at the end of a".
705 * <br>If {@code quasiXPath} is a standard XPath expression and the corresponding node exists,
706 * then that node is returned.
707 * <br>When the node does not exist,
708 * {@code quasiXPath} is broken down into path elements separated by slashes ("/").
709 * <br>A path element can be created if it is of the form "@name", "name", "name[]" or "name[attributes]" where
710 * "atributes" must be a sequence of "@attribute=value" separated by "and".
711 * <br>Example of a valid quasi-XPath expression where each path element can be created if necessary:<ul>
712 * <li>{@code "a/b[]/c[@d='e' and @f=42]"}
713 * </ul>
714 * <br>Example of a valid quasi-XPath expression where
715 * the path elements cannot be created if they don't exist:<ul>
716 * <li>{@code "a[last()]/b[@c<42]/d[position()=33]/e['f'=@g]"}
717 * </ul>
720 * @param context
721 * <br>Not null
722 * @param quasiXPath
723 * <br>Not null
724 * @return
725 * <br>Maybe null
726 * <br>Maybe new
728 public static final Node getOrCreateNode(final Node context, final String quasiXPath) {
729 final String[] pathElements = quasiXPath.split("/");
730 Node result = context;
732 try {
733 for (int index = 0; index < pathElements.length && result != null; ++index) {
734 final String pathElement = pathElements[index];
736 if (pathElement.matches("\\w+\\[\\]")) {
737 result = addChild(result, pathElement);
738 } else {
739 final Node parent = result;
740 result = getNode(result, pathElement);
742 if (result == null) {
743 final Map<String, String> attributes = getEqualityPredicates(pathElement);
744 final Integer nameEnd = pathElement.indexOf("[");
745 final String childName = nameEnd < 0 ? pathElement : pathElement.substring(0, nameEnd);
747 result = addChild(parent, childName);
749 for (final Map.Entry<String, String> entry : attributes.entrySet()) {
750 addChild(result, entry.getKey()).setNodeValue(entry.getValue());
755 } catch (final Exception exception) {
756 throw unchecked(exception);
759 return result;
763 * Creates and adds a new node to {@code parent}.
764 * <br>If {@code xPathElement} starts with "@",
765 * then the new node is an attribute, otherwise it is an element.
767 * @param parent
768 * <br>Not null
769 * <br>Input-output
770 * @param xPathElement
771 * <br>Not null
772 * @return
773 * <br>Not null
774 * <br>New
776 public static final Node addChild(final Node parent, final String xPathElement) {
777 final Boolean isAttribute = xPathElement.startsWith("@");
778 final String name = isAttribute ? xPathElement.substring(1) : xPathElement.split("\\[")[0];
779 final Document document = getOwnerDocument(parent);
780 final Node result = isAttribute ? document.createAttribute(name) : document.createElement(name);
782 if (isAttribute) {
783 ((Element) parent).setAttributeNode((Attr) result);
784 } else {
785 parent.appendChild(result);
788 return result;
793 * @param node
794 * <br>Not null
795 * @return
796 * <br>Maybe null
798 public static final Document getOwnerDocument(final Node node) {
799 if (node.getOwnerDocument() == null && node instanceof Document) {
800 return (Document) node;
803 return node.getOwnerDocument();
808 * @param xPathElement
809 * <br>Not null
810 * @return
811 * <br>Not null
812 * <br>New
814 private static final Map<String, String> getEqualityPredicates(final String xPathElement) {
815 final LinkedHashMap<String, String> result = new LinkedHashMap<String, String>();
816 final String trimmed = xPathElement.trim();
818 if (trimmed.endsWith("]")) {
819 final String constraints = trimmed.substring(trimmed.indexOf("[") + 1, trimmed.length() - 1).trim();
820 final StringBuilder buffer = new StringBuilder();
821 String attributeName = null;
822 int i = 0;
823 ScannerState state = ScannerState.ATTRIBUTE_NAME;
825 while (i < constraints.length()) {
826 final char c = constraints.charAt(i);
828 if (state == ScannerState.ATTRIBUTE_NAME && c == '=') {
829 attributeName = buffer.toString().trim();
831 buffer.setLength(0);
833 state = ScannerState.ATTRIBUTE_VALUE;
834 } else if (state == ScannerState.ATTRIBUTE_VALUE && c == '\\') {
835 buffer.append(c);
836 buffer.append(constraints.charAt(++i));
837 } else {
838 buffer.append(c);
840 final String trimmedBuffer = buffer.toString().trim();
842 if (state == ScannerState.ATTRIBUTE_VALUE && trimmedBuffer.length() > 1 && trimmedBuffer.charAt(0) == trimmedBuffer.charAt(trimmedBuffer.length() - 1)) {
843 result.put(attributeName, getString(null, trimmedBuffer));
845 buffer.setLength(0);
847 state = ScannerState.AND;
848 } else if (state == ScannerState.AND && "and".equalsIgnoreCase(trimmedBuffer)) {
849 buffer.setLength(0);
851 state = ScannerState.ATTRIBUTE_NAME;
856 ++i;
860 return result;
864 * @author codistmonk (creation 2010-07-29)
866 private enum ScannerState {
868 ATTRIBUTE_NAME, ATTRIBUTE_VALUE, AND;