1
2
3
4
5 package com.jcabi.xml;
6
7 import com.jcabi.log.Logger;
8 import java.io.ByteArrayOutputStream;
9 import java.io.File;
10 import java.io.FileNotFoundException;
11 import java.io.IOException;
12 import java.io.InputStream;
13 import java.io.StringReader;
14 import java.io.UnsupportedEncodingException;
15 import java.net.URI;
16 import java.net.URL;
17 import java.nio.charset.StandardCharsets;
18 import java.nio.file.Path;
19 import java.util.HashMap;
20 import java.util.Map;
21 import javax.xml.parsers.DocumentBuilder;
22 import javax.xml.parsers.DocumentBuilderFactory;
23 import javax.xml.parsers.ParserConfigurationException;
24 import javax.xml.transform.Result;
25 import javax.xml.transform.Templates;
26 import javax.xml.transform.Transformer;
27 import javax.xml.transform.TransformerConfigurationException;
28 import javax.xml.transform.TransformerException;
29 import javax.xml.transform.TransformerFactory;
30 import javax.xml.transform.dom.DOMResult;
31 import javax.xml.transform.dom.DOMSource;
32 import javax.xml.transform.stream.StreamResult;
33 import javax.xml.transform.stream.StreamSource;
34 import lombok.EqualsAndHashCode;
35 import org.cactoos.map.MapEntry;
36 import org.cactoos.map.MapOf;
37 import org.cactoos.scalar.Sticky;
38 import org.cactoos.scalar.Synced;
39 import org.cactoos.scalar.Unchecked;
40 import org.w3c.dom.Document;
41
42
43
44
45
46
47
48
49
50
51
52 @EqualsAndHashCode(of = "xsl")
53 @SuppressWarnings("PMD.TooManyMethods")
54 public final class XSLDocument implements XSL {
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81 public static final XSL STRIP = XSLDocument.make(
82 XSL.class.getResourceAsStream("strip.xsl")
83 );
84
85
86
87
88
89
90
91 private static final ThreadLocal<DocumentBuilder> DBUILDER =
92 ThreadLocal.withInitial(
93 () -> {
94 try {
95 return DocumentBuilderFactory.newInstance().newDocumentBuilder();
96 } catch (final ParserConfigurationException ex) {
97 throw new IllegalStateException(
98 "Failed to create DocumentBuilder", ex
99 );
100 }
101 }
102 );
103
104
105
106
107 private final transient String xsl;
108
109
110
111
112 private final transient Sources sources;
113
114
115
116
117 private final transient Map<String, Object> params;
118
119
120
121
122
123 private final transient String sid;
124
125
126
127
128 private final transient Unchecked<Templates> templates;
129
130
131
132
133
134 private final transient Unchecked<String> formatted;
135
136
137
138
139
140 public XSLDocument(final XML src) {
141 this(src, "/");
142 }
143
144
145
146
147
148
149
150 public XSLDocument(final XML src, final String base) {
151 this(src.toString(), base);
152 }
153
154
155
156
157
158
159
160 public XSLDocument(final URL url) throws IOException {
161 this(url, url.toString());
162 }
163
164
165
166
167
168
169
170
171 public XSLDocument(final URL url, final String base) throws IOException {
172 this(new TextResource(url).toString(), base);
173 }
174
175
176
177
178
179
180
181 public XSLDocument(final File file) throws IOException {
182 this(file, file.getAbsolutePath());
183 }
184
185
186
187
188
189
190
191 public XSLDocument(final File file, final String base) throws IOException {
192 this(new TextResource(file).toString(), base);
193 }
194
195
196
197
198
199
200
201 public XSLDocument(final Path file) throws IOException {
202 this(file.toFile());
203 }
204
205
206
207
208
209
210
211
212 public XSLDocument(final Path file, final String base)
213 throws IOException {
214 this(file.toFile(), base);
215 }
216
217
218
219
220
221
222
223 public XSLDocument(final URI uri) throws IOException {
224 this(uri, uri.toString());
225 }
226
227
228
229
230
231
232
233
234 public XSLDocument(final URI uri, final String base) throws IOException {
235 this(new TextResource(uri).toString(), base);
236 }
237
238
239
240
241
242 public XSLDocument(final InputStream stream) {
243 this(new TextResource(stream).toString());
244 }
245
246
247
248
249
250
251
252 public XSLDocument(final InputStream stream, final String base) {
253 this(new TextResource(stream).toString(), base);
254 }
255
256
257
258
259
260 public XSLDocument(final String src) {
261 this(src, Sources.DUMMY);
262 }
263
264
265
266
267
268
269
270 public XSLDocument(final String src, final String base) {
271 this(src, Sources.DUMMY, base);
272 }
273
274
275
276
277
278
279
280 public XSLDocument(final String src, final Sources srcs) {
281 this(src, srcs, new HashMap<>(0));
282 }
283
284
285
286
287
288
289
290
291 public XSLDocument(final String src, final Sources srcs,
292 final String base) {
293 this(src, srcs, new HashMap<>(0), base);
294 }
295
296
297
298
299
300
301
302
303 public XSLDocument(final String src, final Sources srcs,
304 final Map<String, Object> map) {
305 this(src, srcs, map, "/");
306 }
307
308
309
310
311
312
313
314
315
316
317 public XSLDocument(final String src, final Sources srcs,
318 final Map<String, Object> map, final String base) {
319 this(
320 src, srcs, new HashMap<>(map), base,
321 XSLDocument.load(srcs, src, base),
322 XSLDocument.format(src)
323 );
324 }
325
326
327
328
329
330
331
332
333
334
335
336 private XSLDocument(final String src, final Sources srcs,
337 final Map<String, Object> map, final String base,
338 final Unchecked<Templates> tmpl, final Unchecked<String> fmt) {
339 this.xsl = src;
340 this.sources = srcs;
341 this.params = new HashMap<>(map);
342 this.sid = base;
343 this.templates = tmpl;
344 this.formatted = fmt;
345 }
346
347 @Override
348 public XSL with(final Sources src) {
349 return new XSLDocument(this.xsl, src, this.params, this.sid);
350 }
351
352 @Override
353 public XSL with(final String name, final Object value) {
354 return new XSLDocument(
355 this.xsl,
356 this.sources,
357 new MapOf<String, Object>(this.params, new MapEntry<>(name, value)),
358 this.sid,
359 this.templates,
360 this.formatted
361 );
362 }
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380 @SuppressWarnings("PMD.ProhibitPublicStaticMethods")
381 public static XSL make(final InputStream stream) {
382 return new XSLDocument(stream);
383 }
384
385
386
387
388
389
390
391
392 @SuppressWarnings("PMD.ProhibitPublicStaticMethods")
393 public static XSL make(final URL url) {
394 try {
395 return new XSLDocument(url, url.toString());
396 } catch (final IOException ex) {
397 throw new IllegalStateException(
398 String.format(
399 "Failed to read from URL '%s'",
400 url
401 ),
402 ex
403 );
404 }
405 }
406
407 @Override
408 public String toString() {
409 return this.formatted.value();
410 }
411
412 @Override
413 public XML transform(final XML xml) {
414 final Document target = XSLDocument.DBUILDER.get().newDocument();
415 this.transformInto(xml, new DOMResult(target));
416 return new XMLDocument(target);
417 }
418
419 @Override
420 public String applyTo(final XML xml) {
421 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
422 this.transformInto(xml, new StreamResult(baos));
423 try {
424 return baos.toString(StandardCharsets.UTF_8.name());
425 } catch (final UnsupportedEncodingException ex) {
426 throw new IllegalArgumentException(
427 "Failed to convert bytes into UTF-8 string",
428 ex
429 );
430 }
431 }
432
433
434
435
436
437
438
439
440 @SuppressWarnings("PMD.UnnecessaryLocalRule")
441 private void transformInto(final XML xml, final Result result) {
442 final Transformer trans = this.transformer();
443 final ConsoleErrorListener errors = new ConsoleErrorListener();
444 trans.setErrorListener(errors);
445 final long start = System.nanoTime();
446 try {
447 trans.transform(new DOMSource(xml.inner()), result);
448 } catch (final TransformerException ex) {
449 final StringBuilder summary = new StringBuilder(
450 String.join("; ", errors.summary())
451 );
452 if (!summary.toString().equals(ex.getMessageAndLocation())) {
453 summary.append("; ").append(ex.getMessageAndLocation());
454 }
455 throw new IllegalArgumentException(
456 String.format(
457 "Failed to transform by %s: %s",
458 trans.getClass().getName(),
459 summary
460 ),
461 ex
462 );
463 }
464 if (Logger.isTraceEnabled(this)) {
465 Logger.trace(
466 this,
467 "%s transformed XML in %[nano]s",
468 trans.getClass().getName(),
469 System.nanoTime() - start
470 );
471 }
472 }
473
474
475
476
477
478 private Transformer transformer() {
479 final Templates templ = this.templates.value();
480 final Transformer trans;
481 try {
482 trans = templ.newTransformer();
483 } catch (final TransformerConfigurationException ex) {
484 throw new IllegalArgumentException(
485 String.format(
486 "Failed to create transformer by %s",
487 templ.getClass().getName()
488 ),
489 ex
490 );
491 }
492 trans.setURIResolver(this.sources);
493 for (final Map.Entry<String, Object> ent : this.params.entrySet()) {
494 trans.setParameter(ent.getKey(), ent.getValue());
495 }
496 return trans;
497 }
498
499
500
501
502
503
504 private static Unchecked<String> format(final String xsl) {
505 return new Unchecked<>(
506 new Synced<>(new Sticky<>(() -> new XMLDocument(xsl).toString()))
507 );
508 }
509
510
511
512
513
514
515
516
517 private static Unchecked<Templates> load(
518 final Sources sources,
519 final String xsl,
520 final String sid
521 ) {
522 return new Unchecked<>(
523 new Synced<>(new Sticky<>(() -> XSLDocument.doLoad(sources, xsl, sid)))
524 );
525 }
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541 private static Templates doLoad(
542 final Sources sources,
543 final String xsl,
544 final String sid
545 ) {
546 final TransformerFactory factory = TransformerFactory.newInstance();
547 final ConsoleErrorListener errors = new ConsoleErrorListener();
548 factory.setErrorListener(errors);
549 factory.setURIResolver(sources);
550 final Templates tmpl;
551 try {
552 tmpl = factory.newTemplates(
553 new StreamSource(new StringReader(xsl), sid)
554 );
555 } catch (final TransformerConfigurationException ex) {
556 throw new IllegalArgumentException(
557 String.format(
558 "Failed to create transformer by %s",
559 factory.getClass().getName()
560 ),
561 ex
562 );
563 }
564 if (!errors.summary().isEmpty()) {
565 throw new IllegalArgumentException(
566 String.format(
567 "Failed to compile stylesheet by %s: %s",
568 factory.getClass().getName(),
569 errors.summary()
570 )
571 );
572 }
573 return tmpl;
574 }
575 }