1// =================================================================================================
2// ADOBE SYSTEMS INCORPORATED
3// Copyright 2006 Adobe Systems Incorporated
4// All Rights Reserved
5//
6// NOTICE:  Adobe permits you to use, modify, and distribute this file in accordance with the terms
7// of the Adobe license agreement accompanying it.
8// =================================================================================================
9
10package com.adobe.xmp.impl;
11
12import java.io.IOException;
13import java.io.OutputStream;
14import java.io.OutputStreamWriter;
15import java.util.Arrays;
16import java.util.HashSet;
17import java.util.Iterator;
18import java.util.Set;
19
20import com.adobe.xmp.XMPConst;
21import com.adobe.xmp.XMPError;
22import com.adobe.xmp.XMPException;
23import com.adobe.xmp.XMPMeta;
24import com.adobe.xmp.XMPMetaFactory;
25import com.adobe.xmp.options.SerializeOptions;
26
27
28/**
29 * Serializes the <code>XMPMeta</code>-object using the standard RDF serialization format.
30 * The output is written to an <code>OutputStream</code>
31 * according to the <code>SerializeOptions</code>.
32 *
33 * @since   11.07.2006
34 */
35public class XMPSerializerRDF
36{
37	/** default padding */
38	private static final int DEFAULT_PAD = 2048;
39	/** */
40	private static final String PACKET_HEADER  =
41		"<?xpacket begin=\"\uFEFF\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?>";
42	/** The w/r is missing inbetween */
43	private static final String PACKET_TRAILER = "<?xpacket end=\"";
44	/** */
45	private static final String PACKET_TRAILER2 = "\"?>";
46	/** */
47	private static final String RDF_XMPMETA_START =
48		"<x:xmpmeta xmlns:x=\"adobe:ns:meta/\" x:xmptk=\"";
49	/** */
50	private static final String RDF_XMPMETA_END   = "</x:xmpmeta>";
51	/** */
52	private static final String RDF_RDF_START =
53		"<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">";
54	/** */
55	private static final String RDF_RDF_END       = "</rdf:RDF>";
56
57	/** */
58	private static final String RDF_SCHEMA_START  = "<rdf:Description rdf:about=";
59	/** */
60	private static final String RDF_SCHEMA_END    = "</rdf:Description>";
61	/** */
62	private static final String RDF_STRUCT_START  = "<rdf:Description";
63	/** */
64	private static final String RDF_STRUCT_END    = "</rdf:Description>";
65	/** a set of all rdf attribute qualifier */
66	static final Set RDF_ATTR_QUALIFIER = new HashSet(Arrays.asList(new String[] {
67			XMPConst.XML_LANG, "rdf:resource", "rdf:ID", "rdf:bagID", "rdf:nodeID" }));
68
69	/** the metadata object to be serialized. */
70	private XMPMetaImpl xmp;
71	/** the output stream to serialize to */
72	private CountOutputStream outputStream;
73	/** this writer is used to do the actual serialisation */
74	private OutputStreamWriter writer;
75	/** the stored serialisation options */
76	private SerializeOptions options;
77	/** the size of one unicode char, for UTF-8 set to 1
78	 *  (Note: only valid for ASCII chars lower than 0x80),
79	 *  set to 2 in case of UTF-16 */
80	private int unicodeSize = 1; // UTF-8
81	/** the padding in the XMP Packet, or the length of the complete packet in
82	 *  case of option <em>exactPacketLength</em>. */
83	private int padding;
84
85
86	/**
87	 * The actual serialisation.
88	 *
89	 * @param xmp the metadata object to be serialized
90	 * @param out outputStream the output stream to serialize to
91	 * @param options the serialization options
92	 *
93	 * @throws XMPException If case of wrong options or any other serialisaton error.
94	 */
95	public void serialize(XMPMeta xmp, OutputStream out,
96			SerializeOptions options) throws XMPException
97	{
98		try
99		{
100			outputStream = new CountOutputStream(out);
101			writer = new OutputStreamWriter(outputStream, options.getEncoding());
102
103			this.xmp = (XMPMetaImpl) xmp;
104			this.options = options;
105			this.padding = options.getPadding();
106
107			writer = new OutputStreamWriter(outputStream, options.getEncoding());
108
109			checkOptionsConsistence();
110
111			// serializes the whole packet, but don't write the tail yet
112			// and flush to make sure that the written bytes are calculated correctly
113			String tailStr = serializeAsRDF();
114			writer.flush();
115
116			// adds padding
117			addPadding(tailStr.length());
118
119			// writes the tail
120			write(tailStr);
121			writer.flush();
122
123			outputStream.close();
124		}
125		catch (IOException e)
126		{
127			throw new XMPException("Error writing to the OutputStream", XMPError.UNKNOWN);
128		}
129	}
130
131
132	/**
133	 * Calulates the padding according to the options and write it to the stream.
134	 * @param tailLength the length of the tail string
135	 * @throws XMPException thrown if packet size is to small to fit the padding
136	 * @throws IOException forwards writer errors
137	 */
138	private void addPadding(int tailLength) throws XMPException, IOException
139	{
140		if (options.getExactPacketLength())
141		{
142			// the string length is equal to the length of the UTF-8 encoding
143			int minSize = outputStream.getBytesWritten() + tailLength * unicodeSize;
144			if (minSize > padding)
145			{
146				throw new XMPException("Can't fit into specified packet size",
147					XMPError.BADSERIALIZE);
148			}
149			padding -= minSize;	// Now the actual amount of padding to add.
150		}
151
152		// fix rest of the padding according to Unicode unit size.
153		padding /= unicodeSize;
154
155		int newlineLen = options.getNewline().length();
156		if (padding >= newlineLen)
157		{
158			padding -= newlineLen;	// Write this newline last.
159			while (padding >= (100 + newlineLen))
160			{
161				writeChars(100, ' ');
162				writeNewline();
163				padding -= (100 + newlineLen);
164			}
165			writeChars(padding, ' ');
166			writeNewline();
167		}
168		else
169		{
170			writeChars(padding, ' ');
171		}
172	}
173
174
175	/**
176	 * Checks if the supplied options are consistent.
177	 * @throws XMPException Thrown if options are conflicting
178	 */
179	protected void checkOptionsConsistence() throws XMPException
180	{
181		if (options.getEncodeUTF16BE() | options.getEncodeUTF16LE())
182		{
183			unicodeSize = 2;
184		}
185
186		if (options.getExactPacketLength())
187		{
188			if (options.getOmitPacketWrapper() | options.getIncludeThumbnailPad())
189			{
190				throw new XMPException("Inconsistent options for exact size serialize",
191						XMPError.BADOPTIONS);
192			}
193			if ((options.getPadding() & (unicodeSize - 1)) != 0)
194			{
195				throw new XMPException("Exact size must be a multiple of the Unicode element",
196						XMPError.BADOPTIONS);
197			}
198		}
199		else if (options.getReadOnlyPacket())
200		{
201			if (options.getOmitPacketWrapper() | options.getIncludeThumbnailPad())
202			{
203				throw new XMPException("Inconsistent options for read-only packet",
204						XMPError.BADOPTIONS);
205			}
206			padding = 0;
207		}
208		else if (options.getOmitPacketWrapper())
209		{
210			if (options.getIncludeThumbnailPad())
211			{
212				throw new XMPException("Inconsistent options for non-packet serialize",
213						XMPError.BADOPTIONS);
214			}
215			padding = 0;
216		}
217		else
218		{
219			if (padding == 0)
220			{
221				padding = DEFAULT_PAD * unicodeSize;
222			}
223
224			if (options.getIncludeThumbnailPad())
225			{
226				if (!xmp.doesPropertyExist(XMPConst.NS_XMP, "Thumbnails"))
227				{
228					padding += 10000 * unicodeSize;
229				}
230			}
231		}
232	}
233
234
235	/**
236	 * Writes the (optional) packet header and the outer rdf-tags.
237	 * @return Returns the packet end processing instraction to be written after the padding.
238	 * @throws IOException Forwarded writer exceptions.
239	 * @throws XMPException
240	 */
241	private String serializeAsRDF() throws IOException, XMPException
242	{
243		// Write the packet header PI.
244		if (!options.getOmitPacketWrapper())
245		{
246			writeIndent(0);
247			write(PACKET_HEADER);
248			writeNewline();
249		}
250
251		// Write the xmpmeta element's start tag.
252		writeIndent(0);
253		write(RDF_XMPMETA_START);
254		// Note: this flag can only be set by unit tests
255		if (!options.getOmitVersionAttribute())
256		{
257			write(XMPMetaFactory.getVersionInfo().getMessage());
258		}
259		write("\">");
260		writeNewline();
261
262		// Write the rdf:RDF start tag.
263		writeIndent(1);
264		write(RDF_RDF_START);
265		writeNewline();
266
267		// Write all of the properties.
268		if (options.getUseCompactFormat())
269		{
270			serializeCompactRDFSchemas();
271		}
272		else
273		{
274			serializePrettyRDFSchemas();
275		}
276
277		// Write the rdf:RDF end tag.
278		writeIndent(1);
279		write(RDF_RDF_END);
280		writeNewline();
281
282		// Write the xmpmeta end tag.
283		writeIndent(0);
284		write(RDF_XMPMETA_END);
285		writeNewline();
286
287		// Write the packet trailer PI into the tail string as UTF-8.
288		String tailStr = "";
289		if (!options.getOmitPacketWrapper())
290		{
291			for (int level = options.getBaseIndent(); level > 0; level--)
292			{
293				tailStr += options.getIndent();
294			}
295
296			tailStr += PACKET_TRAILER;
297			tailStr += options.getReadOnlyPacket() ? 'r' : 'w';
298			tailStr += PACKET_TRAILER2;
299		}
300
301		return tailStr;
302	}
303
304
305	/**
306	 * Serializes the metadata in pretty-printed manner.
307	 * @throws IOException Forwarded writer exceptions
308	 * @throws XMPException
309	 */
310	private void serializePrettyRDFSchemas() throws IOException, XMPException
311	{
312		if (xmp.getRoot().getChildrenLength() > 0)
313		{
314			for (Iterator it = xmp.getRoot().iterateChildren(); it.hasNext(); )
315			{
316				XMPNode currSchema = (XMPNode) it.next();
317				serializePrettyRDFSchema(currSchema);
318			}
319		}
320		else
321		{
322			writeIndent(2);
323			write(RDF_SCHEMA_START); // Special case an empty XMP object.
324			writeTreeName();
325			write("/>");
326			writeNewline();
327		}
328	}
329
330
331	/**
332	 * @throws IOException
333	 */
334	private void writeTreeName() throws IOException
335	{
336		write('"');
337		String name = xmp.getRoot().getName();
338		if (name != null)
339		{
340			appendNodeValue(name, true);
341		}
342		write('"');
343	}
344
345
346	/**
347	 * Serializes the metadata in compact manner.
348	 * @throws IOException Forwarded writer exceptions
349	 * @throws XMPException
350	 */
351	private void serializeCompactRDFSchemas() throws IOException, XMPException
352	{
353		// Begin the rdf:Description start tag.
354		writeIndent(2);
355		write(RDF_SCHEMA_START);
356		writeTreeName();
357
358		// Write all necessary xmlns attributes.
359		Set usedPrefixes = new HashSet();
360		usedPrefixes.add("xml");
361		usedPrefixes.add("rdf");
362
363		for (Iterator it = xmp.getRoot().iterateChildren(); it.hasNext();)
364		{
365			XMPNode schema = (XMPNode) it.next();
366			declareUsedNamespaces(schema, usedPrefixes, 4);
367		}
368
369		// Write the top level "attrProps" and close the rdf:Description start tag.
370		boolean allAreAttrs = true;
371		for (Iterator it = xmp.getRoot().iterateChildren(); it.hasNext();)
372		{
373			XMPNode schema = (XMPNode) it.next();
374			allAreAttrs &= serializeCompactRDFAttrProps (schema, 3);
375		}
376
377		if (!allAreAttrs)
378		{
379			write('>');
380			writeNewline();
381		}
382		else
383		{
384			write("/>");
385			writeNewline();
386			return;	// ! Done if all properties in all schema are written as attributes.
387		}
388
389		// Write the remaining properties for each schema.
390		for (Iterator it = xmp.getRoot().iterateChildren(); it.hasNext();)
391		{
392			XMPNode schema = (XMPNode) it.next();
393			serializeCompactRDFElementProps (schema, 3);
394		}
395
396		// Write the rdf:Description end tag.
397		writeIndent(2);
398		write(RDF_SCHEMA_END);
399		writeNewline();
400	}
401
402
403
404	/**
405	 * Write each of the parent's simple unqualified properties as an attribute. Returns true if all
406	 * of the properties are written as attributes.
407	 *
408	 * @param parentNode the parent property node
409	 * @param indent the current indent level
410	 * @return Returns true if all properties can be rendered as RDF attribute.
411	 * @throws IOException
412	 */
413	private boolean serializeCompactRDFAttrProps(XMPNode parentNode, int indent) throws IOException
414	{
415		boolean allAreAttrs = true;
416
417		for (Iterator it = parentNode.iterateChildren(); it.hasNext();)
418		{
419			XMPNode prop = (XMPNode) it.next();
420
421			if (canBeRDFAttrProp(prop))
422			{
423				writeNewline();
424				writeIndent(indent);
425				write(prop.getName());
426				write("=\"");
427				appendNodeValue(prop.getValue(), true);
428				write('"');
429			}
430			else
431			{
432				allAreAttrs = false;
433			}
434		}
435		return allAreAttrs;
436	}
437
438
439	/**
440	 * Recursively handles the "value" for a node that must be written as an RDF
441	 * property element. It does not matter if it is a top level property, a
442	 * field of a struct, or an item of an array. The indent is that for the
443	 * property element. The patterns bwlow ignore attribute qualifiers such as
444	 * xml:lang, they don't affect the output form.
445	 *
446	 * <blockquote>
447	 *
448	 * <pre>
449	 *  	&lt;ns:UnqualifiedStructProperty-1
450	 *  		... The fields as attributes, if all are simple and unqualified
451	 *  	/&gt;
452	 *
453	 *  	&lt;ns:UnqualifiedStructProperty-2 rdf:parseType=&quot;Resource&quot;&gt;
454	 *  		... The fields as elements, if none are simple and unqualified
455	 *  	&lt;/ns:UnqualifiedStructProperty-2&gt;
456	 *
457	 *  	&lt;ns:UnqualifiedStructProperty-3&gt;
458	 *  		&lt;rdf:Description
459	 *  			... The simple and unqualified fields as attributes
460	 *  		&gt;
461	 *  			... The compound or qualified fields as elements
462	 *  		&lt;/rdf:Description&gt;
463	 *  	&lt;/ns:UnqualifiedStructProperty-3&gt;
464	 *
465	 *  	&lt;ns:UnqualifiedArrayProperty&gt;
466	 *  		&lt;rdf:Bag&gt; or Seq or Alt
467	 *  			... Array items as rdf:li elements, same forms as top level properties
468	 *  		&lt;/rdf:Bag&gt;
469	 *  	&lt;/ns:UnqualifiedArrayProperty&gt;
470	 *
471	 *  	&lt;ns:QualifiedProperty rdf:parseType=&quot;Resource&quot;&gt;
472	 *  		&lt;rdf:value&gt; ... Property &quot;value&quot;
473	 *  			following the unqualified forms ... &lt;/rdf:value&gt;
474	 *  		... Qualifiers looking like named struct fields
475	 *  	&lt;/ns:QualifiedProperty&gt;
476	 * </pre>
477	 *
478	 * </blockquote>
479	 *
480	 * *** Consider numbered array items, but has compatibility problems. ***
481	 * Consider qualified form with rdf:Description and attributes.
482	 *
483	 * @param parentNode the parent node
484	 * @param indent the current indent level
485	 * @throws IOException Forwards writer exceptions
486	 * @throws XMPException If qualifier and element fields are mixed.
487	 */
488	private void serializeCompactRDFElementProps(XMPNode parentNode, int indent)
489			throws IOException, XMPException
490	{
491		for (Iterator it = parentNode.iterateChildren(); it.hasNext();)
492		{
493			XMPNode node = (XMPNode) it.next();
494			if (canBeRDFAttrProp (node))
495			{
496				continue;
497			}
498
499			boolean emitEndTag = true;
500			boolean indentEndTag = true;
501
502			// Determine the XML element name, write the name part of the start tag. Look over the
503			// qualifiers to decide on "normal" versus "rdf:value" form. Emit the attribute
504			// qualifiers at the same time.
505			String elemName = node.getName();
506			if (XMPConst.ARRAY_ITEM_NAME.equals(elemName))
507			{
508				elemName = "rdf:li";
509			}
510
511			writeIndent(indent);
512			write('<');
513			write(elemName);
514
515			boolean hasGeneralQualifiers = false;
516			boolean hasRDFResourceQual   = false;
517
518			for (Iterator iq = 	node.iterateQualifier(); iq.hasNext();)
519			{
520				XMPNode qualifier = (XMPNode) iq.next();
521				if (!RDF_ATTR_QUALIFIER.contains(qualifier.getName()))
522				{
523					hasGeneralQualifiers = true;
524				}
525				else
526				{
527					hasRDFResourceQual = "rdf:resource".equals(qualifier.getName());
528					write(' ');
529					write(qualifier.getName());
530					write("=\"");
531					appendNodeValue(qualifier.getValue(), true);
532					write('"');
533				}
534			}
535
536
537			// Process the property according to the standard patterns.
538			if (hasGeneralQualifiers)
539			{
540				serializeCompactRDFGeneralQualifier(indent, node);
541			}
542			else
543			{
544				// This node has only attribute qualifiers. Emit as a property element.
545				if (!node.getOptions().isCompositeProperty())
546				{
547					Object[] result = serializeCompactRDFSimpleProp(node);
548					emitEndTag = ((Boolean) result[0]).booleanValue();
549					indentEndTag = ((Boolean) result[1]).booleanValue();
550				}
551				else if (node.getOptions().isArray())
552				{
553					serializeCompactRDFArrayProp(node, indent);
554				}
555				else
556				{
557					emitEndTag = serializeCompactRDFStructProp(
558						node, indent, hasRDFResourceQual);
559				}
560
561			}
562
563			// Emit the property element end tag.
564			if (emitEndTag)
565			{
566				if (indentEndTag)
567				{
568					writeIndent(indent);
569				}
570				write("</");
571				write(elemName);
572				write('>');
573				writeNewline();
574			}
575
576		}
577	}
578
579
580	/**
581	 * Serializes a simple property.
582	 *
583	 * @param node an XMPNode
584	 * @return Returns an array containing the flags emitEndTag and indentEndTag.
585	 * @throws IOException Forwards the writer exceptions.
586	 */
587	private Object[] serializeCompactRDFSimpleProp(XMPNode node) throws IOException
588	{
589		// This is a simple property.
590		Boolean emitEndTag = Boolean.TRUE;
591		Boolean indentEndTag = Boolean.TRUE;
592
593		if (node.getOptions().isURI())
594		{
595			write(" rdf:resource=\"");
596			appendNodeValue(node.getValue(), true);
597			write("\"/>");
598			writeNewline();
599			emitEndTag = Boolean.FALSE;
600		}
601		else if (node.getValue() == null  ||  node.getValue().length() == 0)
602		{
603			write("/>");
604			writeNewline();
605			emitEndTag = Boolean.FALSE;
606		}
607		else
608		{
609			write('>');
610			appendNodeValue (node.getValue(), false);
611			indentEndTag = Boolean.FALSE;
612		}
613
614		return new Object[] {emitEndTag, indentEndTag};
615	}
616
617
618	/**
619	 * Serializes an array property.
620	 *
621	 * @param node an XMPNode
622	 * @param indent the current indent level
623	 * @throws IOException Forwards the writer exceptions.
624	 * @throws XMPException If qualifier and element fields are mixed.
625	 */
626	private void serializeCompactRDFArrayProp(XMPNode node, int indent) throws IOException,
627			XMPException
628	{
629		// This is an array.
630		write('>');
631		writeNewline();
632		emitRDFArrayTag (node, true, indent + 1);
633
634		if (node.getOptions().isArrayAltText())
635		{
636			XMPNodeUtils.normalizeLangArray (node);
637		}
638
639		serializeCompactRDFElementProps(node, indent + 2);
640
641		emitRDFArrayTag(node, false, indent + 1);
642	}
643
644
645	/**
646	 * Serializes a struct property.
647	 *
648	 * @param node an XMPNode
649	 * @param indent the current indent level
650	 * @param hasRDFResourceQual Flag if the element has resource qualifier
651	 * @return Returns true if an end flag shall be emitted.
652	 * @throws IOException Forwards the writer exceptions.
653	 * @throws XMPException If qualifier and element fields are mixed.
654	 */
655	private boolean serializeCompactRDFStructProp(XMPNode node, int indent,
656			boolean hasRDFResourceQual) throws XMPException, IOException
657	{
658		// This must be a struct.
659		boolean hasAttrFields = false;
660		boolean hasElemFields = false;
661		boolean emitEndTag = true;
662
663		for (Iterator ic = node.iterateChildren(); ic.hasNext(); )
664		{
665			XMPNode field = (XMPNode) ic.next();
666			if (canBeRDFAttrProp(field))
667			{
668				hasAttrFields = true;
669			}
670			else
671			{
672				hasElemFields = true;
673			}
674
675			if (hasAttrFields  &&  hasElemFields)
676			{
677				break;	// No sense looking further.
678			}
679		}
680
681		if (hasRDFResourceQual && hasElemFields)
682		{
683			throw new XMPException(
684					"Can't mix rdf:resource qualifier and element fields",
685					XMPError.BADRDF);
686		}
687
688		if (!node.hasChildren())
689		{
690			// Catch an empty struct as a special case. The case
691			// below would emit an empty
692			// XML element, which gets reparsed as a simple property
693			// with an empty value.
694			write(" rdf:parseType=\"Resource\"/>");
695			writeNewline();
696			emitEndTag = false;
697
698		}
699		else if (!hasElemFields)
700		{
701			// All fields can be attributes, use the
702			// emptyPropertyElt form.
703			serializeCompactRDFAttrProps(node, indent + 1);
704			write("/>");
705			writeNewline();
706			emitEndTag = false;
707
708		}
709		else if (!hasAttrFields)
710		{
711			// All fields must be elements, use the
712			// parseTypeResourcePropertyElt form.
713			write(" rdf:parseType=\"Resource\">");
714			writeNewline();
715			serializeCompactRDFElementProps(node, indent + 1);
716
717		}
718		else
719		{
720			// Have a mix of attributes and elements, use an inner rdf:Description.
721			write('>');
722			writeNewline();
723			writeIndent(indent + 1);
724			write(RDF_STRUCT_START);
725			serializeCompactRDFAttrProps(node, indent + 2);
726			write(">");
727			writeNewline();
728			serializeCompactRDFElementProps(node, indent + 1);
729			writeIndent(indent + 1);
730			write(RDF_STRUCT_END);
731			writeNewline();
732		}
733		return emitEndTag;
734	}
735
736
737	/**
738	 * Serializes the general qualifier.
739	 * @param node the root node of the subtree
740	 * @param indent the current indent level
741	 * @throws IOException Forwards all writer exceptions.
742	 * @throws XMPException If qualifier and element fields are mixed.
743	 */
744	private void serializeCompactRDFGeneralQualifier(int indent, XMPNode node)
745			throws IOException, XMPException
746	{
747		// The node has general qualifiers, ones that can't be
748		// attributes on a property element.
749		// Emit using the qualified property pseudo-struct form. The
750		// value is output by a call
751		// to SerializePrettyRDFProperty with emitAsRDFValue set.
752		write(" rdf:parseType=\"Resource\">");
753		writeNewline();
754
755		serializePrettyRDFProperty(node, true, indent + 1);
756
757		for (Iterator iq = 	node.iterateQualifier(); iq.hasNext();)
758		{
759			XMPNode qualifier = (XMPNode) iq.next();
760			serializePrettyRDFProperty(qualifier, false, indent + 1);
761		}
762	}
763
764
765	/**
766	 * Serializes one schema with all contained properties in pretty-printed
767	 * manner.<br>
768	 * Each schema's properties are written in a separate
769	 * rdf:Description element. All of the necessary namespaces are declared in
770	 * the rdf:Description element. The baseIndent is the base level for the
771	 * entire serialization, that of the x:xmpmeta element. An xml:lang
772	 * qualifier is written as an attribute of the property start tag, not by
773	 * itself forcing the qualified property form.
774	 *
775	 * <blockquote>
776	 *
777	 * <pre>
778	 *  	 &lt;rdf:Description rdf:about=&quot;TreeName&quot; xmlns:ns=&quot;URI&quot; ... &gt;
779	 *
780	 *  	 	... The actual properties of the schema, see SerializePrettyRDFProperty
781	 *
782	 *  	 	&lt;!-- ns1:Alias is aliased to ns2:Actual --&gt;  ... If alias comments are wanted
783	 *
784	 *  	 &lt;/rdf:Description&gt;
785	 * </pre>
786	 *
787	 * </blockquote>
788	 *
789	 * @param schemaNode a schema node
790	 * @throws IOException Forwarded writer exceptions
791	 * @throws XMPException
792	 */
793	private void serializePrettyRDFSchema(XMPNode schemaNode) throws IOException, XMPException
794	{
795		writeIndent(2);
796		write(RDF_SCHEMA_START);
797		writeTreeName();
798
799		Set usedPrefixes = new HashSet();
800		usedPrefixes.add("xml");
801		usedPrefixes.add("rdf");
802
803		declareUsedNamespaces(schemaNode, usedPrefixes, 4);
804
805		write('>');
806		writeNewline();
807
808		// Write each of the schema's actual properties.
809		for (Iterator it = schemaNode.iterateChildren(); it.hasNext();)
810		{
811			XMPNode propNode = (XMPNode) it.next();
812			serializePrettyRDFProperty(propNode, false, 3);
813		}
814
815		// Write the rdf:Description end tag.
816		writeIndent(2);
817		write(RDF_SCHEMA_END);
818		writeNewline();
819	}
820
821
822	/**
823	 * Writes all used namespaces of the subtree in node to the output.
824	 * The subtree is recursivly traversed.
825	 * @param node the root node of the subtree
826	 * @param usedPrefixes a set containing currently used prefixes
827	 * @param indent the current indent level
828	 * @throws IOException Forwards all writer exceptions.
829	 */
830	private void declareUsedNamespaces(XMPNode node, Set usedPrefixes, int indent)
831			throws IOException
832	{
833		if (node.getOptions().isSchemaNode())
834		{
835			// The schema node name is the URI, the value is the prefix.
836			String prefix = node.getValue().substring(0, node.getValue().length() - 1);
837			declareNamespace(prefix, node.getName(), usedPrefixes, indent);
838		}
839		else if (node.getOptions().isStruct())
840		{
841			for (Iterator it = node.iterateChildren(); it.hasNext();)
842			{
843				XMPNode field = (XMPNode) it.next();
844				declareNamespace(field.getName(), null, usedPrefixes, indent);
845			}
846		}
847
848		for (Iterator it = node.iterateChildren(); it.hasNext();)
849		{
850			XMPNode child = (XMPNode) it.next();
851			declareUsedNamespaces(child, usedPrefixes, indent);
852		}
853
854		for (Iterator it = node.iterateQualifier(); it.hasNext();)
855		{
856			XMPNode qualifier = (XMPNode) it.next();
857			declareNamespace(qualifier.getName(), null, usedPrefixes, indent);
858			declareUsedNamespaces(qualifier, usedPrefixes, indent);
859		}
860	}
861
862
863	/**
864	 * Writes one namespace declaration to the output.
865	 * @param prefix a namespace prefix (without colon) or a complete qname (when namespace == null)
866	 * @param namespace the a namespace
867	 * @param usedPrefixes a set containing currently used prefixes
868	 * @param indent the current indent level
869	 * @throws IOException Forwards all writer exceptions.
870	 */
871	private void declareNamespace(String prefix, String namespace, Set usedPrefixes, int indent)
872			throws IOException
873	{
874		if (namespace == null)
875		{
876			// prefix contains qname, extract prefix and lookup namespace with prefix
877			QName qname = new QName(prefix);
878			if (qname.hasPrefix())
879			{
880				prefix = qname.getPrefix();
881				// add colon for lookup
882				namespace = XMPMetaFactory.getSchemaRegistry().getNamespaceURI(prefix + ":");
883				// prefix w/o colon
884				declareNamespace(prefix, namespace, usedPrefixes, indent);
885			}
886			else
887			{
888				return;
889			}
890		}
891
892		if (!usedPrefixes.contains(prefix))
893		{
894			writeNewline();
895			writeIndent(indent);
896			write("xmlns:");
897			write(prefix);
898			write("=\"");
899			write(namespace);
900			write('"');
901			usedPrefixes.add(prefix);
902		}
903	}
904
905
906	/**
907	 * Recursively handles the "value" for a node. It does not matter if it is a
908	 * top level property, a field of a struct, or an item of an array. The
909	 * indent is that for the property element. An xml:lang qualifier is written
910	 * as an attribute of the property start tag, not by itself forcing the
911	 * qualified property form. The patterns below mostly ignore attribute
912	 * qualifiers like xml:lang. Except for the one struct case, attribute
913	 * qualifiers don't affect the output form.
914	 *
915	 * <blockquote>
916	 *
917	 * <pre>
918	 * 	&lt;ns:UnqualifiedSimpleProperty&gt;value&lt;/ns:UnqualifiedSimpleProperty&gt;
919	 *
920	 * 	&lt;ns:UnqualifiedStructProperty rdf:parseType=&quot;Resource&quot;&gt;
921	 * 		(If no rdf:resource qualifier)
922	 * 		... Fields, same forms as top level properties
923	 * 	&lt;/ns:UnqualifiedStructProperty&gt;
924	 *
925	 * 	&lt;ns:ResourceStructProperty rdf:resource=&quot;URI&quot;
926	 * 		... Fields as attributes
927	 * 	&gt;
928	 *
929	 * 	&lt;ns:UnqualifiedArrayProperty&gt;
930	 * 		&lt;rdf:Bag&gt; or Seq or Alt
931	 * 			... Array items as rdf:li elements, same forms as top level properties
932	 * 		&lt;/rdf:Bag&gt;
933	 * 	&lt;/ns:UnqualifiedArrayProperty&gt;
934	 *
935	 * 	&lt;ns:QualifiedProperty rdf:parseType=&quot;Resource&quot;&gt;
936	 * 		&lt;rdf:value&gt; ... Property &quot;value&quot; following the unqualified
937	 * 			forms ... &lt;/rdf:value&gt;
938	 * 		... Qualifiers looking like named struct fields
939	 * 	&lt;/ns:QualifiedProperty&gt;
940	 * </pre>
941	 *
942	 * </blockquote>
943	 *
944	 * @param node the property node
945	 * @param emitAsRDFValue property shall be renderes as attribute rather than tag
946	 * @param indent the current indent level
947	 * @throws IOException Forwards all writer exceptions.
948	 * @throws XMPException If &quot;rdf:resource&quot; and general qualifiers are mixed.
949	 */
950	private void serializePrettyRDFProperty(XMPNode node, boolean emitAsRDFValue, int indent)
951			throws IOException, XMPException
952	{
953		boolean emitEndTag   = true;
954		boolean indentEndTag = true;
955
956		// Determine the XML element name. Open the start tag with the name and
957		// attribute qualifiers.
958
959		String elemName = node.getName();
960		if (emitAsRDFValue)
961		{
962			elemName = "rdf:value";
963		}
964		else if (XMPConst.ARRAY_ITEM_NAME.equals(elemName))
965		{
966			elemName = "rdf:li";
967		}
968
969		writeIndent(indent);
970		write('<');
971		write(elemName);
972
973		boolean hasGeneralQualifiers = false;
974		boolean hasRDFResourceQual   = false;
975
976		for (Iterator it = node.iterateQualifier(); it.hasNext();)
977		{
978			XMPNode qualifier = (XMPNode) it.next();
979			if (!RDF_ATTR_QUALIFIER.contains(qualifier.getName()))
980			{
981				hasGeneralQualifiers = true;
982			}
983			else
984			{
985				hasRDFResourceQual = "rdf:resource".equals(qualifier.getName());
986				if (!emitAsRDFValue)
987				{
988					write(' ');
989					write(qualifier.getName());
990					write("=\"");
991					appendNodeValue(qualifier.getValue(), true);
992					write('"');
993				}
994			}
995		}
996
997		// Process the property according to the standard patterns.
998
999		if (hasGeneralQualifiers &&  !emitAsRDFValue)
1000		{
1001			// This node has general, non-attribute, qualifiers. Emit using the
1002			// qualified property form.
1003			// ! The value is output by a recursive call ON THE SAME NODE with
1004			// emitAsRDFValue set.
1005
1006			if (hasRDFResourceQual)
1007			{
1008				throw new XMPException("Can't mix rdf:resource and general qualifiers",
1009						XMPError.BADRDF);
1010			}
1011
1012			write(" rdf:parseType=\"Resource\">");
1013			writeNewline();
1014
1015			serializePrettyRDFProperty(node, true, indent + 1);
1016
1017			for (Iterator it = node.iterateQualifier(); it.hasNext();)
1018			{
1019				XMPNode qualifier = (XMPNode) it.next();
1020				if (!RDF_ATTR_QUALIFIER.contains(qualifier.getName()))
1021				{
1022					serializePrettyRDFProperty(qualifier, false, indent + 1);
1023				}
1024			}
1025		}
1026		else
1027		{
1028			// This node has no general qualifiers. Emit using an unqualified form.
1029
1030			if (!node.getOptions().isCompositeProperty())
1031			{
1032				// This is a simple property.
1033
1034				if (node.getOptions().isURI())
1035				{
1036					write(" rdf:resource=\"");
1037					appendNodeValue(node.getValue(), true);
1038					write("\"/>");
1039					writeNewline();
1040					emitEndTag = false;
1041				}
1042				else if (node.getValue() == null ||  "".equals(node.getValue()))
1043				{
1044					write("/>");
1045					writeNewline();
1046					emitEndTag = false;
1047				}
1048				else
1049				{
1050					write('>');
1051					appendNodeValue(node.getValue(), false);
1052					indentEndTag = false;
1053				}
1054			}
1055			else if (node.getOptions().isArray())
1056			{
1057				// This is an array.
1058				write('>');
1059				writeNewline();
1060				emitRDFArrayTag(node, true, indent + 1);
1061				if (node.getOptions().isArrayAltText())
1062				{
1063					XMPNodeUtils.normalizeLangArray(node);
1064				}
1065				for (Iterator it = node.iterateChildren(); it.hasNext();)
1066				{
1067					XMPNode child = (XMPNode) it.next();
1068					serializePrettyRDFProperty(child, false, indent + 2);
1069				}
1070				emitRDFArrayTag(node, false, indent + 1);
1071
1072
1073			}
1074			else if (!hasRDFResourceQual)
1075			{
1076				// This is a "normal" struct, use the rdf:parseType="Resource" form.
1077				if (!node.hasChildren())
1078				{
1079					write(" rdf:parseType=\"Resource\"/>");
1080					writeNewline();
1081					emitEndTag = false;
1082				}
1083				else
1084				{
1085					write(" rdf:parseType=\"Resource\">");
1086					writeNewline();
1087					for (Iterator it = node.iterateChildren(); it.hasNext();)
1088					{
1089						XMPNode child = (XMPNode) it.next();
1090						serializePrettyRDFProperty(child, false, indent + 1);
1091					}
1092				}
1093			}
1094			else
1095			{
1096				// This is a struct with an rdf:resource attribute, use the
1097				// "empty property element" form.
1098				for (Iterator it = node.iterateChildren(); it.hasNext();)
1099				{
1100					XMPNode child = (XMPNode) it.next();
1101					if (!canBeRDFAttrProp(child))
1102					{
1103						throw new XMPException("Can't mix rdf:resource and complex fields",
1104								XMPError.BADRDF);
1105					}
1106					writeNewline();
1107					writeIndent(indent + 1);
1108					write(' ');
1109					write(child.getName());
1110					write("=\"");
1111					appendNodeValue(child.getValue(), true);
1112					write('"');
1113				}
1114				write("/>");
1115				writeNewline();
1116				emitEndTag = false;
1117			}
1118		}
1119
1120		// Emit the property element end tag.
1121		if (emitEndTag)
1122		{
1123			if (indentEndTag)
1124			{
1125				writeIndent(indent);
1126			}
1127			write("</");
1128			write(elemName);
1129			write('>');
1130			writeNewline();
1131		}
1132	}
1133
1134
1135	/**
1136	 * Writes the array start and end tags.
1137	 *
1138	 * @param arrayNode an array node
1139	 * @param isStartTag flag if its the start or end tag
1140	 * @param indent the current indent level
1141	 * @throws IOException forwards writer exceptions
1142	 */
1143	private void emitRDFArrayTag(XMPNode arrayNode, boolean isStartTag, int indent)
1144		throws IOException
1145	{
1146		if (isStartTag  ||  arrayNode.hasChildren())
1147		{
1148			writeIndent(indent);
1149			write(isStartTag ? "<rdf:" : "</rdf:");
1150
1151			if (arrayNode.getOptions().isArrayAlternate())
1152			{
1153				write("Alt");
1154			}
1155			else if (arrayNode.getOptions().isArrayOrdered())
1156			{
1157				write("Seq");
1158			}
1159			else
1160			{
1161				write("Bag");
1162			}
1163
1164			if (isStartTag && !arrayNode.hasChildren())
1165			{
1166				write("/>");
1167			}
1168			else
1169			{
1170				write(">");
1171			}
1172
1173			writeNewline();
1174		}
1175	}
1176
1177
1178	/**
1179	 * Serializes the node value in XML encoding. Its used for tag bodies and
1180	 * attributes. <em>Note:</em> The attribute is always limited by quotes,
1181	 * thats why <code>&amp;apos;</code> is never serialized. <em>Note:</em>
1182	 * Control chars are written unescaped, but if the user uses others than tab, LF
1183	 * and CR the resulting XML will become invalid.
1184	 *
1185	 * @param value the value of the node
1186	 * @param forAttribute flag if value is an attribute value
1187	 * @throws IOException
1188	 */
1189	private void appendNodeValue(String value, boolean forAttribute) throws IOException
1190	{
1191		write (Utils.escapeXML(value, forAttribute, true));
1192	}
1193
1194
1195	/**
1196	 * A node can be serialized as RDF-Attribute, if it meets the following conditions:
1197	 * <ul>
1198	 *  	<li>is not array item
1199	 * 		<li>don't has qualifier
1200	 * 		<li>is no URI
1201	 * 		<li>is no composite property
1202	 * </ul>
1203	 *
1204	 * @param node an XMPNode
1205	 * @return Returns true if the node serialized as RDF-Attribute
1206	 */
1207	private boolean canBeRDFAttrProp(XMPNode node)
1208	{
1209		return
1210			!node.hasQualifier()  &&
1211			!node.getOptions().isURI()  &&
1212			!node.getOptions().isCompositeProperty()  &&
1213			!XMPConst.ARRAY_ITEM_NAME.equals(node.getName());
1214	}
1215
1216
1217	/**
1218	 * Writes indents and automatically includes the baseindend from the options.
1219	 * @param times number of indents to write
1220	 * @throws IOException forwards exception
1221	 */
1222	private void writeIndent(int times) throws IOException
1223	{
1224		for (int i = options.getBaseIndent() + times; i > 0; i--)
1225		{
1226			writer.write(options.getIndent());
1227		}
1228	}
1229
1230
1231	/**
1232	 * Writes a char to the output.
1233	 * @param c a char
1234	 * @throws IOException forwards writer exceptions
1235	 */
1236	private void write(int c) throws IOException
1237	{
1238		writer.write(c);
1239	}
1240
1241
1242	/**
1243	 * Writes a String to the output.
1244	 * @param str a String
1245	 * @throws IOException forwards writer exceptions
1246	 */
1247	private void write(String str) throws IOException
1248	{
1249		writer.write(str);
1250	}
1251
1252
1253	/**
1254	 * Writes an amount of chars, mostly spaces
1255	 * @param number number of chars
1256	 * @param c a char
1257	 * @throws IOException
1258	 */
1259	private void writeChars(int number, char c) throws IOException
1260	{
1261		for (; number > 0; number--)
1262		{
1263			writer.write(c);
1264		}
1265	}
1266
1267
1268	/**
1269	 * Writes a newline according to the options.
1270	 * @throws IOException Forwards exception
1271	 */
1272	private void writeNewline() throws IOException
1273	{
1274		writer.write(options.getNewline());
1275	}
1276}