blob: b00877f1d9c9379620ab2239d39b94884b5b4a66 [file] [log] [blame]
// =================================================================================================
// 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;
}
else if (!hasElemFields)
{
// All fields can be attributes, use the
// emptyPropertyElt form.
serializeCompactRDFAttrProps(node, indent + 1);
write("/>");
writeNewline();
emitEndTag = false;
}
else if (!hasAttrFields)
{
// All fields must be elements, use the
// parseTypeResourcePropertyElt form.
write(" rdf:parseType=\"Resource\">");
writeNewline();
serializeCompactRDFElementProps(node, indent + 1);
}
else
{
// Have a mix of attributes and elements, use an inner rdf:Description.
write('>');
writeNewline();
writeIndent(indent + 1);
write(RDF_STRUCT_START);
serializeCompactRDFAttrProps(node, indent + 2);
write(">");
writeNewline();
serializeCompactRDFElementProps(node, indent + 1);
writeIndent(indent + 1);
write(RDF_STRUCT_END);
writeNewline();
}
return emitEndTag;
}
/**
* Serializes the general qualifier.
* @param node the root node of the subtree
* @param indent the current indent level
* @throws IOException Forwards all writer exceptions.
* @throws XMPException If qualifier and element fields are mixed.
*/
private void serializeCompactRDFGeneralQualifier(int indent, XMPNode node)
throws IOException, XMPException
{
// The node has general qualifiers, ones that can't be
// attributes on a property element.
// Emit using the qualified property pseudo-struct form. The
// value is output by a call
// to SerializePrettyRDFProperty with emitAsRDFValue set.
write(" rdf:parseType=\"Resource\">");
writeNewline();
serializePrettyRDFProperty(node, true, indent + 1);
for (Iterator iq = node.iterateQualifier(); iq.hasNext();)
{
XMPNode qualifier = (XMPNode) iq.next();
serializePrettyRDFProperty(qualifier, false, indent + 1);
}
}
/**
* Serializes one schema with all contained properties in pretty-printed
* manner.<br>
* Each schema's properties are written in a separate
* rdf:Description element. All of the necessary namespaces are declared in
* the rdf:Description element. The baseIndent is the base level for the
* entire serialization, that of the x:xmpmeta element. An xml:lang
* qualifier is written as an attribute of the property start tag, not by
* itself forcing the qualified property form.
*
* <blockquote>
*
* <pre>
* &lt;rdf:Description rdf:about=&quot;TreeName&quot; xmlns:ns=&quot;URI&quot; ... &gt;
*
* ... The actual properties of the schema, see SerializePrettyRDFProperty
*
* &lt;!-- ns1:Alias is aliased to ns2:Actual --&gt; ... If alias comments are wanted
*
* &lt;/rdf:Description&gt;
* </pre>
*
* </blockquote>
*
* @param schemaNode a schema node
* @throws IOException Forwarded writer exceptions
* @throws XMPException
*/
private void serializePrettyRDFSchema(XMPNode schemaNode) throws IOException, XMPException
{
writeIndent(2);
write(RDF_SCHEMA_START);
writeTreeName();
Set usedPrefixes = new HashSet();
usedPrefixes.add("xml");
usedPrefixes.add("rdf");
declareUsedNamespaces(schemaNode, usedPrefixes, 4);
write('>');
writeNewline();
// Write each of the schema's actual properties.
for (Iterator it = schemaNode.iterateChildren(); it.hasNext();)
{
XMPNode propNode = (XMPNode) it.next();
serializePrettyRDFProperty(propNode, false, 3);
}
// Write the rdf:Description end tag.
writeIndent(2);
write(RDF_SCHEMA_END);
writeNewline();
}
/**
* Writes all used namespaces of the subtree in node to the output.
* The subtree is recursivly traversed.
* @param node the root node of the subtree
* @param usedPrefixes a set containing currently used prefixes
* @param indent the current indent level
* @throws IOException Forwards all writer exceptions.
*/
private void declareUsedNamespaces(XMPNode node, Set usedPrefixes, int indent)
throws IOException
{
if (node.getOptions().isSchemaNode())
{
// The schema node name is the URI, the value is the prefix.
String prefix = node.getValue().substring(0, node.getValue().length() - 1);
declareNamespace(prefix, node.getName(), usedPrefixes, indent);
}
else if (node.getOptions().isStruct())
{
for (Iterator it = node.iterateChildren(); it.hasNext();)
{
XMPNode field = (XMPNode) it.next();
declareNamespace(field.getName(), null, usedPrefixes, indent);
}
}
for (Iterator it = node.iterateChildren(); it.hasNext();)
{
XMPNode child = (XMPNode) it.next();
declareUsedNamespaces(child, usedPrefixes, indent);
}
for (Iterator it = node.iterateQualifier(); it.hasNext();)
{
XMPNode qualifier = (XMPNode) it.next();
declareNamespace(qualifier.getName(), null, usedPrefixes, indent);
declareUsedNamespaces(qualifier, usedPrefixes, indent);
}
}
/**
* Writes one namespace declaration to the output.
* @param prefix a namespace prefix (without colon) or a complete qname (when namespace == null)
* @param namespace the a namespace
* @param usedPrefixes a set containing currently used prefixes
* @param indent the current indent level
* @throws IOException Forwards all writer exceptions.
*/
private void declareNamespace(String prefix, String namespace, Set usedPrefixes, int indent)
throws IOException
{
if (namespace == null)
{
// prefix contains qname, extract prefix and lookup namespace with prefix
QName qname = new QName(prefix);
if (qname.hasPrefix())
{
prefix = qname.getPrefix();
// add colon for lookup
namespace = XMPMetaFactory.getSchemaRegistry().getNamespaceURI(prefix + ":");
// prefix w/o colon
declareNamespace(prefix, namespace, usedPrefixes, indent);
}
else
{
return;
}
}
if (!usedPrefixes.contains(prefix))
{
writeNewline();
writeIndent(indent);
write("xmlns:");
write(prefix);
write("=\"");
write(namespace);
write('"');
usedPrefixes.add(prefix);
}
}
/**
* Recursively handles the "value" for a node. 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. An xml:lang qualifier is written
* as an attribute of the property start tag, not by itself forcing the
* qualified property form. The patterns below mostly ignore attribute
* qualifiers like xml:lang. Except for the one struct case, attribute
* qualifiers don't affect the output form.
*
* <blockquote>
*
* <pre>
* &lt;ns:UnqualifiedSimpleProperty&gt;value&lt;/ns:UnqualifiedSimpleProperty&gt;
*
* &lt;ns:UnqualifiedStructProperty rdf:parseType=&quot;Resource&quot;&gt;
* (If no rdf:resource qualifier)
* ... Fields, same forms as top level properties
* &lt;/ns:UnqualifiedStructProperty&gt;
*
* &lt;ns:ResourceStructProperty rdf:resource=&quot;URI&quot;
* ... Fields as attributes
* &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>
*
* @param node the property node
* @param emitAsRDFValue property shall be renderes as attribute rather than tag
* @param indent the current indent level
* @throws IOException Forwards all writer exceptions.
* @throws XMPException If &quot;rdf:resource&quot; and general qualifiers are mixed.
*/
private void serializePrettyRDFProperty(XMPNode node, boolean emitAsRDFValue, int indent)
throws IOException, XMPException
{
boolean emitEndTag = true;
boolean indentEndTag = true;
// Determine the XML element name. Open the start tag with the name and
// attribute qualifiers.
String elemName = node.getName();
if (emitAsRDFValue)
{
elemName = "rdf:value";
}
else if (XMPConst.ARRAY_ITEM_NAME.equals(elemName))
{
elemName = "rdf:li";
}
writeIndent(indent);
write('<');
write(elemName);
boolean hasGeneralQualifiers = false;
boolean hasRDFResourceQual = false;
for (Iterator it = node.iterateQualifier(); it.hasNext();)
{
XMPNode qualifier = (XMPNode) it.next();
if (!RDF_ATTR_QUALIFIER.contains(qualifier.getName()))
{
hasGeneralQualifiers = true;
}
else
{
hasRDFResourceQual = "rdf:resource".equals(qualifier.getName());
if (!emitAsRDFValue)
{
write(' ');
write(qualifier.getName());
write("=\"");
appendNodeValue(qualifier.getValue(), true);
write('"');
}
}
}
// Process the property according to the standard patterns.
if (hasGeneralQualifiers && !emitAsRDFValue)
{
// This node has general, non-attribute, qualifiers. Emit using the
// qualified property form.
// ! The value is output by a recursive call ON THE SAME NODE with
// emitAsRDFValue set.
if (hasRDFResourceQual)
{
throw new XMPException("Can't mix rdf:resource and general qualifiers",
XMPError.BADRDF);
}
write(" rdf:parseType=\"Resource\">");
writeNewline();
serializePrettyRDFProperty(node, true, indent + 1);
for (Iterator it = node.iterateQualifier(); it.hasNext();)
{
XMPNode qualifier = (XMPNode) it.next();
if (!RDF_ATTR_QUALIFIER.contains(qualifier.getName()))
{
serializePrettyRDFProperty(qualifier, false, indent + 1);
}
}
}
else
{
// This node has no general qualifiers. Emit using an unqualified form.
if (!node.getOptions().isCompositeProperty())
{
// This is a simple property.
if (node.getOptions().isURI())
{
write(" rdf:resource=\"");
appendNodeValue(node.getValue(), true);
write("\"/>");
writeNewline();
emitEndTag = false;
}
else if (node.getValue() == null || "".equals(node.getValue()))
{
write("/>");
writeNewline();
emitEndTag = false;
}
else
{
write('>');
appendNodeValue(node.getValue(), false);
indentEndTag = false;
}
}
else if (node.getOptions().isArray())
{
// This is an array.
write('>');
writeNewline();
emitRDFArrayTag(node, true, indent + 1);
if (node.getOptions().isArrayAltText())
{
XMPNodeUtils.normalizeLangArray(node);
}
for (Iterator it = node.iterateChildren(); it.hasNext();)
{
XMPNode child = (XMPNode) it.next();
serializePrettyRDFProperty(child, false, indent + 2);
}
emitRDFArrayTag(node, false, indent + 1);
}
else if (!hasRDFResourceQual)
{
// This is a "normal" struct, use the rdf:parseType="Resource" form.
if (!node.hasChildren())
{
write(" rdf:parseType=\"Resource\"/>");
writeNewline();
emitEndTag = false;
}
else
{
write(" rdf:parseType=\"Resource\">");
writeNewline();
for (Iterator it = node.iterateChildren(); it.hasNext();)
{
XMPNode child = (XMPNode) it.next();
serializePrettyRDFProperty(child, false, indent + 1);
}
}
}
else
{
// This is a struct with an rdf:resource attribute, use the
// "empty property element" form.
for (Iterator it = node.iterateChildren(); it.hasNext();)
{
XMPNode child = (XMPNode) it.next();
if (!canBeRDFAttrProp(child))
{
throw new XMPException("Can't mix rdf:resource and complex fields",
XMPError.BADRDF);
}
writeNewline();
writeIndent(indent + 1);
write(' ');
write(child.getName());
write("=\"");
appendNodeValue(child.getValue(), true);
write('"');
}
write("/>");
writeNewline();
emitEndTag = false;
}
}
// Emit the property element end tag.
if (emitEndTag)
{
if (indentEndTag)
{
writeIndent(indent);
}
write("</");
write(elemName);
write('>');
writeNewline();
}
}
/**
* Writes the array start and end tags.
*
* @param arrayNode an array node
* @param isStartTag flag if its the start or end tag
* @param indent the current indent level
* @throws IOException forwards writer exceptions
*/
private void emitRDFArrayTag(XMPNode arrayNode, boolean isStartTag, int indent)
throws IOException
{
if (isStartTag || arrayNode.hasChildren())
{
writeIndent(indent);
write(isStartTag ? "<rdf:" : "</rdf:");
if (arrayNode.getOptions().isArrayAlternate())
{
write("Alt");
}
else if (arrayNode.getOptions().isArrayOrdered())
{
write("Seq");
}
else
{
write("Bag");
}
if (isStartTag && !arrayNode.hasChildren())
{
write("/>");
}
else
{
write(">");
}
writeNewline();
}
}
/**
* Serializes the node value in XML encoding. Its used for tag bodies and
* attributes. <em>Note:</em> The attribute is always limited by quotes,
* thats why <code>&amp;apos;</code> is never serialized. <em>Note:</em>
* Control chars are written unescaped, but if the user uses others than tab, LF
* and CR the resulting XML will become invalid.
*
* @param value the value of the node
* @param forAttribute flag if value is an attribute value
* @throws IOException
*/
private void appendNodeValue(String value, boolean forAttribute) throws IOException
{
write (Utils.escapeXML(value, forAttribute, true));
}
/**
* A node can be serialized as RDF-Attribute, if it meets the following conditions:
* <ul>
* <li>is not array item
* <li>don't has qualifier
* <li>is no URI
* <li>is no composite property
* </ul>
*
* @param node an XMPNode
* @return Returns true if the node serialized as RDF-Attribute
*/
private boolean canBeRDFAttrProp(XMPNode node)
{
return
!node.hasQualifier() &&
!node.getOptions().isURI() &&
!node.getOptions().isCompositeProperty() &&
!XMPConst.ARRAY_ITEM_NAME.equals(node.getName());
}
/**
* Writes indents and automatically includes the baseindend from the options.
* @param times number of indents to write
* @throws IOException forwards exception
*/
private void writeIndent(int times) throws IOException
{
for (int i = options.getBaseIndent() + times; i > 0; i--)
{
writer.write(options.getIndent());
}
}
/**
* Writes a char to the output.
* @param c a char
* @throws IOException forwards writer exceptions
*/
private void write(int c) throws IOException
{
writer.write(c);
}
/**
* Writes a String to the output.
* @param str a String
* @throws IOException forwards writer exceptions
*/
private void write(String str) throws IOException
{
writer.write(str);
}
/**
* Writes an amount of chars, mostly spaces
* @param number number of chars
* @param c a char
* @throws IOException
*/
private void writeChars(int number, char c) throws IOException
{
for (; number > 0; number--)
{
writer.write(c);
}
}
/**
* Writes a newline according to the options.
* @throws IOException Forwards exception
*/
private void writeNewline() throws IOException
{
writer.write(options.getNewline());
}
}