Home | History | Annotate | Download | only in impl
      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 	 */
     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 }