summaryrefslogtreecommitdiff
path: root/java/XMPCore/src/com/adobe/xmp/impl/XMPSerializerRDF.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/XMPCore/src/com/adobe/xmp/impl/XMPSerializerRDF.java')
-rw-r--r--java/XMPCore/src/com/adobe/xmp/impl/XMPSerializerRDF.java1295
1 files changed, 1295 insertions, 0 deletions
diff --git a/java/XMPCore/src/com/adobe/xmp/impl/XMPSerializerRDF.java b/java/XMPCore/src/com/adobe/xmp/impl/XMPSerializerRDF.java
new file mode 100644
index 0000000..24cafd3
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/impl/XMPSerializerRDF.java
@@ -0,0 +1,1295 @@
+// =================================================================================================
+// ADOBE SYSTEMS INCORPORATED
+// Copyright 2006-2007 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 isCompact = node.getOptions().isCompact();
+ boolean hasGeneralQualifiers = isCompact; // Might also become true later.
+ 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, isCompact);
+ }
+ 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
+ * @param isCompact flag if qual shall be renderen in compact form.
+ * @throws IOException Forwards all writer exceptions.
+ * @throws XMPException If qualifier and element fields are mixed.
+ */
+ private void serializeCompactRDFGeneralQualifier(int indent, XMPNode node, boolean isCompact)
+ 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);
+
+ if (isCompact)
+ {
+ // Emit a "pxmp:compact" fake qualifier.
+ writeIndent(1);
+ write("<pxmp:compact/>");
+ writeNewline();
+ }
+
+ 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 isCompact = node.getOptions().isCompact();
+ boolean hasGeneralQualifiers = isCompact; // Might also become true later.
+ 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);
+
+ if (isCompact)
+ {
+ // Emit a "pxmp:compact" fake qualifier.
+ writeIndent(indent);
+ write("<pxmp:compact/>");
+ writeNewline();
+ }
+
+ 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());
+ }
+} \ No newline at end of file