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