001/*-------------------------------------------------------------------------+
002|                                                                          |
003| Copyright 2005-2011 The ConQAT Project                                   |
004|                                                                          |
005| Licensed under the Apache License, Version 2.0 (the "License");          |
006| you may not use this file except in compliance with the License.         |
007| 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+-------------------------------------------------------------------------*/
017package org.conqat.lib.commons.xml;
018
019import java.io.File;
020import java.io.FileInputStream;
021import java.io.IOException;
022import java.io.Reader;
023import java.io.StringReader;
024import java.net.URL;
025import java.nio.charset.Charset;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029
030import org.conqat.lib.commons.assertion.CCSMAssert;
031import org.conqat.lib.commons.enums.EnumUtils;
032import org.conqat.lib.commons.filesystem.FileSystemUtils;
033import org.conqat.lib.commons.string.StringUtils;
034import org.w3c.dom.Document;
035import org.w3c.dom.Element;
036import org.w3c.dom.NodeList;
037import org.xml.sax.InputSource;
038import org.xml.sax.SAXException;
039
040/**
041 * Utility class for reading XML documents. The XML document can be validated by
042 * an optional schema that is passed via {@link #setSchema(URL)}. Please consult
043 * test case XMLReaderTest to see how this class is intended to be used.
044 */
045public abstract class XMLReader<E extends Enum<E>, A extends Enum<A>, X extends Exception> {
046
047        /** The current DOM element. */
048        private Element currentDOMElement;
049
050        /** The schema URL to use or <code>null</code> if no schema is used. */
051        private URL schemaURL;
052
053        /** Resolver used by the writer. */
054        private final IXMLResolver<E, A> xmlResolver;
055
056        /** Reader that accesses content. */
057        private final Reader reader;
058
059        /**
060         * Create new reader.
061         * 
062         * @param file
063         *            the file to be read
064         * @param xmlResolver
065         *            resolvers used by this reader
066         */
067        public XMLReader(File file, IXMLResolver<E, A> xmlResolver) throws IOException {
068                this(file, null, xmlResolver);
069        }
070
071        /**
072         * Create reader.
073         * 
074         * @param file
075         *            the file to be read
076         * @param encoding
077         *            XML encoding of the file. No encoding is set if <code>null</code>.
078         * @param xmlResolver
079         *            resolvers used by this reader
080         */
081        public XMLReader(File file, Charset encoding, IXMLResolver<E, A> xmlResolver) throws IOException {
082                this(FileSystemUtils.streamReader(new FileInputStream(file), ensureNotNullEncoding(encoding)), xmlResolver);
083        }
084
085        /**
086         * Create reader.
087         * 
088         * @param content
089         *            XML string that gets parsed validation will be performed if
090         *            <code>null</code>.
091         * @param xmlResolver
092         *            resolvers used by this reader
093         */
094        public XMLReader(String content, IXMLResolver<E, A> xmlResolver) {
095                this(new StringReader(content), xmlResolver);
096        }
097
098        /**
099         * Create reader.
100         * 
101         * @param reader
102         *            the reader used to access the XML document.
103         * @param xmlResolver
104         *            resolvers used by this reader
105         */
106        public XMLReader(Reader reader, IXMLResolver<E, A> xmlResolver) {
107                CCSMAssert.isFalse(reader == null, "Reader may not be null.");
108                CCSMAssert.isFalse(xmlResolver == null, "XML resolver may not be null.");
109                this.reader = reader;
110                this.xmlResolver = xmlResolver;
111        }
112
113        /** Replaces a null value with the name of the default encoding. */
114        private static Charset ensureNotNullEncoding(Charset encoding) {
115                if (encoding == null) {
116                        return Charset.defaultCharset();
117                }
118                return encoding;
119        }
120
121        /** Sets the URL pointing to the schema that is used for validation. */
122        protected void setSchema(URL schemaURL) {
123                this.schemaURL = schemaURL;
124        }
125
126        /**
127         * Get <code>boolean</code> value of an attribute.
128         * 
129         * @return the boolean value, semantics for non-translatable or empty values is
130         *         defined by {@link Boolean#parseBoolean(String)}.
131         */
132        protected boolean getBooleanAttribute(A attribute) {
133                String value = getStringAttribute(attribute);
134                return Boolean.parseBoolean(value);
135        }
136
137        /**
138         * Get the text content of a child element of the current element.
139         * 
140         * @param childElement
141         *            the child element
142         * @return the text or <code>null</code> if the current element doesn't have the
143         *         requested child element
144         */
145        protected String getChildText(E childElement) {
146
147                String elementName = xmlResolver.resolveElementName(childElement);
148                Element domElement = XMLUtils.getNamedChild(currentDOMElement, elementName);
149                if (domElement == null) {
150                        return null;
151                }
152
153                return domElement.getTextContent();
154        }
155
156        /**
157         * Translate attribute value to an enumeration element.
158         * 
159         * @param attribute
160         *            the attribute
161         * @param enumClass
162         *            the enumeration class
163         * 
164         * @return the enum value, semantics for non-translatable or empty values is
165         *         defined by {@link Enum#valueOf(Class, String)}.
166         */
167        protected <T extends Enum<T>> T getEnumAttribute(A attribute, Class<T> enumClass) {
168                String value = getStringAttribute(attribute);
169                return Enum.valueOf(enumClass, value);
170        }
171
172        /**
173         * Translate attribute value to an enumeration element.
174         * 
175         * @param attribute
176         *            the attribute
177         * @param enumClass
178         *            the enumeration class
179         * @param defaultValue
180         *            the default value to return in case the attribute is not specified
181         *            or the enumeration does not contain the specified value.
182         * 
183         * @return The enum value, semantics for non-translatable or empty values is
184         *         defined by {@link EnumUtils#valueOfIgnoreCase(Class, String)}.
185         */
186        protected <T extends Enum<T>> T getEnumAttributeIgnoreCase(A attribute, Class<T> enumClass, T defaultValue) {
187                String value = getStringAttribute(attribute);
188                if (StringUtils.isEmpty(value)) {
189                        return defaultValue;
190                }
191                T result = EnumUtils.valueOfIgnoreCase(enumClass, value);
192                if (result == null) {
193                        return defaultValue;
194                }
195                return result;
196        }
197
198        /**
199         * Get <code>int</code> value of an attribute.
200         * 
201         * @return the int value, semantics for non-translatable or empty values is
202         *         defined by {@link Integer#parseInt(String)}.
203         */
204        protected int getIntAttribute(A attribute) {
205                String value = getStringAttribute(attribute);
206                return Integer.parseInt(value);
207        }
208
209        /**
210         * Get <code>long</code> value of an attribute.
211         * 
212         * @return the long value, semantics for non-translatable or empty values is
213         *         defined by {@link Long#parseLong(String)}.
214         */
215        protected long getLongAttribute(A attribute) {
216                String value = getStringAttribute(attribute);
217                return Long.parseLong(value);
218        }
219
220        /**
221         * Get attribute value.
222         * 
223         * 
224         * @return the attribute value or the empty string if attribute is undefined.
225         */
226        protected String getStringAttribute(A attribute) {
227                return currentDOMElement.getAttribute(xmlResolver.resolveAttributeName(attribute));
228        }
229
230        /**
231         * Get non-empty attribute value. If the value is null or the empty String, an
232         * {@link IOException} will be thrown.
233         */
234        protected String getNonEmptyStringAttribute(A attribute) throws IOException {
235                String value = getStringAttribute(attribute);
236                if (StringUtils.isEmpty(value)) {
237                        throw new IOException("Expected non-empty value for " + attribute.name());
238                }
239                return value;
240        }
241
242        /** Returns true if the current element has a given attribute. */
243        protected boolean hasAttribute(A attribute) {
244                return currentDOMElement.hasAttribute(xmlResolver.resolveAttributeName(attribute));
245        }
246
247        /**
248         * Get text content of current node.
249         */
250        protected String getText() {
251                return currentDOMElement.getTextContent();
252        }
253
254        /**
255         * Parse file. This sets the current element focus to the document root element.
256         * If schema URL was set the document is validated against the schema.
257         * <p>
258         * Sub classes should typically wrap this method with a proper error handling
259         * mechanism.
260         * 
261         * @throws SAXException
262         *             if a parsing exceptions occurs
263         * @throws IOException
264         *             if an IO exception occurs.
265         */
266        protected void parseFile() throws SAXException, IOException {
267                try {
268                        InputSource input = new InputSource(reader);
269                        Document document;
270                        if (schemaURL == null) {
271                                document = XMLUtils.parse(input);
272                        } else {
273                                document = XMLUtils.parse(input, schemaURL);
274                        }
275                        currentDOMElement = document.getDocumentElement();
276                } finally {
277                        reader.close();
278                }
279        }
280
281        /**
282         * Process the child elements of the current element with a given processor.
283         * Target elements are specified by
284         * {@link IXMLElementProcessor#getTargetElement()}.
285         * 
286         * @param processor
287         *            the processor used to process the elements
288         * @throws X
289         *             if the processor throws an exception
290         */
291        protected void processChildElements(IXMLElementProcessor<E, X> processor) throws X {
292                String targetElementName = xmlResolver.resolveElementName(processor.getTargetElement());
293                processElementList(processor, XMLUtils.getNamedChildren(currentDOMElement, targetElementName));
294        }
295
296        /**
297         * Process all descendant elements of the current element with a given
298         * processor. In contrast to
299         * {@link #processChildElements(IXMLElementProcessor)}, not only direct child
300         * elements are processed. Descendant elements are processed in the sequence
301         * they are found during a top-down, left-right traversal of the XML document.
302         * <p>
303         * Target elements are specified by
304         * {@link IXMLElementProcessor#getTargetElement()}.
305         * 
306         * @param processor
307         *            the processor used to process the elements
308         * @throws X
309         *             if the processor throws an exception
310         */
311        protected void processDecendantElements(IXMLElementProcessor<E, X> processor) throws X {
312                String targetElementName = xmlResolver.resolveElementName(processor.getTargetElement());
313
314                NodeList descendantNodes = currentDOMElement.getElementsByTagName(targetElementName);
315
316                processElementList(processor, XMLUtils.elementNodes(descendantNodes));
317        }
318
319        /**
320         * Processes the elements in the list with the given processor
321         * 
322         * @param processor
323         *            the processor used to process the elements
324         * @param elements
325         *            list of elements that get processed
326         * @throws X
327         *             if the processor throws an exception
328         */
329        private void processElementList(IXMLElementProcessor<E, X> processor, List<Element> elements) throws X {
330                Element oldElement = currentDOMElement;
331
332                for (Element child : elements) {
333                        currentDOMElement = child;
334                        processor.process();
335                }
336
337                currentDOMElement = oldElement;
338        }
339
340        /**
341         * This works similar to the template mechanism known from XSLT. It traverses
342         * the DOM tree starting from the current DOM element in depth-first fashion.
343         * For each element it checks if one of the provided processors has the current
344         * element as target element. If a matching processor is found, it is executed.
345         * 
346         * @throws AssertionError
347         *             if multiple processors apply to the same target element
348         */
349        @SuppressWarnings("unchecked")
350        protected void apply(IXMLElementProcessor<E, X>... processors) throws X {
351                Map<String, IXMLElementProcessor<E, X>> processorMap = new HashMap<>();
352
353                for (IXMLElementProcessor<E, X> processor : processors) {
354                        String targetElementName = xmlResolver.resolveElementName(processor.getTargetElement());
355                        CCSMAssert.isFalse(processorMap.containsKey(targetElementName),
356                                        "Multiple processors found for element: " + targetElementName);
357                        processorMap.put(targetElementName, processor);
358                }
359
360                Element oldElement = currentDOMElement;
361                traverse(processorMap);
362                currentDOMElement = oldElement;
363        }
364
365        /**
366         * Traverse element tree in depth-first fashion and execute the processors
367         * provided by the processor map.
368         */
369        private void traverse(Map<String, IXMLElementProcessor<E, X>> processorMap) throws X {
370                IXMLElementProcessor<E, X> processor = processorMap.get(currentDOMElement.getTagName());
371                if (processor != null) {
372                        processor.process();
373                }
374
375                NodeList nodeList = currentDOMElement.getChildNodes();
376
377                for (Element element : XMLUtils.elementNodes(nodeList)) {
378                        currentDOMElement = element;
379                        traverse(processorMap);
380                }
381        }
382
383}