View Javadoc

1   /*
2    * Copyright 2010 - org.tinyjee.maven
3    *
4    *    Licensed under the Apache License, Version 2.0 (the "License");
5    *    you may not use this file except in compliance with the License.
6    *    You may obtain a copy of the License at
7    *
8    *        http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *    Unless required by applicable law or agreed to in writing, software
11   *    distributed under the License is distributed on an "AS IS" BASIS,
12   *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *    See the License for the specific language governing permissions and
14   *    limitations under the License.
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   * XmlLoader is a 'source-class' compatible helper class that can load and transform XML/JSON content and provide XPath and DOM
55   * oriented access to the document elements when used in combination with a velocity template.
56   * <p/>
57   * The loader supports remote URLs (e.g. REST-services) and local paths to get the XML (or JSON) content (similar to the standard
58   * inclusion engine).
59   * <p/>
60   * Use this loader when you want to access the parsed content of XML (or JSON) markup using the DOM API with the option to
61   * apply optional XSL transformations. This extension is not needed if your aim is to include code snippets that are
62   * selected using XPath expressions.
63   * <p/>
64   * <b>Usage:</b><div><code>
65   * %{include|source=template.vm|source-class=org.tinyjee.maven.dim.extensions.XmlLoader|xml=file.xml}
66   * </code></div>
67   * or when using the alias:<div><code>
68   * %{include|source=template.vm|source-xml=file.xml}
69   * </code></div>
70   * include XML after XSL transformation:<div><code>
71   * %{include|source-xml=file.xml|xsl=transform-to-html.xsl}
72   * </code></div>
73   *
74   * @author Juergen_Kellerer, 2011-10-06
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  	 * Implements the "{@link org.tinyjee.maven.dim.extensions.XmlLoader#PARAM_ALIAS}" alias functionality.
83  	 */
84  	public static class AliasHandler extends AbstractAliasHandler {
85  		/**
86  		 * Constructs the handler (Note: Is called by {@link org.tinyjee.maven.dim.spi.RequestParameterTransformer#TRANSFORMERS}).
87  		 */
88  		public AliasHandler() {
89  			super(PARAM_ALIAS, PARAM_XML, XmlLoader.class.getName());
90  		}
91  	}
92  
93  	/**
94  	 * Defines a shortcut for the request properties 'source-class' and 'xml'.
95  	 * <p/>
96  	 * If this parameter is present inside the request parameters list, the system will effectively behave as if
97  	 * 'source-class' and 'xml' where set separately.
98  	 */
99  	public static final String PARAM_ALIAS = "source-xml";
100 
101 	/**
102 	 * Sets the URL or local path to the XML file to load inside the request parameters.
103 	 * <p/>
104 	 * The specified file is resolved in exactly the same way as when using the parameter "<code>source</code>"
105 	 * (see Macro reference).
106 	 * <p/>
107 	 * <i>Note:</i> When no further source is specified, the xml loader sets the serialized representation of
108 	 * this file as the content that is included by the macro. If a source IS specified, the defined out parameters
109 	 * are set and allow DOM or XPath based access to the document structure.
110 	 */
111 	public static final String PARAM_XML = "xml";
112 
113 	/**
114 	 * Sets a fully qualified class name to a class that either provides a {@link Document} instance, a {@link javax.xml.transform.Source}
115 	 * or is annotated with {@link javax.xml.bind.annotation.XmlRootElement}.
116 	 * <p/>
117 	 * The specified class is resolved in exactly the same way as when using the parameter "{@code source-class}" (see Macro reference).
118 	 * <p/>
119 	 * In addition to fully qualified class names, this parameter supports also factory methods that create the classes:
120 	 * <ul>
121 	 * <li>my.package.MyClass</li>
122 	 * <li>my.package.MyClass.getInstance()</li>
123 	 * <li>my.package.MyClassFactory.getMyClass()</li>
124 	 * <li>my.package.MyClassFactory.getInstance().getMyClass()</li>
125 	 * </ul>
126 	 * <p/>
127 	 * Source class is implemented as a convenience parameter to "{@code xml=class:}", thus using the
128 	 * {@code xml=class:my.pa..} is exactly the same as using {@code xml-class=my.pa..}. Effectively this means that
129 	 * if this loader is used with the alias syntax, xml classes can be used via {@code source-xml=class:my.package.MyClass}.
130 	 */
131 	public static final String PARAM_XML_CLASS = "xml-class";
132 
133 	/**
134 	 * <i>Experimental:</i> Sets the URL or local path to JSON encoded content that is translated to XML and included.
135 	 * <p/>
136 	 * JSON to XML transcoding is currently experimental and is primarily meant to be used for XSL or XPath matching on
137 	 * a JSON source (e.g. from a REST web service).
138 	 * See {@link PositioningJsonDocumentBuilder} for details on how JSON is mapped to XML.
139 	 */
140 	public static final String PARAM_JSON = "json";
141 
142 	/**
143 	 * <b>Optional parameter</b>, sets the path to an XSL file that is used to transform the given input.
144 	 * <p/>
145 	 * The specified file is resolved in exactly the same way as when using the parameter "<code>source</code>"
146 	 * (see Macro reference).
147 	 * <p/>
148 	 * When a transformation is performed, all given macro call parameters are passed to the XSL processor making them
149 	 * available as xsl params when using global definitions like:<br/>
150 	 * <code>&lt;xsl:param name="my-macro-param" select="'default-value'"&gt;</code>
151 	 */
152 	public static final String PARAM_XSL = "xsl";
153 
154 	/**
155 	 * <b>Optional parameter</b>, sets a relative or absolute URL to an XSL file that is added as standard template
156 	 * to the document being included but it is not executed.
157 	 * <p/>
158 	 * This is useful to place <code>&lt;?xml-stylesheet ...</code> declarations in included content.
159 	 */
160 	public static final String PARAM_ADD_XSL = "add-xsl";
161 
162 	/**
163 	 * <b>Optional parameter</b>, enables the inclusion of the XML declaration (<code>&lt;?xml...</code>) if set to "true".
164 	 */
165 	public static final String PARAM_ADD_DECLARATION = "add-declaration";
166 
167 	/**
168 	 * Enum parameter that specifies whether XML schemata are loaded in order to include them with the XML content.
169 	 * (Default: false)
170 	 * Possible values for this parameter are: (true|false|local-only)
171 	 * <p/>
172 	 * Note: Enabling this option does not enable schema validation, it enables loading schema content into the
173 	 * output parameter {@code schemaMap}.
174 	 */
175 	public static final String PARAM_LOAD_SCHEMA = "load-schema";
176 
177 	/**
178 	 * <b>Optional parameter</b>, toggles whether the XML is indented before inclusion.
179 	 */
180 	public static final String PARAM_INDENT = "indent";
181 
182 	/**
183 	 * <b>Optional parameter</b>, toggles whether the XML parser handles namespaces.
184 	 * <p/>
185 	 * The default value for this option is 'true' when an XML transformation is specified (= <code>xsl</code> is set).
186 	 * Without a xml transformation namespace awareness is turned off by default to simplify xpath based matching.
187 	 */
188 	public static final String PARAM_NAMESPACE_AWARE = "namespace-aware";
189 
190 	/**
191 	 * Is set with the parsed document object ({@link Document}) and can be accessed from velocity templates.
192 	 */
193 	public static final String OUT_PARAM_DOCUMENT = "document";
194 
195 	/**
196 	 * Is set with the original parsed document as it existed before applying any transformations.
197 	 * When <code>xsl</code> is not set, <code>originalDocument</code> and <code>document</code> are the same.
198 	 */
199 	public static final String OUT_PARAM_ORIGINAL_DOCUMENT = "originalDocument";
200 
201 	/**
202 	 * If filled with a map of all loaded XML schemata (in case of schema loading is enabled).
203 	 * <p/>
204 	 * The map contains entries of the format:<code><pre>[
205 	 *     "schema-system-id": {@link Document},
206 	 *     ...
207 	 * ]</pre></code>
208 	 * <p/>
209 	 * <i>Note:</i> Schema loading works with any input type that defines a schema (including schema generation on JAXB classes).
210 	 * <p/>
211 	 * <i>Tipp:</i> Schema content can either be included in the site, parts can be included via XPath matching or finally schema
212 	 * files can also be linked using: <code><pre>
213 	 * Referenced XML schemata:&lt;ul&gt;
214 	 * #foreach($entry in $schemaMap.entrySet())
215 	 *    #if($entry.key.startWith("http"))
216 	 *        #set($href=$entry.key)
217 	 *    #else
218 	 *        #set($href=$globals.attachContent($entry.key, $xpath.serialize($entry.value)))
219 	 *    #end
220 	 *    &lt;li&gt;&lt;a href="$href"&gt;$href&lt;/a&gt;&lt;/li&gt;
221 	 * #end &lt;/ul&gt;
222 	 * </pre></code>
223 	 */
224 	public static final String OUT_PARAM_SCHEMA_MAP = "schemaMap";
225 
226 	/**
227 	 * Is set with an implementation of "{@link org.tinyjee.maven.dim.utils.XPathEvaluator}" that is configured to match xpath expressions
228 	 * against DOM tree of {@code document}.
229 	 * <p/>
230 	 * Usage (from within a velocity template):<br/>
231 	 * <code>Title of <b>$document.documentURI</b> is: <b>$xpath.findText("/html/head/title")</b></code>
232 	 * <p/>
233 	 * <b>Note:</b> When <code>namespace-aware</code> is set to true, elements are matched using namespace prefixes.
234 	 * This is true also for the default namespace configured via <code>xmlns="http://some/namespace"</code>. The xpath evaluator
235 	 * assigns the prefix "<code>default</code>" for the default namespace in order to make it accessible for an xpath evaluation.
236 	 * <br/>
237 	 * E.g. the XHTML example from above changes to <code>$xpath.findText("/default:html/default:head/default:title")</code>.
238 	 * As a consequence, other namespace URIs that use a prefix of "default" will be ignored and are therefore unsupported.
239 	 */
240 	public static final String OUT_PARAM_XPATH = "xpath";
241 
242 	/**
243 	 * Is the same as "<code>xpath</code>" but configured to match on the original document.
244 	 */
245 	public static final String OUT_PARAM_ORIGINAL_XPATH = "originalXpath";
246 
247 	final transient TransformerFactory transformerFactory = TransformerFactory.newInstance();
248 
249 	/**
250 	 * Constructs a new XML loader.
251 	 *
252 	 * @param baseDir       the base dir to of the maven module.
253 	 * @param requestParams the request params of the macro call.
254 	 * @throws Exception In case of loading or transforming fails.
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 }