gt: 		else if (!hasElemFields)
context: // =================================================================================================
  // ADOBE SYSTEMS INCORPORATED
  // Copyright 2006 Adobe Systems Incorporated
  // All Rights Reserved
  //
  // NOTICE:  Adobe permits you to use, modify, and distribute this file in accordance with the terms
  // of the Adobe license agreement accompanying it.
  // =================================================================================================
  
  package com.adobe.xmp.impl;
  
  import java.io.IOException;
  import java.io.OutputStream;
  import java.io.OutputStreamWriter;
  import java.util.Arrays;
  import java.util.HashSet;
  import java.util.Iterator;
  import java.util.Set;
  
  import com.adobe.xmp.XMPConst;
  import com.adobe.xmp.XMPError;
  import com.adobe.xmp.XMPException;
  import com.adobe.xmp.XMPMeta;
  import com.adobe.xmp.XMPMetaFactory;
  import com.adobe.xmp.options.SerializeOptions;
  
  
  /**
   * Serializes the <code>XMPMeta</code>-object using the standard RDF serialization format. 
   * The output is written to an <code>OutputStream</code> 
   * according to the <code>SerializeOptions</code>. 
   * 
   * @since   11.07.2006
   */
  public class XMPSerializerRDF
  {
  	/** default padding */
  	private static final int DEFAULT_PAD = 2048;	
  	/** */
  	private static final String PACKET_HEADER  =
  		"<?xpacket begin=\"\uFEFF\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?>";
  	/** The w/r is missing inbetween */
  	private static final String PACKET_TRAILER = "<?xpacket end=\"";
  	/** */
  	private static final String PACKET_TRAILER2 = "\"?>"; 
  	/** */
  	private static final String RDF_XMPMETA_START = 
  		"<x:xmpmeta xmlns:x=\"adobe:ns:meta/\" x:xmptk=\"";
  	/** */
  	private static final String RDF_XMPMETA_END   = "</x:xmpmeta>";
  	/** */
  	private static final String RDF_RDF_START = 
  		"<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">";
  	/** */
  	private static final String RDF_RDF_END       = "</rdf:RDF>";
  	
  	/** */
  	private static final String RDF_SCHEMA_START  = "<rdf:Description rdf:about=";
  	/** */
  	private static final String RDF_SCHEMA_END    = "</rdf:Description>";
  	/** */
  	private static final String RDF_STRUCT_START  = "<rdf:Description";
  	/** */
  	private static final String RDF_STRUCT_END    = "</rdf:Description>";
  	/** a set of all rdf attribute qualifier */
  	static final Set RDF_ATTR_QUALIFIER = new HashSet(Arrays.asList(new String[] {
  			XMPConst.XML_LANG, "rdf:resource", "rdf:ID", "rdf:bagID", "rdf:nodeID" }));
  	
  	/** the metadata object to be serialized. */ 
  	private XMPMetaImpl xmp; 
  	/** the output stream to serialize to */ 
  	private CountOutputStream outputStream;
  	/** this writer is used to do the actual serialisation */
  	private OutputStreamWriter writer;
  	/** the stored serialisation options */
  	private SerializeOptions options;
  	/** the size of one unicode char, for UTF-8 set to 1 
  	 *  (Note: only valid for ASCII chars lower than 0x80),
  	 *  set to 2 in case of UTF-16 */
  	private int unicodeSize = 1; // UTF-8
  	/** the padding in the XMP Packet, or the length of the complete packet in
  	 *  case of option <em>exactPacketLength</em>. */ 
  	private int padding;
  
  	
  	/**
  	 * The actual serialisation.
  	 * 
  	 * @param xmp the metadata object to be serialized
  	 * @param out outputStream the output stream to serialize to
  	 * @param options the serialization options
  	 * 
  	 * @throws XMPException If case of wrong options or any other serialisaton error.
  	 */
  	public void serialize(XMPMeta xmp, OutputStream out, 
  			SerializeOptions options) throws XMPException
  	{
  		try
  		{
  			outputStream = new CountOutputStream(out); 
  			writer = new OutputStreamWriter(outputStream, options.getEncoding());
  			
  			this.xmp = (XMPMetaImpl) xmp;
  			this.options = options;
  			this.padding = options.getPadding();
  
  			writer = new OutputStreamWriter(outputStream, options.getEncoding());
  			
  			checkOptionsConsistence();
  			
  			// serializes the whole packet, but don't write the tail yet 
  			// and flush to make sure that the written bytes are calculated correctly
  			String tailStr = serializeAsRDF();
  			writer.flush();
  			
  			// adds padding
  			addPadding(tailStr.length());
  
  			// writes the tail
  			write(tailStr);
  			writer.flush();
  			
  			outputStream.close();
  		}
  		catch (IOException e)
  		{
  			throw new XMPException("Error writing to the OutputStream", XMPError.UNKNOWN);
  		}
  	}
  
  
  	/**
  	 * Calulates the padding according to the options and write it to the stream.
  	 * @param tailLength the length of the tail string 
  	 * @throws XMPException thrown if packet size is to small to fit the padding
  	 * @throws IOException forwards writer errors
  	 */
  	private void addPadding(int tailLength) throws XMPException, IOException
  	{
  		if (options.getExactPacketLength())
  		{
  			// the string length is equal to the length of the UTF-8 encoding
  			int minSize = outputStream.getBytesWritten() + tailLength * unicodeSize;
  			if (minSize > padding)
  			{
  				throw new XMPException("Can't fit into specified packet size",
  					XMPError.BADSERIALIZE);
  			}
  			padding -= minSize;	// Now the actual amount of padding to add.
  		}
  
  		// fix rest of the padding according to Unicode unit size.
  		padding /= unicodeSize;
  		
  		int newlineLen = options.getNewline().length();
  		if (padding >= newlineLen)
  		{
  			padding -= newlineLen;	// Write this newline last.
  			while (padding >= (100 + newlineLen))
  			{
  				writeChars(100, ' ');
  				writeNewline();
  				padding -= (100 + newlineLen);
  			}
  			writeChars(padding, ' ');
  			writeNewline();
  		}
  		else
  		{
  			writeChars(padding, ' ');				
  		}
  	}
  
  
  	/**
  	 * Checks if the supplied options are consistent.
  	 * @throws XMPException Thrown if options are conflicting
  	 */
  	protected void checkOptionsConsistence() throws XMPException
  	{
  		if (options.getEncodeUTF16BE() | options.getEncodeUTF16LE())
  		{
  			unicodeSize = 2;
  		}
  
  		if (options.getExactPacketLength())
  		{
  			if (options.getOmitPacketWrapper() | options.getIncludeThumbnailPad())
  			{
  				throw new XMPException("Inconsistent options for exact size serialize",
  						XMPError.BADOPTIONS);
  			}
  			if ((options.getPadding() & (unicodeSize - 1)) != 0)
  			{
  				throw new XMPException("Exact size must be a multiple of the Unicode element",
  						XMPError.BADOPTIONS);
  			}
  		}
  		else if (options.getReadOnlyPacket())
  		{
  			if (options.getOmitPacketWrapper() | options.getIncludeThumbnailPad())
  			{
  				throw new XMPException("Inconsistent options for read-only packet",
  						XMPError.BADOPTIONS);
  			}
  			padding = 0;
  		}
  		else if (options.getOmitPacketWrapper())
  		{
  			if (options.getIncludeThumbnailPad())
  			{
  				throw new XMPException("Inconsistent options for non-packet serialize",
  						XMPError.BADOPTIONS);
  			}
  			padding = 0;
  		}
  		else
  		{
  			if (padding == 0)
  			{
  				padding = DEFAULT_PAD * unicodeSize;
  			}
  			
  			if (options.getIncludeThumbnailPad())
  			{
  				if (!xmp.doesPropertyExist(XMPConst.NS_XMP, "Thumbnails"))
  				{
  					padding += 10000 * unicodeSize;
  				}
  			}
  		}
  	}
  	
  	
  	/**
  	 * Writes the (optional) packet header and the outer rdf-tags. 
  	 * @return Returns the packet end processing instraction to be written after the padding. 
  	 * @throws IOException Forwarded writer exceptions.
  	 * @throws XMPException 
  	 */
  	private String serializeAsRDF() throws IOException, XMPException
  	{
  		// Write the packet header PI.
  		if (!options.getOmitPacketWrapper())
  		{
  			writeIndent(0);
  			write(PACKET_HEADER);
  			writeNewline();
  		}
  	
  		// Write the xmpmeta element's start tag.
  		writeIndent(0);
  		write(RDF_XMPMETA_START);
  		// Note: this flag can only be set by unit tests
  		if (!options.getOmitVersionAttribute())
  		{	
  			write(XMPMetaFactory.getVersionInfo().getMessage());
  		}	
  		write("\">");
  		writeNewline();
  	
  		// Write the rdf:RDF start tag.
  		writeIndent(1);
  		write(RDF_RDF_START);
  		writeNewline();
  		
  		// Write all of the properties.
  		if (options.getUseCompactFormat())
  		{
  			serializeCompactRDFSchemas();
  		} 
  		else
  		{
  			serializePrettyRDFSchemas();
  		}
  	
  		// Write the rdf:RDF end tag.
  		writeIndent(1);
  		write(RDF_RDF_END);
  		writeNewline();
  	
  		// Write the xmpmeta end tag.
  		writeIndent(0);		
  		write(RDF_XMPMETA_END);
  		writeNewline();
  		
  		// Write the packet trailer PI into the tail string as UTF-8.
  		String tailStr = "";
  		if (!options.getOmitPacketWrapper())
  		{
  			for (int level = options.getBaseIndent(); level > 0; level--)
  			{
  				tailStr += options.getIndent();
  			}
  
  			tailStr += PACKET_TRAILER;
  			tailStr += options.getReadOnlyPacket() ? 'r' : 'w';
  			tailStr += PACKET_TRAILER2;
  		}
  		
  		return tailStr;
  	}
  
  	
  	/**
  	 * Serializes the metadata in pretty-printed manner.
  	 * @throws IOException Forwarded writer exceptions
  	 * @throws XMPException 
  	 */
  	private void serializePrettyRDFSchemas() throws IOException, XMPException
  	{
  		if (xmp.getRoot().getChildrenLength() > 0)
  		{
  			for (Iterator it = xmp.getRoot().iterateChildren(); it.hasNext(); )
  			{
  				XMPNode currSchema = (XMPNode) it.next();
  				serializePrettyRDFSchema(currSchema);
  			}
  		}
  		else
  		{
  			writeIndent(2);
  			write(RDF_SCHEMA_START); // Special case an empty XMP object.
  			writeTreeName();
  			write("/>");
  			writeNewline();
  		}		
  	}
  
  
  	/**
  	 * @throws IOException
  	 */
  	private void writeTreeName() throws IOException
  	{
  		write('"');
  		String name = xmp.getRoot().getName();
  		if (name != null)
  		{	
  			appendNodeValue(name, true);
  		}	
  		write('"');
  	}
  	
  	
  	/**
  	 * Serializes the metadata in compact manner.
  	 * @throws IOException Forwarded writer exceptions
  	 * @throws XMPException 
  	 */
  	private void serializeCompactRDFSchemas() throws IOException, XMPException
  	{
  		// Begin the rdf:Description start tag.
  		writeIndent(2);
  		write(RDF_SCHEMA_START);
  		writeTreeName();
  		
  		// Write all necessary xmlns attributes.
  		Set usedPrefixes = new HashSet();
  		usedPrefixes.add("xml");
  		usedPrefixes.add("rdf");
  
  		for (Iterator it = xmp.getRoot().iterateChildren(); it.hasNext();)
  		{
  			XMPNode schema = (XMPNode) it.next();
  			declareUsedNamespaces(schema, usedPrefixes, 4);
  		}
  	
  		// Write the top level "attrProps" and close the rdf:Description start tag.
  		boolean allAreAttrs = true;
  		for (Iterator it = xmp.getRoot().iterateChildren(); it.hasNext();)
  		{
  			XMPNode schema = (XMPNode) it.next();
  			allAreAttrs &= serializeCompactRDFAttrProps (schema, 3);
  		}
  
  		if (!allAreAttrs)
  		{
  			write('>');
  			writeNewline();
  		}
  		else
  		{
  			write("/>");
  			writeNewline();
  			return;	// ! Done if all properties in all schema are written as attributes.
  		}
  	
  		// Write the remaining properties for each schema.
  		for (Iterator it = xmp.getRoot().iterateChildren(); it.hasNext();)
  		{
  			XMPNode schema = (XMPNode) it.next();
  			serializeCompactRDFElementProps (schema, 3);
  		}
  
  		// Write the rdf:Description end tag.
  		writeIndent(2);
  		write(RDF_SCHEMA_END);
  		writeNewline();
  	}
  
  
  	
  	/**
  	 * Write each of the parent's simple unqualified properties as an attribute. Returns true if all
  	 * of the properties are written as attributes.
  	 * 
  	 * @param parentNode the parent property node
  	 * @param indent the current indent level
  	 * @return Returns true if all properties can be rendered as RDF attribute.
  	 * @throws IOException
  	 */
  	private boolean serializeCompactRDFAttrProps(XMPNode parentNode, int indent) throws IOException
  	{
  		boolean allAreAttrs = true;
  	
  		for (Iterator it = parentNode.iterateChildren(); it.hasNext();)
  		{
  			XMPNode prop = (XMPNode) it.next();
  		
  			if (canBeRDFAttrProp(prop))
  			{
  				writeNewline();
  				writeIndent(indent);
  				write(prop.getName());
  				write("=\"");
  				appendNodeValue(prop.getValue(), true);
  				write('"');
  			}
  			else
  			{
  				allAreAttrs = false;
  			}
  		}
  		return allAreAttrs;
  	}
  
  	
  	/**
  	 * Recursively handles the "value" for a node that must be written as an RDF
  	 * property element. It does not matter if it is a top level property, a
  	 * field of a struct, or an item of an array. The indent is that for the
  	 * property element. The patterns bwlow ignore attribute qualifiers such as
  	 * xml:lang, they don't affect the output form.
  	 * 
  	 * <blockquote>
  	 * 
  	 * <pre>
  	 *  	&lt;ns:UnqualifiedStructProperty-1
  	 *  		... The fields as attributes, if all are simple and unqualified
  	 *  	/&gt;
  	 *  
  	 *  	&lt;ns:UnqualifiedStructProperty-2 rdf:parseType=&quot;Resource&quot;&gt;
  	 *  		... The fields as elements, if none are simple and unqualified
  	 *  	&lt;/ns:UnqualifiedStructProperty-2&gt;
  	 *  
  	 *  	&lt;ns:UnqualifiedStructProperty-3&gt;
  	 *  		&lt;rdf:Description
  	 *  			... The simple and unqualified fields as attributes
  	 *  		&gt;
  	 *  			... The compound or qualified fields as elements
  	 *  		&lt;/rdf:Description&gt;
  	 *  	&lt;/ns:UnqualifiedStructProperty-3&gt;
  	 *  
  	 *  	&lt;ns:UnqualifiedArrayProperty&gt;
  	 *  		&lt;rdf:Bag&gt; or Seq or Alt
  	 *  			... Array items as rdf:li elements, same forms as top level properties
  	 *  		&lt;/rdf:Bag&gt;
  	 *  	&lt;/ns:UnqualifiedArrayProperty&gt;
  	 *  
  	 *  	&lt;ns:QualifiedProperty rdf:parseType=&quot;Resource&quot;&gt;
  	 *  		&lt;rdf:value&gt; ... Property &quot;value&quot; 
  	 *  			following the unqualified forms ... &lt;/rdf:value&gt;
  	 *  		... Qualifiers looking like named struct fields
  	 *  	&lt;/ns:QualifiedProperty&gt;
  	 * </pre>
  	 * 
  	 * </blockquote>
  	 * 
  	 * *** Consider numbered array items, but has compatibility problems. ***
  	 * Consider qualified form with rdf:Description and attributes.
  	 * 
  	 * @param parentNode the parent node
  	 * @param indent the current indent level
  	 * @throws IOException Forwards writer exceptions
  	 * @throws XMPException If qualifier and element fields are mixed.
  	 */
  	private void serializeCompactRDFElementProps(XMPNode parentNode, int indent)
  			throws IOException, XMPException
  	{
  		for (Iterator it = parentNode.iterateChildren(); it.hasNext();)
  		{
  			XMPNode node = (XMPNode) it.next();
  			if (canBeRDFAttrProp (node))
  			{	
  				continue;
  			}
  	
  			boolean emitEndTag = true;
  			boolean indentEndTag = true;
  	
  			// Determine the XML element name, write the name part of the start tag. Look over the
  			// qualifiers to decide on "normal" versus "rdf:value" form. Emit the attribute
  			// qualifiers at the same time.
  			String elemName = node.getName();
  			if (XMPConst.ARRAY_ITEM_NAME.equals(elemName))
  			{
  				elemName = "rdf:li";
  			}
  	
  			writeIndent(indent);
  			write('<');
  			write(elemName);
  	
  			boolean hasGeneralQualifiers = false;
  			boolean hasRDFResourceQual   = false;
  	
  			for (Iterator iq = 	node.iterateQualifier(); iq.hasNext();)
  			{
  				XMPNode qualifier = (XMPNode) iq.next();
  				if (!RDF_ATTR_QUALIFIER.contains(qualifier.getName()))
  				{
  					hasGeneralQualifiers = true;
  				}
  				else
  				{
  					hasRDFResourceQual = "rdf:resource".equals(qualifier.getName());
  					write(' ');
  					write(qualifier.getName());
  					write("=\"");
  					appendNodeValue(qualifier.getValue(), true);
  					write('"');
  				}
  			}
  			
  			
  			// Process the property according to the standard patterns.
  			if (hasGeneralQualifiers)
  			{
  				serializeCompactRDFGeneralQualifier(indent, node);
  			}
  			else
  			{
  				// This node has only attribute qualifiers. Emit as a property element.
  				if (!node.getOptions().isCompositeProperty())
  				{
  					Object[] result = serializeCompactRDFSimpleProp(node);
  					emitEndTag = ((Boolean) result[0]).booleanValue();
  					indentEndTag = ((Boolean) result[1]).booleanValue();
  				}
  				else if (node.getOptions().isArray())
  				{
  					serializeCompactRDFArrayProp(node, indent);
  				}
  				else
  				{
  					emitEndTag = serializeCompactRDFStructProp(
  						node, indent, hasRDFResourceQual);
  				}
  	
  			}
  
  			// Emit the property element end tag.
  			if (emitEndTag)
  			{
  				if (indentEndTag)
  				{
  					writeIndent(indent);
  				}
  				write("</");
  				write(elemName);
  				write('>');
  				writeNewline();
  			}
  	
  		}
  	}
  
  
  	/**
  	 * Serializes a simple property.
  	 * 
  	 * @param node an XMPNode 
  	 * @return Returns an array containing the flags emitEndTag and indentEndTag.
  	 * @throws IOException Forwards the writer exceptions.
  	 */
  	private Object[] serializeCompactRDFSimpleProp(XMPNode node) throws IOException
  	{
  		// This is a simple property.
  		Boolean emitEndTag = Boolean.TRUE;
  		Boolean indentEndTag = Boolean.TRUE;		
  
  		if (node.getOptions().isURI())
  		{
  			write(" rdf:resource=\"");
  			appendNodeValue(node.getValue(), true);
  			write("\"/>");
  			writeNewline();
  			emitEndTag = Boolean.FALSE;
  		}
  		else if (node.getValue() == null  ||  node.getValue().length() == 0)
  		{
  			write("/>");
  			writeNewline();
  			emitEndTag = Boolean.FALSE;
  		}
  		else
  		{
  			write('>');
  			appendNodeValue (node.getValue(), false);
  			indentEndTag = Boolean.FALSE;
  		}
  		
  		return new Object[] {emitEndTag, indentEndTag};
  	}
  
  
  	/**
  	 * Serializes an array property.
  	 * 
  	 * @param node an XMPNode 
  	 * @param indent the current indent level 
  	 * @throws IOException Forwards the writer exceptions.
  	 * @throws XMPException If qualifier and element fields are mixed.
  	 */
  	private void serializeCompactRDFArrayProp(XMPNode node, int indent) throws IOException,
  			XMPException
  	{
  		// This is an array.
  		write('>');
  		writeNewline();
  		emitRDFArrayTag (node, true, indent + 1);
  		
  		if (node.getOptions().isArrayAltText())
  		{
  			XMPNodeUtils.normalizeLangArray (node);
  		}
  		
  		serializeCompactRDFElementProps(node, indent + 2);
  		
  		emitRDFArrayTag(node, false, indent + 1);
  	}
  	
  	
  	/**
  	 * Serializes a struct property.
  	 * 
  	 * @param node an XMPNode 
  	 * @param indent the current indent level 
  	 * @param hasRDFResourceQual Flag if the element has resource qualifier
  	 * @return Returns true if an end flag shall be emitted.
  	 * @throws IOException Forwards the writer exceptions.
  	 * @throws XMPException If qualifier and element fields are mixed.
  	 */
  	private boolean serializeCompactRDFStructProp(XMPNode node, int indent,
  			boolean hasRDFResourceQual) throws XMPException, IOException
  	{
  		// This must be a struct.
  		boolean hasAttrFields = false;
  		boolean hasElemFields = false;
  		boolean emitEndTag = true;
  		
  		for (Iterator ic = node.iterateChildren(); ic.hasNext(); )
  		{
  			XMPNode field = (XMPNode) ic.next();
  			if (canBeRDFAttrProp(field))
  			{
  				hasAttrFields = true;
  			}
  			else
  			{
  				hasElemFields = true;
  			}
  
  			if (hasAttrFields  &&  hasElemFields)
  			{
  				break;	// No sense looking further.
  			}
  		}
  		
  		if (hasRDFResourceQual && hasElemFields)
  		{
  			throw new XMPException(
  					"Can't mix rdf:resource qualifier and element fields",
  					XMPError.BADRDF);
  		}
  
  		if (!node.hasChildren())
  		{
  			// Catch an empty struct as a special case. The case
  			// below would emit an empty
  			// XML element, which gets reparsed as a simple property
  			// with an empty value.
  			write(" rdf:parseType=\"Resource\"/>");
  			writeNewline();
  			emitEndTag = false;
  		
  		}
  