Coverage Report - org.xembly.Xembler
 
Classes in this File Line Coverage Branch Coverage Complexity
Xembler
62%
45/72
50%
19/38
4.875
 
 1  
 /**
 2  
  * Copyright (c) 2013-2017, xembly.org
 3  
  * All rights reserved.
 4  
  *
 5  
  * Redistribution and use in source and binary forms, with or without
 6  
  * modification, are permitted provided that the following conditions
 7  
  * are met: 1) Redistributions of source code must retain the above
 8  
  * copyright notice, this list of conditions and the following
 9  
  * disclaimer. 2) Redistributions in binary form must reproduce the above
 10  
  * copyright notice, this list of conditions and the following
 11  
  * disclaimer in the documentation and/or other materials provided
 12  
  * with the distribution. 3) Neither the name of the xembly.org nor
 13  
  * the names of its contributors may be used to endorse or promote
 14  
  * products derived from this software without specific prior written
 15  
  * permission.
 16  
  *
 17  
  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 18  
  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
 19  
  * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 20  
  * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 21  
  * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 22  
  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 23  
  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 24  
  * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 25  
  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 26  
  * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 27  
  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 28  
  * OF THE POSSIBILITY OF SUCH DAMAGE.
 29  
  */
 30  
 package org.xembly;
 31  
 
 32  
 import java.io.StringWriter;
 33  
 import java.util.Collections;
 34  
 import javax.xml.parsers.DocumentBuilderFactory;
 35  
 import javax.xml.parsers.ParserConfigurationException;
 36  
 import javax.xml.transform.OutputKeys;
 37  
 import javax.xml.transform.Transformer;
 38  
 import javax.xml.transform.TransformerConfigurationException;
 39  
 import javax.xml.transform.TransformerException;
 40  
 import javax.xml.transform.TransformerFactory;
 41  
 import javax.xml.transform.dom.DOMSource;
 42  
 import javax.xml.transform.stream.StreamResult;
 43  
 import lombok.EqualsAndHashCode;
 44  
 import lombok.ToString;
 45  
 import org.w3c.dom.DOMException;
 46  
 import org.w3c.dom.Document;
 47  
 import org.w3c.dom.Node;
 48  
 
 49  
 /**
 50  
  * Processor of Xembly directives, main entry point to the package.
 51  
  *
 52  
  * <p>For example, to modify a DOM document:
 53  
  *
 54  
  * <pre> Document dom = DocumentBuilderFactory.newInstance()
 55  
  *   .newDocumentBuilder().newDocument();
 56  
  * dom.appendChild(dom.createElement("root"));
 57  
  * new Xembler(
 58  
  *   new Directives()
 59  
  *     .xpath("/root")
 60  
  *     .addIfAbsent("employees")
 61  
  *     .add("employee")
 62  
  *     .attr("id", 6564)
 63  
  * ).apply(dom);</pre>
 64  
  *
 65  
  * <p>You can also convert your Xembly directives directly to XML document:
 66  
  *
 67  
  * <pre> String xml = new Xembler(
 68  
  *   new Directives()
 69  
  *     .xpath("/root")
 70  
  *     .addIfAbsent("employees")
 71  
  *     .add("employee")
 72  
  *     .attr("id", 6564)
 73  
  * ).xml("root");</pre>
 74  
  *
 75  
  * <p>Since version 0.18 you can convert directives to XML without
 76  
  * a necessity to catch checked exceptions.
 77  
  * Use {@code *Quietly()} methods for that:
 78  
  * {@link #xmlQuietly()}, {@link #domQuietly()},
 79  
  * and {@link #applyQuietly(org.w3c.dom.Node)}.
 80  
  *
 81  
  * @author Yegor Bugayenko (yegor256@gmail.com)
 82  
  * @version $Id: 3a2a0533da3086f769169501ab1b5749eea75236 $
 83  
  * @since 0.1
 84  
  * @checkstyle ClassDataAbstractionCouplingCheck (500 lines)
 85  
  */
 86  0
 @ToString
 87  0
 @EqualsAndHashCode(of = "directives")
 88  
 public final class Xembler {
 89  
 
 90  
     /**
 91  
      * Builder factory.
 92  
      */
 93  
     private static final DocumentBuilderFactory BFACTORY =
 94  1
         DocumentBuilderFactory.newInstance();
 95  
 
 96  
     /**
 97  
      * Transformer factory.
 98  
      */
 99  
     private static final TransformerFactory TFACTORY =
 100  1
         TransformerFactory.newInstance();
 101  
 
 102  
     /**
 103  
      * Array of directives.
 104  
      */
 105  
     private final transient Iterable<Directive> directives;
 106  
 
 107  
     static {
 108  1
         Xembler.BFACTORY.setNamespaceAware(true);
 109  1
         Xembler.BFACTORY.setValidating(false);
 110  1
         Xembler.BFACTORY.setCoalescing(false);
 111  1
     }
 112  
 
 113  
     /**
 114  
      * Public ctor.
 115  
      * @param dirs Directives
 116  
      */
 117  34
     public Xembler(final Iterable<Directive> dirs) {
 118  34
         this.directives = dirs;
 119  34
     }
 120  
 
 121  
     /**
 122  
      * Apply all changes to the document/node, without any checked exceptions.
 123  
      * @param dom DOM document/node
 124  
      * @return The same document/node
 125  
      * @since 0.18
 126  
      */
 127  
     public Node applyQuietly(final Node dom) {
 128  
         try {
 129  1
             return this.apply(dom);
 130  0
         } catch (final ImpossibleModificationException ex) {
 131  0
             throw new IllegalArgumentException(
 132  0
                 String.format(
 133  
                     "failed to apply to DOM quietly: %s",
 134  
                     this.directives
 135  
                 ),
 136  
                 ex
 137  
             );
 138  
         }
 139  
     }
 140  
 
 141  
     /**
 142  
      * Apply all changes to the document/node.
 143  
      * @param dom DOM document/node
 144  
      * @return The same document/node
 145  
      * @throws ImpossibleModificationException If can't modify
 146  
      */
 147  
     public Node apply(final Node dom) throws ImpossibleModificationException {
 148  34
         Directive.Cursor cursor = new DomCursor(
 149  34
             Collections.singletonList(dom)
 150  
         );
 151  34
         int pos = 1;
 152  34
         final Directive.Stack stack = new DomStack();
 153  34
         for (final Directive dir : this.directives) {
 154  
             try {
 155  580
                 cursor = dir.exec(dom, cursor, stack);
 156  0
             } catch (final ImpossibleModificationException ex) {
 157  0
                 throw new ImpossibleModificationException(
 158  0
                     String.format("directive #%d: %s", pos, dir),
 159  
                     ex
 160  
                 );
 161  4
             } catch (final DOMException ex) {
 162  4
                 throw new ImpossibleModificationException(
 163  4
                     String.format("DOM exception at dir #%d: %s", pos, dir),
 164  
                     ex
 165  
                 );
 166  576
             }
 167  576
             ++pos;
 168  576
         }
 169  30
         return dom;
 170  
     }
 171  
 
 172  
     /**
 173  
      * Apply all changes to an empty DOM, without checked exceptions.
 174  
      * @return DOM created
 175  
      * @since 0.18
 176  
      */
 177  
     public Document domQuietly() {
 178  
         try {
 179  0
             return this.dom();
 180  0
         } catch (final ImpossibleModificationException ex) {
 181  0
             throw new IllegalStateException(
 182  0
                 String.format(
 183  
                     "failed to create DOM quietly: %s",
 184  
                     this.directives
 185  
                 ),
 186  
                 ex
 187  
             );
 188  
         }
 189  
     }
 190  
 
 191  
     /**
 192  
      * Apply all changes to an empty DOM.
 193  
      * @return DOM created
 194  
      * @throws ImpossibleModificationException If can't modify
 195  
      * @since 0.9
 196  
      */
 197  
     public Document dom() throws ImpossibleModificationException {
 198  
         final Document dom;
 199  
         try {
 200  8
             dom = Xembler.BFACTORY.newDocumentBuilder().newDocument();
 201  0
         } catch (final ParserConfigurationException ex) {
 202  0
             throw new IllegalStateException(
 203  0
                 String.format(
 204  
                     "failed to obtain a new DOM document from %s",
 205  0
                     Xembler.BFACTORY.getClass().getCanonicalName()
 206  
                 ),
 207  
                 ex
 208  
             );
 209  8
         }
 210  8
         this.apply(dom);
 211  8
         return dom;
 212  
     }
 213  
 
 214  
     /**
 215  
      * Convert to XML document, without checked exceptions.
 216  
      * @return XML document
 217  
      * @since 0.18
 218  
      */
 219  
     public String xmlQuietly() {
 220  
         try {
 221  1
             return this.xml();
 222  0
         } catch (final ImpossibleModificationException ex) {
 223  0
             throw new IllegalStateException(
 224  0
                 String.format(
 225  
                     "failed to build XML quietly: %s",
 226  
                     this.directives
 227  
                 ),
 228  
                 ex
 229  
             );
 230  
         }
 231  
     }
 232  
 
 233  
     /**
 234  
      * Convert to XML document.
 235  
      * @return XML document
 236  
      * @throws ImpossibleModificationException If can't modify
 237  
      * @since 0.9
 238  
      */
 239  
     public String xml() throws ImpossibleModificationException {
 240  
         final Transformer transformer;
 241  
         try {
 242  8
             transformer = Xembler.TFACTORY.newTransformer();
 243  0
         } catch (final TransformerConfigurationException ex) {
 244  0
             throw new IllegalStateException(
 245  0
                 String.format(
 246  
                     "failed to create new Transformer at %s",
 247  0
                     Xembler.TFACTORY.getClass().getCanonicalName()
 248  
                 ),
 249  
                 ex
 250  
             );
 251  8
         }
 252  8
         transformer.setOutputProperty(OutputKeys.INDENT, "yes");
 253  8
         transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
 254  8
         final StringWriter writer = new StringWriter();
 255  
         try {
 256  16
             transformer.transform(
 257  8
                 new DOMSource(this.dom()),
 258  
                 new StreamResult(writer)
 259  
             );
 260  0
         } catch (final TransformerException ex) {
 261  0
             throw new IllegalArgumentException(
 262  0
                 String.format(
 263  
                     "failed to transform DOM to text by %s",
 264  0
                     transformer.getClass().getCanonicalName()
 265  
                 ),
 266  
                 ex
 267  
             );
 268  8
         }
 269  8
         return writer.toString();
 270  
     }
 271  
 
 272  
     /**
 273  
      * Utility method to escape text before using it as a text value
 274  
      * in XML.
 275  
      *
 276  
      * <p>Use it like this, in order to avoid runtime exceptions:
 277  
      *
 278  
      * <pre>new Directives().xpath("/test")
 279  
      *   .set(Xembler.escape("illegal: \u0000"));</pre>
 280  
      *
 281  
      * @param text Text to escape
 282  
      * @return The same text with escaped characters, which are not XML-legal
 283  
      * @since 0.14
 284  
      * @checkstyle MagicNumber (20 lines)
 285  
      * @checkstyle CyclomaticComplexity (20 lines)
 286  
      * @checkstyle BooleanExpressionComplexity (20 lines)
 287  
      */
 288  
     public static String escape(final String text) {
 289  1
         final StringBuilder output = new StringBuilder(text.length());
 290  1
         final char[] chars = text.toCharArray();
 291  15
         for (final char chr : chars) {
 292  14
             final boolean illegal = chr >= 0x00 && chr <= 0x08
 293  
                 || chr >= 0x0B && chr <= 0x0C
 294  
                 || chr >= 0x0E && chr <= 0x1F
 295  
                 || chr >= 0x7F && chr <= 0x84
 296  
                 || chr >= 0x86 && chr <= 0x9F;
 297  14
             if (illegal) {
 298  1
                 output.append(String.format("\\u%04x", (int) chr));
 299  
             } else {
 300  13
                 output.append(chr);
 301  
             }
 302  
         }
 303  1
         return output.toString();
 304  
     }
 305  
 
 306  
 }