Coverage Report - org.xembly.Directives
 
Classes in this File Line Coverage Branch Coverage Complexity
Directives
71%
97/135
64%
27/42
2.909
 
 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.util.Collection;
 33  
 import java.util.Collections;
 34  
 import java.util.Iterator;
 35  
 import java.util.LinkedList;
 36  
 import java.util.Map;
 37  
 import java.util.concurrent.CopyOnWriteArrayList;
 38  
 import lombok.EqualsAndHashCode;
 39  
 import org.antlr.runtime.ANTLRStringStream;
 40  
 import org.antlr.runtime.CharStream;
 41  
 import org.antlr.runtime.CommonTokenStream;
 42  
 import org.antlr.runtime.RecognitionException;
 43  
 import org.antlr.runtime.TokenStream;
 44  
 import org.w3c.dom.Attr;
 45  
 import org.w3c.dom.NamedNodeMap;
 46  
 import org.w3c.dom.Node;
 47  
 import org.w3c.dom.NodeList;
 48  
 
 49  
 /**
 50  
  * Collection of {@link Directive}s, instantiable from {@link String}.
 51  
  *
 52  
  * <p>For example, to fetch directives from a string and apply to the
 53  
  * DOM document:
 54  
  *
 55  
  * <pre> Document dom = DocumentBuilderFactory.newInstance()
 56  
  *   .newDocumentBuilder().newDocument();
 57  
  * dom.appendChild(dom.createElement("root"));
 58  
  * new Xembler(
 59  
  *   new Directives("XPATH 'root'; ADD 'employee';")
 60  
  * ).apply(dom);</pre>
 61  
  *
 62  
  * <p>{@link Directives} can be used as a builder of Xembly script:
 63  
  *
 64  
  * <pre> Document dom = DocumentBuilderFactory.newInstance()
 65  
  *   .newDocumentBuilder().newDocument();
 66  
  * dom.appendChild(dom.createElement("root"));
 67  
  * new Xembler(
 68  
  *   new Directives()
 69  
  *     .xpath("/root")
 70  
  *     .addIf("employees")
 71  
  *     .add("employee")
 72  
  *     .attr("id", 6564)
 73  
  *     .up()
 74  
  *     .xpath("employee[&#64;id='100']")
 75  
  *     .strict(1)
 76  
  *     .remove()
 77  
  * ).apply(dom);</pre>
 78  
  *
 79  
  * <p>The class is mutable and thread-safe.
 80  
  *
 81  
  * @author Yegor Bugayenko (yegor256@gmail.com)
 82  
  * @version $Id: b15d239ba796a01432b37940de6cebc1472674bf $
 83  
  * @since 0.1
 84  
  * @checkstyle ClassDataAbstractionCoupling (500 lines)
 85  
  * @checkstyle ClassFanOutComplexity (500 lines)
 86  
  */
 87  0
 @EqualsAndHashCode(callSuper = false, of = "all")
 88  
 @SuppressWarnings
 89  
     (
 90  
         {
 91  
             "PMD.TooManyMethods",
 92  
             "PMD.CyclomaticComplexity",
 93  
             "PMD.GodClass",
 94  
             "PMD.StdCyclomaticComplexity"
 95  
         }
 96  
     )
 97  
 public final class Directives implements Iterable<Directive> {
 98  
 
 99  
     /**
 100  
      * Right margin.
 101  
      */
 102  
     private static final int MARGIN = 80;
 103  
 
 104  
     /**
 105  
      * List of directives.
 106  
      */
 107  98
     private final transient Collection<Directive> all =
 108  
         new CopyOnWriteArrayList<Directive>();
 109  
 
 110  
     /**
 111  
      * Public ctor.
 112  
      */
 113  
     public Directives() {
 114  73
         this(Collections.<Directive>emptyList());
 115  73
     }
 116  
 
 117  
     /**
 118  
      * Public ctor.
 119  
      * @param text Xembly script
 120  
      * @throws SyntaxException If syntax is broken
 121  
      */
 122  
     public Directives(final String text) throws SyntaxException {
 123  29
         this(Directives.parse(text));
 124  25
     }
 125  
 
 126  
     /**
 127  
      * Public ctor.
 128  
      * @param dirs Directives
 129  
      */
 130  98
     public Directives(final Iterable<Directive> dirs) {
 131  98
         this.append(dirs);
 132  98
     }
 133  
 
 134  
     @Override
 135  
     public String toString() {
 136  4
         final StringBuilder text = new StringBuilder(0);
 137  4
         int width = 0;
 138  4
         int idx = 0;
 139  4
         for (final Directive dir : this.all) {
 140  40
             if (idx > 0 && width == 0) {
 141  4
                 text.append('\n').append(idx).append(':');
 142  
             }
 143  40
             final String txt = dir.toString();
 144  40
             text.append(txt).append(';');
 145  40
             width += txt.length();
 146  40
             if (width > Directives.MARGIN) {
 147  4
                 width = 0;
 148  
             }
 149  40
             ++idx;
 150  40
         }
 151  4
         return text.toString().trim();
 152  
     }
 153  
 
 154  
     @Override
 155  
     public Iterator<Directive> iterator() {
 156  94
         return this.all.iterator();
 157  
     }
 158  
 
 159  
     /**
 160  
      * Create a collection of directives, which can create a copy
 161  
      * of provided node.
 162  
      *
 163  
      * <p>For example, you already have a node in an XML document,
 164  
      * which you'd like to add to another XML document:
 165  
      *
 166  
      * <pre> Document target = parse("&lt;root/&gt;");
 167  
      * Node node = parse("&lt;user name='Jeffrey'/&gt;");
 168  
      * new Xembler(
 169  
      *   new Directives()
 170  
      *     .xpath("/*")
 171  
      *     .add("jeff")
 172  
      *     .append(Directives.copyOf(node))
 173  
      * ).apply(target);
 174  
      * assert print(target).equals(
 175  
      *   "&lt;root&gt;&lt;jeff name='Jeffrey'&gt;&lt;/root&gt;"
 176  
      * );
 177  
      * </pre>
 178  
      *
 179  
      * @param node Node to analyze
 180  
      * @return Collection of directives
 181  
      * @since 0.13
 182  
      * @checkstyle CyclomaticComplexity (50 lines)
 183  
      */
 184  
     @SuppressWarnings("PMD.StdCyclomaticComplexity")
 185  
     public static Iterable<Directive> copyOf(final Node node) {
 186  8
         final Directives dirs = new Directives();
 187  8
         if (node.hasAttributes()) {
 188  2
             final NamedNodeMap attrs = node.getAttributes();
 189  2
             final int len = attrs.getLength();
 190  4
             for (int idx = 0; idx < len; ++idx) {
 191  2
                 final Attr attr = Attr.class.cast(attrs.item(idx));
 192  2
                 dirs.attr(attr.getNodeName(), attr.getNodeValue());
 193  
             }
 194  
         }
 195  8
         if (node.hasChildNodes()) {
 196  6
             final NodeList children = node.getChildNodes();
 197  6
             final int len = children.getLength();
 198  18
             for (int idx = 0; idx < len; ++idx) {
 199  12
                 final Node child = children.item(idx);
 200  12
                 switch (child.getNodeType()) {
 201  
                     case Node.ELEMENT_NODE:
 202  7
                         dirs.add(child.getNodeName())
 203  7
                             .append(Directives.copyOf(child))
 204  7
                             .up();
 205  7
                         break;
 206  
                     case Node.ATTRIBUTE_NODE:
 207  0
                         dirs.attr(child.getNodeName(), child.getNodeValue());
 208  0
                         break;
 209  
                     case Node.TEXT_NODE:
 210  
                     case Node.CDATA_SECTION_NODE:
 211  3
                         if (len == 1) {
 212  2
                             dirs.set(child.getTextContent());
 213  1
                         } else if (!child.getTextContent().trim().isEmpty()) {
 214  0
                             throw new IllegalArgumentException(
 215  0
                                 String.format(
 216  
                                     // @checkstyle LineLength (1 line)
 217  
                                     "TEXT node #%d is not allowed together with other %d nodes in %s",
 218  0
                                     idx, len, child.getNodeName()
 219  
                                 )
 220  
                             );
 221  
                         }
 222  
                         break;
 223  
                     case Node.PROCESSING_INSTRUCTION_NODE:
 224  1
                         dirs.pi(child.getNodeName(), child.getNodeValue());
 225  1
                         break;
 226  
                     case Node.ENTITY_NODE:
 227  
                     case Node.COMMENT_NODE:
 228  1
                         break;
 229  
                     default:
 230  0
                         throw new IllegalArgumentException(
 231  0
                             String.format(
 232  
                                 "unsupported type %d of node %s",
 233  0
                                 child.getNodeType(), child.getNodeName()
 234  
                             )
 235  
                         );
 236  
                 }
 237  
             }
 238  
         }
 239  8
         return dirs;
 240  
     }
 241  
 
 242  
     /**
 243  
      * Append all directives.
 244  
      * @param dirs Directives to append
 245  
      * @return This object
 246  
      * @since 0.11
 247  
      */
 248  
     public Directives append(final Iterable<Directive> dirs) {
 249  155
         final Collection<Directive> list = new LinkedList<Directive>();
 250  155
         for (final Directive dir : dirs) {
 251  577
             list.add(dir);
 252  576
         }
 253  156
         this.all.addAll(list);
 254  156
         return this;
 255  
     }
 256  
 
 257  
     /**
 258  
      * Add node to all current nodes.
 259  
      * @param name Name of the node to add
 260  
      * @return This object
 261  
      * @since 0.5
 262  
      */
 263  
     public Directives add(final Object name) {
 264  
         try {
 265  144
             this.all.add(new AddDirective(name.toString()));
 266  0
         } catch (final XmlContentException ex) {
 267  0
             throw new IllegalArgumentException(
 268  0
                 String.format(
 269  
                     "failed to understand XML content, ADD(%s)",
 270  
                     name
 271  
                 ),
 272  
                 ex
 273  
             );
 274  144
         }
 275  144
         return this;
 276  
     }
 277  
 
 278  
     /**
 279  
      * Add multiple nodes and set their text values.
 280  
      *
 281  
      * <p>Every pair in the provided map will be treated as a new
 282  
      * node name and value. It's a convenient utility method that simplifies
 283  
      * the process of adding a collection of nodes with pre-set values. For
 284  
      * example:
 285  
      *
 286  
      * <pre> new Directives()
 287  
      *   .add("first", "hello, world!")
 288  
      *   .add(
 289  
      *     new ArrayMap&lt;String, Object&gt;()
 290  
      *       .with("alpha", 1)
 291  
      *       .with("beta", "2")
 292  
      *       .with("gamma", new Date())
 293  
      *   )
 294  
      *   .add("second");
 295  
      * </pre>
 296  
      *
 297  
      * <p>If a value provided contains illegal XML characters, a runtime
 298  
      * exception will be thrown. To avoid this, it is recommended to use
 299  
      * {@link Xembler#escape(String)}.
 300  
      *
 301  
      * @param <K> Type of key
 302  
      * @param <V> Type of value
 303  
      * @param nodes Names and values of nodes to add
 304  
      * @return This object
 305  
      * @since 0.8
 306  
      */
 307  
     public <K, V> Directives add(final Map<K, V> nodes) {
 308  1
         for (final Map.Entry<K, V> entry : nodes.entrySet()) {
 309  2
             this.add(entry.getKey().toString())
 310  2
                 .set(entry.getValue().toString())
 311  2
                 .up();
 312  2
         }
 313  1
         return this;
 314  
     }
 315  
 
 316  
     /**
 317  
      * Add node if it's absent.
 318  
      * @param name Name of the node to add
 319  
      * @return This object
 320  
      * @since 0.5
 321  
      */
 322  
     public Directives addIf(final Object name) {
 323  
         try {
 324  2
             this.all.add(new AddIfDirective(name.toString()));
 325  0
         } catch (final XmlContentException ex) {
 326  0
             throw new IllegalArgumentException(
 327  0
                 String.format(
 328  
                     "failed to understand XML content, ADDIF(%s)",
 329  
                     name
 330  
                 ),
 331  
                 ex
 332  
             );
 333  2
         }
 334  2
         return this;
 335  
     }
 336  
 
 337  
     /**
 338  
      * Remove all current nodes and move cursor to their parents.
 339  
      * @return This object
 340  
      * @since 0.5
 341  
      */
 342  
     public Directives remove() {
 343  4
         this.all.add(new RemoveDirective());
 344  4
         return this;
 345  
     }
 346  
 
 347  
     /**
 348  
      * Set attribute.
 349  
      *
 350  
      * <p>If a value provided contains illegal XML characters, a runtime
 351  
      * exception will be thrown. To avoid this, it is recommended to use
 352  
      * {@link Xembler#escape(String)}.
 353  
      *
 354  
      * @param name Name of the attribute
 355  
      * @param value Value to set
 356  
      * @return This object
 357  
      * @since 0.5
 358  
      */
 359  
     public Directives attr(final Object name, final Object value) {
 360  
         try {
 361  108
             this.all.add(new AttrDirective(name.toString(), value.toString()));
 362  0
         } catch (final XmlContentException ex) {
 363  0
             throw new IllegalArgumentException(
 364  0
                 String.format(
 365  
                     "failed to understand XML content, ATTR(%s, %s)",
 366  
                     name, value
 367  
                 ),
 368  
                 ex
 369  
             );
 370  107
         }
 371  107
         return this;
 372  
     }
 373  
 
 374  
     /**
 375  
      * Add processing instruction.
 376  
      *
 377  
      * <p>If a value provided contains illegal XML characters, a runtime
 378  
      * exception will be thrown. To avoid this, it is recommended to use
 379  
      * {@link Xembler#escape(String)}.
 380  
      *
 381  
      * @param target PI name
 382  
      * @param data Data to set
 383  
      * @return This object
 384  
      * @since 0.9
 385  
      * @checkstyle MethodName (3 lines)
 386  
      */
 387  
     @SuppressWarnings("PMD.ShortMethodName")
 388  
     public Directives pi(final Object target, final Object data) {
 389  
         try {
 390  2
             this.all.add(new PiDirective(target.toString(), data.toString()));
 391  0
         } catch (final XmlContentException ex) {
 392  0
             throw new IllegalArgumentException(
 393  0
                 String.format(
 394  
                     "failed to understand XML content, PI(%s, %s)",
 395  
                     target, data
 396  
                 ),
 397  
                 ex
 398  
             );
 399  2
         }
 400  2
         return this;
 401  
     }
 402  
 
 403  
     /**
 404  
      * Set text content.
 405  
      *
 406  
      * <p>If a value provided contains illegal XML characters, a runtime
 407  
      * exception will be thrown. To avoid this, it is recommended to use
 408  
      * {@link Xembler#escape(String)}.
 409  
      *
 410  
      * @param text Text to set
 411  
      * @return This object
 412  
      * @since 0.5
 413  
      */
 414  
     public Directives set(final Object text) {
 415  
         try {
 416  109
             this.all.add(new SetDirective(text.toString()));
 417  0
         } catch (final XmlContentException ex) {
 418  0
             throw new IllegalArgumentException(
 419  0
                 String.format(
 420  
                     "failed to understand XML content, SET(%s)",
 421  
                     text
 422  
                 ),
 423  
                 ex
 424  
             );
 425  109
         }
 426  109
         return this;
 427  
     }
 428  
 
 429  
     /**
 430  
      * Set text content.
 431  
      * @param text Text to set
 432  
      * @return This object
 433  
      * @since 0.7
 434  
      */
 435  
     public Directives xset(final Object text) {
 436  
         try {
 437  0
             this.all.add(new XsetDirective(text.toString()));
 438  0
         } catch (final XmlContentException ex) {
 439  0
             throw new IllegalArgumentException(
 440  0
                 String.format(
 441  
                     "failed to understand XML content, XSET(%s)",
 442  
                     text
 443  
                 ),
 444  
                 ex
 445  
             );
 446  0
         }
 447  0
         return this;
 448  
     }
 449  
 
 450  
     /**
 451  
      * Go one node/level up.
 452  
      * @return This object
 453  
      * @since 0.5
 454  
      * @checkstyle MethodName (3 lines)
 455  
      */
 456  
     @SuppressWarnings("PMD.ShortMethodName")
 457  
     public Directives up() {
 458  112
         this.all.add(new UpDirective());
 459  111
         return this;
 460  
     }
 461  
 
 462  
     /**
 463  
      * Go to XPath.
 464  
      * @param path Path to go to
 465  
      * @return This object
 466  
      * @since 0.5
 467  
      */
 468  
     public Directives xpath(final Object path) {
 469  
         try {
 470  8
             this.all.add(new XpathDirective(path.toString()));
 471  0
         } catch (final XmlContentException ex) {
 472  0
             throw new IllegalArgumentException(
 473  0
                 String.format(
 474  
                     "failed to understand XML content, XPATH(%s)",
 475  
                     path
 476  
                 ),
 477  
                 ex
 478  
             );
 479  8
         }
 480  8
         return this;
 481  
     }
 482  
 
 483  
     /**
 484  
      * Check that there is exactly this number of current nodes.
 485  
      * @param number Number of expected nodes
 486  
      * @return This object
 487  
      * @since 0.5
 488  
      */
 489  
     public Directives strict(final int number) {
 490  2
         this.all.add(new StrictDirective(number));
 491  2
         return this;
 492  
     }
 493  
 
 494  
     /**
 495  
      * Push current cursor to stack.
 496  
      * @return This object
 497  
      * @since 0.16
 498  
      */
 499  
     public Directives push() {
 500  2
         this.all.add(new PushDirective());
 501  2
         return this;
 502  
     }
 503  
 
 504  
     /**
 505  
      * Pop cursor to stack and replace current cursor with it.
 506  
      * @return This object
 507  
      * @since 0.16
 508  
      */
 509  
     public Directives pop() {
 510  2
         this.all.add(new PopDirective());
 511  2
         return this;
 512  
     }
 513  
 
 514  
     /**
 515  
      * Set CDATA section.
 516  
      *
 517  
      * <p>If a value provided contains illegal XML characters, a runtime
 518  
      * exception will be thrown. To avoid this, it is recommended to use
 519  
      * {@link Xembler#escape(String)}.
 520  
      *
 521  
      * @param text Text to set
 522  
      * @return This object
 523  
      * @since 0.17
 524  
      */
 525  
     public Directives cdata(final Object text) {
 526  
         try {
 527  1
             this.all.add(new CdataDirective(text.toString()));
 528  0
         } catch (final XmlContentException ex) {
 529  0
             throw new IllegalArgumentException(
 530  0
                 String.format(
 531  
                     "failed to understand XML content, CDATA(%s)",
 532  
                     text
 533  
                 ),
 534  
                 ex
 535  
             );
 536  1
         }
 537  1
         return this;
 538  
     }
 539  
 
 540  
     /**
 541  
      * Parse script.
 542  
      * @param script Script to parse
 543  
      * @return Collection of directives
 544  
      * @throws SyntaxException If can't parse
 545  
      */
 546  
     private static Collection<Directive> parse(final String script)
 547  
         throws SyntaxException {
 548  29
         final CharStream input = new ANTLRStringStream(script);
 549  29
         final XemblyLexer lexer = new XemblyLexer(input);
 550  29
         final TokenStream tokens = new CommonTokenStream(lexer);
 551  29
         final XemblyParser parser = new XemblyParser(tokens);
 552  
         try {
 553  29
             return parser.directives();
 554  0
         } catch (final RecognitionException ex) {
 555  0
             throw new SyntaxException(script, ex);
 556  4
         } catch (final ParsingException ex) {
 557  4
             throw new SyntaxException(script, ex);
 558  
         }
 559  
     }
 560  
 
 561  
 }