diff options
Diffstat (limited to 'java/XMPCore/src/com/adobe/xmp/impl/XMPSerializerRDF.java')
-rw-r--r-- | java/XMPCore/src/com/adobe/xmp/impl/XMPSerializerRDF.java | 1295 |
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> + * <ns:UnqualifiedStructProperty-1 + * ... The fields as attributes, if all are simple and unqualified + * /> + * + * <ns:UnqualifiedStructProperty-2 rdf:parseType="Resource"> + * ... The fields as elements, if none are simple and unqualified + * </ns:UnqualifiedStructProperty-2> + * + * <ns:UnqualifiedStructProperty-3> + * <rdf:Description + * ... The simple and unqualified fields as attributes + * > + * ... The compound or qualified fields as elements + * </rdf:Description> + * </ns:UnqualifiedStructProperty-3> + * + * <ns:UnqualifiedArrayProperty> + * <rdf:Bag> or Seq or Alt + * ... Array items as rdf:li elements, same forms as top level properties + * </rdf:Bag> + * </ns:UnqualifiedArrayProperty> + * + * <ns:QualifiedProperty rdf:parseType="Resource"> + * <rdf:value> ... Property "value" + * following the unqualified forms ... </rdf:value> + * ... Qualifiers looking like named struct fields + * </ns:QualifiedProperty> + * </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> + * <rdf:Description rdf:about="TreeName" xmlns:ns="URI" ... > + * + * ... The actual properties of the schema, see SerializePrettyRDFProperty + * + * <!-- ns1:Alias is aliased to ns2:Actual --> ... If alias comments are wanted + * + * </rdf:Description> + * </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> + * <ns:UnqualifiedSimpleProperty>value</ns:UnqualifiedSimpleProperty> + * + * <ns:UnqualifiedStructProperty rdf:parseType="Resource"> + * (If no rdf:resource qualifier) + * ... Fields, same forms as top level properties + * </ns:UnqualifiedStructProperty> + * + * <ns:ResourceStructProperty rdf:resource="URI" + * ... Fields as attributes + * > + * + * <ns:UnqualifiedArrayProperty> + * <rdf:Bag> or Seq or Alt + * ... Array items as rdf:li elements, same forms as top level properties + * </rdf:Bag> + * </ns:UnqualifiedArrayProperty> + * + * <ns:QualifiedProperty rdf:parseType="Resource"> + * <rdf:value> ... Property "value" following the unqualified + * forms ... </rdf:value> + * ... Qualifiers looking like named struct fields + * </ns:QualifiedProperty> + * </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 "rdf:resource" 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>&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 |