//Copyright (c) 2006, Adobe Systems Incorporated //All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // 1. Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // 2. Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // 3. All advertising materials mentioning features or use of this software // must display the following acknowledgement: // This product includes software developed by the Adobe Systems Incorporated. // 4. Neither the name of the Adobe Systems Incorporated nor the // names of its contributors may be used to endorse or promote products // derived from this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY ADOBE SYSTEMS INCORPORATED ''AS IS'' AND ANY // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE // DISCLAIMED. IN NO EVENT SHALL ADOBE SYSTEMS INCORPORATED BE LIABLE FOR ANY // DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // // http://www.adobe.com/devnet/xmp/library/eula-xmp-library-java.html package com.itextpdf.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.itextpdf.xmp.XMPConst; import com.itextpdf.xmp.XMPError; import com.itextpdf.xmp.XMPException; import com.itextpdf.xmp.XMPMeta; import com.itextpdf.xmp.XMPMetaFactory; import com.itextpdf.xmp.options.PropertyOptions; import com.itextpdf.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>"; /** */ private static final String RDF_EMPTY_STRUCT = "<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 serialization */ private OutputStreamWriter writer; /** the stored serialization 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 serialization. * * @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 serialization error. */ public void serialize(XMPMeta xmp, OutputStream out, SerializeOptions options) throws XMPException { try { outputStream = new CountOutputStream(out); //This writer is never used 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); } } /** * Calculates 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 { int level = 0; // Write the packet header PI. if (!options.getOmitPacketWrapper()) { writeIndent(level); write(PACKET_HEADER); writeNewline(); } // Write the x:xmpmeta element's start tag. if (!options.getOmitXmpMetaElement()) { writeIndent(level); write(RDF_XMPMETA_START); // Note: this flag can only be set by unit tests if (!options.getOmitVersionAttribute()) { write(XMPMetaFactory.getVersionInfo().getMessage()); } write("\">"); writeNewline(); level++; } // Write the rdf:RDF start tag. writeIndent(level); write(RDF_RDF_START); writeNewline(); // Write all of the properties. if (options.getUseCanonicalFormat()) { serializeCanonicalRDFSchemas(level); } else { serializeCompactRDFSchemas(level); } // Write the rdf:RDF end tag. writeIndent(level); write(RDF_RDF_END); writeNewline(); // Write the xmpmeta end tag. if (!options.getOmitXmpMetaElement()) { level--; writeIndent(level); write(RDF_XMPMETA_END); writeNewline(); } // Write the packet trailer PI into the tail string as UTF-8. String tailStr = ""; if (!options.getOmitPacketWrapper()) { for (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. * @param level indent level * @throws IOException Forwarded writer exceptions * @throws XMPException */ private void serializeCanonicalRDFSchemas(int level) throws IOException, XMPException { if (xmp.getRoot().getChildrenLength() > 0) { startOuterRDFDescription(xmp.getRoot(), level); for (Iterator it = xmp.getRoot().iterateChildren(); it.hasNext(); ) { XMPNode currSchema = (XMPNode) it.next(); serializeCanonicalRDFSchema(currSchema, level); } endOuterRDFDescription(level); } else { writeIndent(level + 1); 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. * @param level indent level to start with * @throws IOException Forwarded writer exceptions * @throws XMPException */ private void serializeCompactRDFSchemas(int level) throws IOException, XMPException { // Begin the rdf:Description start tag. writeIndent(level + 1); 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, level + 3); } // 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, level + 2); } 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, level + 2); } // Write the rdf:Description end tag. writeIndent(level + 1); 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 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(); serializeCanonicalRDFProperty(node, false, true, indent + 1); for (Iterator iq = node.iterateQualifier(); iq.hasNext();) { XMPNode qualifier = (XMPNode) iq.next(); serializeCanonicalRDFProperty(qualifier, false, false, indent + 1); } } /** * Serializes one schema with all contained properties in pretty-printed * manner.<br> * Each schema's properties are written to a single * 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 * @param level * @throws IOException Forwarded writer exceptions * @throws XMPException */ private void serializeCanonicalRDFSchema(XMPNode schemaNode, int level) throws IOException, XMPException { // Write each of the schema's actual properties. for (Iterator it = schemaNode.iterateChildren(); it.hasNext();) { XMPNode propNode = (XMPNode) it.next(); serializeCanonicalRDFProperty(propNode, options.getUseCanonicalFormat(), false, level + 2); } } /** * 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); } } /** * Start the outer rdf:Description element, including all needed xmlns attributes. * Leave the element open so that the compact form can add property attributes. * * @throws IOException If the writing to */ private void startOuterRDFDescription(XMPNode schemaNode, int level) throws IOException { writeIndent(level + 1); write(RDF_SCHEMA_START); writeTreeName(); Set usedPrefixes = new HashSet(); usedPrefixes.add("xml"); usedPrefixes.add("rdf"); declareUsedNamespaces(schemaNode, usedPrefixes, level + 3); write('>'); writeNewline(); } /** * Write the </rdf:Description> end tag. */ private void endOuterRDFDescription(int level) throws IOException { writeIndent(level + 1); write(RDF_SCHEMA_END); writeNewline(); } /** * 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> (If no rdf:resource qualifier) * <rdf:Description> * ... Fields, same forms as top level properties * </rdf:Description> * </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:Description> * <rdf:value> ... Property "value" following the unqualified * forms ... </rdf:value> * ... Qualifiers looking like named struct fields * </rdf:Description> * </ns:QualifiedProperty> * </pre> * * </blockquote> * * @param node the property node * @param emitAsRDFValue property shall be rendered as attribute rather than tag * @param useCanonicalRDF use canonical form with inner description tag or * the compact form with rdf:ParseType="resource" attribute. * @param indent the current indent level * @throws IOException Forwards all writer exceptions. * @throws XMPException If "rdf:resource" and general qualifiers are mixed. */ private void serializeCanonicalRDFProperty( XMPNode node, boolean useCanonicalRDF, 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); } // Change serialization to canonical format with inner rdf:Description-tag // depending on option if (useCanonicalRDF) { write(">"); writeNewline(); indent++; writeIndent(indent); write(RDF_STRUCT_START); write(">"); } else { write(" rdf:parseType=\"Resource\">"); } writeNewline(); serializeCanonicalRDFProperty(node, useCanonicalRDF, true, indent + 1); for (Iterator it = node.iterateQualifier(); it.hasNext();) { XMPNode qualifier = (XMPNode) it.next(); if (!RDF_ATTR_QUALIFIER.contains(qualifier.getName())) { serializeCanonicalRDFProperty(qualifier, useCanonicalRDF, false, indent + 1); } } if (useCanonicalRDF) { writeIndent(indent); write(RDF_STRUCT_END); writeNewline(); indent--; } } 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(); serializeCanonicalRDFProperty(child, useCanonicalRDF, 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()) { // Change serialization to canonical format with inner rdf:Description-tag // if option is set if (useCanonicalRDF) { write(">"); writeNewline(); writeIndent(indent + 1); write(RDF_EMPTY_STRUCT); } else { write(" rdf:parseType=\"Resource\"/>"); emitEndTag = false; } writeNewline(); } else { // Change serialization to canonical format with inner rdf:Description-tag // if option is set if (useCanonicalRDF) { write(">"); writeNewline(); indent++; writeIndent(indent); write(RDF_STRUCT_START); write(">"); } else { write(" rdf:parseType=\"Resource\">"); } writeNewline(); for (Iterator it = node.iterateChildren(); it.hasNext();) { XMPNode child = (XMPNode) it.next(); serializeCanonicalRDFProperty(child, useCanonicalRDF, false, indent + 1); } if (useCanonicalRDF) { writeIndent(indent); write(RDF_STRUCT_END); writeNewline(); indent--; } } } 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 { if (value == null) { value = ""; } 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() && !node.getOptions().containsOneOf(PropertyOptions.SEPARATE_NODE) && !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()); } }