• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
10 package com.adobe.xmp.impl;
11 
12 import java.io.IOException;
13 import java.io.OutputStream;
14 import java.io.OutputStreamWriter;
15 import java.util.Arrays;
16 import java.util.HashSet;
17 import java.util.Iterator;
18 import java.util.Set;
19 
20 import com.adobe.xmp.XMPConst;
21 import com.adobe.xmp.XMPError;
22 import com.adobe.xmp.XMPException;
23 import com.adobe.xmp.XMPMeta;
24 import com.adobe.xmp.XMPMetaFactory;
25 import 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  */
35 public 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 	 */
serialize(XMPMeta xmp, OutputStream out, SerializeOptions options)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 	 */
addPadding(int tailLength)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 	 */
checkOptionsConsistence()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 	 */
serializeAsRDF()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 	 */
serializePrettyRDFSchemas()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 	 */
writeTreeName()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 	 */
serializeCompactRDFSchemas()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 	 */
serializeCompactRDFAttrProps(XMPNode parentNode, int indent)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 	 */
serializeCompactRDFElementProps(XMPNode parentNode, int indent)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 	 */
serializeCompactRDFSimpleProp(XMPNode node)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 	 */
serializeCompactRDFArrayProp(XMPNode node, int indent)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 	 */
serializeCompactRDFStructProp(XMPNode node, int indent, boolean hasRDFResourceQual)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 	 */
serializeCompactRDFGeneralQualifier(int indent, XMPNode node)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 	 */
serializePrettyRDFSchema(XMPNode schemaNode)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 	 */
declareUsedNamespaces(XMPNode node, Set usedPrefixes, int indent)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 	 */
declareNamespace(String prefix, String namespace, Set usedPrefixes, int indent)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 	 */
serializePrettyRDFProperty(XMPNode node, boolean emitAsRDFValue, int indent)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 	 */
emitRDFArrayTag(XMPNode arrayNode, boolean isStartTag, int indent)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 	 */
appendNodeValue(String value, boolean forAttribute)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 	 */
canBeRDFAttrProp(XMPNode node)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 	 */
writeIndent(int times)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 	 */
write(int c)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 	 */
write(String str)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 	 */
writeChars(int number, char c)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 	 */
writeNewline()1272 	private void writeNewline() throws IOException
1273 	{
1274 		writer.write(options.getNewline());
1275 	}
1276 }