1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.tinyjee.maven.dim.extensions;
18
19 import org.apache.maven.doxia.logging.Log;
20 import org.tinyjee.maven.dim.spi.Globals;
21 import org.tinyjee.maven.dim.utils.*;
22 import org.w3c.dom.Document;
23 import org.w3c.dom.NamedNodeMap;
24 import org.w3c.dom.Node;
25 import org.w3c.dom.ProcessingInstruction;
26
27 import javax.xml.transform.OutputKeys;
28 import javax.xml.transform.Transformer;
29 import javax.xml.transform.TransformerException;
30 import javax.xml.transform.TransformerFactory;
31 import javax.xml.transform.dom.DOMResult;
32 import javax.xml.transform.dom.DOMSource;
33 import javax.xml.transform.stream.StreamResult;
34 import javax.xml.transform.stream.StreamSource;
35 import java.beans.XMLEncoder;
36 import java.io.ByteArrayInputStream;
37 import java.io.ByteArrayOutputStream;
38 import java.io.File;
39 import java.lang.reflect.Method;
40 import java.lang.reflect.Modifier;
41 import java.net.URI;
42 import java.net.URL;
43 import java.util.*;
44
45 import static java.lang.Boolean.parseBoolean;
46 import static java.lang.String.valueOf;
47 import static java.util.Locale.ENGLISH;
48 import static org.tinyjee.maven.dim.IncludeMacroSignature.*;
49 import static org.tinyjee.maven.dim.spi.ResourceResolver.findSource;
50 import static org.tinyjee.maven.dim.spi.ResourceResolver.resolveClass;
51 import static org.tinyjee.maven.dim.utils.XPathEvaluatorImplementation.serializeNode;
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76 public class XmlLoader extends HashMap<String, Object> {
77
78 private static final long serialVersionUID = -3702740619423786753L;
79 private static final String LOCAL_ONLY = "local-only";
80
81
82
83
84 public static class AliasHandler extends AbstractAliasHandler {
85
86
87
88 public AliasHandler() {
89 super(PARAM_ALIAS, PARAM_XML, XmlLoader.class.getName());
90 }
91 }
92
93
94
95
96
97
98
99 public static final String PARAM_ALIAS = "source-xml";
100
101
102
103
104
105
106
107
108
109
110
111 public static final String PARAM_XML = "xml";
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131 public static final String PARAM_XML_CLASS = "xml-class";
132
133
134
135
136
137
138
139
140 public static final String PARAM_JSON = "json";
141
142
143
144
145
146
147
148
149
150
151
152 public static final String PARAM_XSL = "xsl";
153
154
155
156
157
158
159
160 public static final String PARAM_ADD_XSL = "add-xsl";
161
162
163
164
165 public static final String PARAM_ADD_DECLARATION = "add-declaration";
166
167
168
169
170
171
172
173
174
175 public static final String PARAM_LOAD_SCHEMA = "load-schema";
176
177
178
179
180 public static final String PARAM_INDENT = "indent";
181
182
183
184
185
186
187
188 public static final String PARAM_NAMESPACE_AWARE = "namespace-aware";
189
190
191
192
193 public static final String OUT_PARAM_DOCUMENT = "document";
194
195
196
197
198
199 public static final String OUT_PARAM_ORIGINAL_DOCUMENT = "originalDocument";
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224 public static final String OUT_PARAM_SCHEMA_MAP = "schemaMap";
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240 public static final String OUT_PARAM_XPATH = "xpath";
241
242
243
244
245 public static final String OUT_PARAM_ORIGINAL_XPATH = "originalXpath";
246
247 final transient TransformerFactory transformerFactory = TransformerFactory.newInstance();
248
249
250
251
252
253
254
255
256 public XmlLoader(File baseDir, Map<String, Object> requestParams) throws Exception {
257 final Log log = Globals.getLog();
258 final boolean debug = log.isDebugEnabled();
259
260 putAll(requestParams);
261
262 if (containsKey(PARAM_XML_CLASS)) put(PARAM_XML, "class:" + get(PARAM_XML_CLASS));
263
264 String sourcePath = (String) get(containsKey(PARAM_JSON) ? PARAM_JSON : PARAM_XML);
265 Object rawNamespaceAware = containsKey(PARAM_NAMESPACE_AWARE) ? get(PARAM_NAMESPACE_AWARE) : containsKey(PARAM_XSL);
266 final boolean namespaceAware = parseBoolean(valueOf(rawNamespaceAware));
267
268 final Map<String, Document> schemaMap = containsKey(PARAM_LOAD_SCHEMA) &&
269 !"false".equalsIgnoreCase(valueOf(get(PARAM_LOAD_SCHEMA))) ? new LinkedHashMap<String, Document>() : null;
270
271 Document document = sourcePath.startsWith("class:") ?
272 buildDocument(baseDir, sourcePath.substring(6), schemaMap) :
273 readDocument(baseDir, sourcePath, containsKey(PARAM_JSON) || sourcePath.endsWith(".json"), namespaceAware);
274 Document originalDocument = document;
275 put("document-uri", document.getDocumentURI());
276
277 if (containsKey(PARAM_XSL))
278 document = transformDocument(baseDir, document);
279
280 if (containsKey(PARAM_ADD_XSL)) {
281 ProcessingInstruction pi = document.createProcessingInstruction(
282 "xml-stylesheet", "type=\"text/xsl\" href=\"" + get(PARAM_ADD_XSL) + '"');
283 document.insertBefore(pi, document.getDocumentElement());
284 }
285
286 put(OUT_PARAM_DOCUMENT, document);
287 XPathEvaluatorImplementation documentXpathEvaluator = new XPathEvaluatorImplementation(document);
288 put(OUT_PARAM_XPATH, documentXpathEvaluator);
289 put(OUT_PARAM_ORIGINAL_DOCUMENT, originalDocument);
290 put(OUT_PARAM_ORIGINAL_XPATH, new XPathEvaluatorImplementation(originalDocument));
291
292 put(OUT_PARAM_SCHEMA_MAP, schemaMap);
293 if (schemaMap != null) {
294 if (debug) log.debug("About to load all referenced xml schema documents.");
295 loadSchema(document, schemaMap);
296 }
297
298 if (debug) {
299 if (document != originalDocument) {
300 log.debug("Original Document:\n" + serializeNode(originalDocument, true, true));
301 log.debug("Transformed Document:\n" + serializeNode(document, true, true));
302 } else
303 log.debug("Document:\n" + serializeNode(document, true, true));
304 }
305
306 if (!containsKey(PARAM_SOURCE)) {
307 if (!containsKey(PARAM_HIGHLIGHT_TYPE)) put(PARAM_HIGHLIGHT_TYPE, "xml");
308 if (!containsKey(PARAM_SOURCE_CONTENT_TYPE)) put(PARAM_SOURCE_CONTENT_TYPE, "xml");
309
310 if (debug) {
311 log.debug("Did not find any 'source', assuming the loaded XML document is the content to include. " +
312 "(" + PARAM_VERBATIM + '=' + get(PARAM_VERBATIM) + ", " +
313 PARAM_HIGHLIGHT_TYPE + '=' + get(PARAM_HIGHLIGHT_TYPE) + ", " +
314 PARAM_SOURCE_CONTENT_TYPE + '=' + get(PARAM_SOURCE_CONTENT_TYPE) + ')');
315 }
316
317 put("content", serializeNode(document, !parseBoolean(valueOf(get(PARAM_ADD_DECLARATION))), true));
318 }
319 }
320
321 Document transformDocument(File baseDir, Document document) throws Exception {
322 final Log log = Globals.getLog();
323 final boolean debug = log.isDebugEnabled();
324 if (!containsKey(PARAM_VERBATIM)) put(PARAM_VERBATIM, false);
325
326 URL xslURL = findSource(baseDir, (String) get(PARAM_XSL));
327 if (debug) log.debug("About to transform previously loaded XML content using XSL " + xslURL);
328 Transformer transformer = transformerFactory.newTransformer(new StreamSource(xslURL.openStream(), xslURL.toString()));
329 try {
330 for (Map.Entry<String, Object> entry : entrySet()) {
331 if (entry.getValue() == null) continue;
332 transformer.setParameter(entry.getKey(), entry.getValue());
333 }
334 document = sourceToDocument(transformer, new DOMSource(document));
335 } catch (TransformerException e) {
336 log.error("Failed transforming document '" + document.getDocumentURI() + "' using XSL '" + xslURL + "'. " +
337 "Run with -X to see the content that failed.", e);
338 if (debug) log.debug("Document that failed:\n" + serializeNode(document, true, true));
339 throw e;
340 }
341 return beautifyDocument(document, false);
342 }
343
344 Document readDocument(File baseDir, String sourcePath, boolean sourceIsJson, boolean namespaceAware) throws Exception {
345 final Log log = Globals.getLog();
346 final URL xmlURL = findSource(baseDir, sourcePath);
347
348 if (log.isDebugEnabled()) {
349 log.debug("About to create XML content (namespaceAware=" + namespaceAware + ") from " +
350 (sourceIsJson ? "JSON" : "XML") + " encoded in " + xmlURL);
351 }
352
353 final AbstractPositioningDocumentBuilder documentBuilder = sourceIsJson ?
354 new PositioningJsonDocumentBuilder() :
355 new PositioningDocumentBuilder(namespaceAware);
356
357 return beautifyDocument(documentBuilder.parse(xmlURL), false);
358 }
359
360 Document buildDocument(File baseDir, String classExpression, Map<String, Document> schemaMap) throws Exception {
361 final Log log = Globals.getLog();
362 final boolean debug = log.isDebugEnabled();
363
364 final String className;
365 int braceIndex = classExpression.indexOf('(');
366 if (braceIndex != -1)
367 className = classExpression.substring(0, classExpression.lastIndexOf('.', braceIndex));
368 else
369 className = classExpression;
370
371 if (debug) log.debug("About to create XML content from class '" + className + '\'');
372
373 Object xmlInstance = null;
374 Class<?> xmlClass = null;
375 try {
376 xmlClass = resolveClass(baseDir, className);
377 if (className.equals(classExpression))
378 xmlInstance = xmlClass.newInstance();
379 else {
380 String[] methods = classExpression.substring(className.length() + 1).split("\\)\\.");
381 for (String methodName : methods) {
382 if (debug) log.debug("Attempting to look for public method '" + methodName + "\' in class '" + className + '\'');
383 Method method = xmlClass.getMethod(methodName.substring(0, methodName.indexOf('(')));
384 xmlInstance = method.invoke(Modifier.isStatic(method.getModifiers()) ? null : xmlInstance);
385 xmlClass = xmlInstance.getClass();
386 }
387 }
388 } catch (Exception e) {
389 throw new RuntimeException(e);
390 }
391
392 return instanceToDocument(xmlClass, xmlInstance, schemaMap);
393 }
394
395
396 void loadSchema(Document document, Map<String, Document> schemaMap) throws Exception {
397 final Log log = Globals.getLog();
398 final boolean debug = log.isDebugEnabled();
399 final boolean localOnly = LOCAL_ONLY.equalsIgnoreCase(valueOf(get(PARAM_LOAD_SCHEMA)));
400
401 for (Node node : new NodeListAdapter(document.getElementsByTagName("*"))) {
402 NamedNodeMap attributes = node.getAttributes();
403 if (attributes != null) {
404 List<String> systemIds = null;
405
406 Node schemaLocation = attributes.getNamedItem("xsi:schemaLocation");
407 if (schemaLocation == null)
408 schemaLocation = attributes.getNamedItemNS("http://www.w3.org/2001/XMLSchema-instance", "schemaLocation");
409 if (schemaLocation != null) {
410 String[] locations = schemaLocation.getNodeValue().split("\\s+");
411 if (locations.length % 2 != 0) {
412 throw new IllegalArgumentException("Cannot load xml schemata, the location(s) " + Arrays.toString(locations) +
413 " specified in element '<" + node.getNodeName() + "/>' are not formatted as expected, " +
414 " 'namespace systemId nextNamespace nextSystemId..'");
415 }
416 systemIds = new ArrayList<String>(locations.length / 2);
417 for (int i = 1; i < locations.length; i += 2) systemIds.add(locations[i]);
418 }
419
420 schemaLocation = attributes.getNamedItem("xsi:noNamespaceSchemaLocation");
421 if (schemaLocation != null) {
422 String[] locations = schemaLocation.getNodeValue().split("\\s+");
423 if (systemIds == null) systemIds = new ArrayList<String>(locations.length);
424 Collections.addAll(systemIds, locations);
425 }
426
427 if (systemIds == null)
428 continue;
429
430 final Transformer transformer = transformerFactory.newTransformer();
431 for (String systemId : systemIds) {
432 if (schemaMap.containsKey(systemId)) {
433 if (debug) log.debug("Not loading schema '" + systemId + "\' as it was already loaded.");
434 continue;
435 }
436
437 if (debug) log.debug("About to lookup schema '" + systemId + '\'');
438 String documentURI = document.getDocumentURI();
439 URI schemaUri = documentURI == null ? URI.create(systemId) : URI.create(documentURI).resolve(systemId);
440 if (localOnly && valueOf(schemaUri.getScheme()).toLowerCase(ENGLISH).startsWith("http")) continue;
441
442 systemId = schemaUri.toASCIIString();
443 if (debug) log.debug("Loading schema '" + systemId + '\'');
444 schemaMap.put(systemId, beautifyDocument(sourceToDocument(transformer, new StreamSource(systemId)), false));
445 }
446 }
447 }
448 }
449
450 boolean requiresBeautification() {
451 return parseBoolean(valueOf(get(PARAM_INDENT)));
452 }
453
454 Document beautifyDocument(Document document, boolean force) throws Exception {
455 final Log log = Globals.getLog();
456 final boolean debug = log.isDebugEnabled();
457 if (!force && !requiresBeautification()) {
458 if (debug) log.debug("XML beautification not required, returning original document " + document.getDocumentURI() + '.');
459 return document;
460 }
461
462 if (debug) log.debug("Beautifying document " + document.getDocumentURI() + '.');
463 final Transformer transformer = transformerFactory.newTransformer();
464 transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
465 transformer.setOutputProperty(OutputKeys.INDENT, "yes");
466 transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
467
468 ByteArrayOutputStream buffer = new ByteArrayOutputStream(64 * 1024);
469 transformer.transform(new DOMSource(document), new StreamResult(buffer));
470 return sourceToDocument(transformer, new StreamSource(new ByteArrayInputStream(buffer.toByteArray()), document.getDocumentURI()));
471 }
472
473 Document sourceToDocument(String systemId, javax.xml.transform.Source source) throws Exception {
474 source.setSystemId(systemId);
475 return sourceToDocument(transformerFactory.newTransformer(), source);
476 }
477
478 Document sourceToDocument(Transformer transformer, javax.xml.transform.Source source) throws Exception {
479 Document document;
480 DOMResult domResult = new DOMResult(null, source.getSystemId());
481 transformer.transform(source, domResult);
482 document = (Document) domResult.getNode();
483 document.setDocumentURI(source.getSystemId());
484 return document;
485 }
486
487 Document instanceToDocument(Class<?> xmlClass, Object xmlInstance, Map<String, Document> schemaMap) throws Exception {
488 final Log log = Globals.getLog();
489 final boolean debug = log.isDebugEnabled();
490
491 final String className = xmlClass.getName();
492 final Document document;
493 if (xmlInstance instanceof Document) {
494 if (debug) log.debug("Found that class is an instance of 'Document', using it directly");
495 document = (Document) xmlInstance;
496 } else if (xmlInstance instanceof javax.xml.transform.Source) {
497 if (debug) log.debug("Found that class is an instance of 'javax.xml.transform.Source', transforming it to a 'Document'.");
498 document = sourceToDocument(className, (javax.xml.transform.Source) xmlInstance);
499 } else if (xmlClass.isAnnotationPresent(javax.xml.bind.annotation.XmlRootElement.class)) {
500 if (debug) log.debug("Found that class is an annotated with 'XmlRootElement', transforming it to a 'Document'.");
501 @SuppressWarnings("unchecked")
502 JaxbXmlSerializer<Object> serializer = new JaxbXmlSerializer<Object>((Class<Object>) xmlClass);
503 document = serializer.serialize(xmlInstance, schemaMap != null);
504 if (schemaMap != null) {
505 for (Map.Entry<String, Document> entry : serializer.generateSchema().entrySet())
506 schemaMap.put(entry.getKey(), beautifyDocument(entry.getValue(), true));
507 }
508 } else {
509 if (debug) log.debug("Did not find any supported XML type, using Bean encoding to transform it to a 'Document'.");
510 ByteArrayOutputStream buffer = new ByteArrayOutputStream();
511 XMLEncoder encoder = new XMLEncoder(buffer);
512 encoder.writeObject(xmlInstance);
513 encoder.close();
514 document = sourceToDocument(className, new StreamSource(new ByteArrayInputStream(buffer.toByteArray())));
515 }
516
517 if (document.getDocumentURI() == null) {
518 final String documentURI = xmlClass.getSimpleName().replaceAll("(?!^)([A-Z]+)", "-$1").toLowerCase() + ".xml";
519 if (debug) log.debug("The generated document contained no document URI, using '" + documentURI + "' instead.");
520 document.setDocumentURI(documentURI);
521 }
522
523 return beautifyDocument(document, true);
524 }
525 }