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}