Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
Directives |
|
| 2.909090909090909;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[@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("<root/>"); | |
167 | * Node node = parse("<user name='Jeffrey'/>"); | |
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 | * "<root><jeff name='Jeffrey'></root>" | |
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<String, Object>() | |
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 | } |