1 // ================================================================================================= 2 // ADOBE SYSTEMS INCORPORATED 3 // Copyright 2006 Adobe Systems Incorporated 4 // All Rights Reserved 5 // 6 // NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms 7 // of the Adobe license agreement accompanying it. 8 // ================================================================================================= 9 10 package com.adobe.xmp.impl; 11 12 import java.io.IOException; 13 import java.io.OutputStream; 14 import java.io.OutputStreamWriter; 15 import java.util.Arrays; 16 import java.util.HashSet; 17 import java.util.Iterator; 18 import java.util.Set; 19 20 import com.adobe.xmp.XMPConst; 21 import com.adobe.xmp.XMPError; 22 import com.adobe.xmp.XMPException; 23 import com.adobe.xmp.XMPMeta; 24 import com.adobe.xmp.XMPMetaFactory; 25 import com.adobe.xmp.options.SerializeOptions; 26 27 28 /** 29 * Serializes the <code>XMPMeta</code>-object using the standard RDF serialization format. 30 * The output is written to an <code>OutputStream</code> 31 * according to the <code>SerializeOptions</code>. 32 * 33 * @since 11.07.2006 34 */ 35 public class XMPSerializerRDF 36 { 37 /** default padding */ 38 private static final int DEFAULT_PAD = 2048; 39 /** */ 40 private static final String PACKET_HEADER = 41 "<?xpacket begin=\"\uFEFF\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?>"; 42 /** The w/r is missing inbetween */ 43 private static final String PACKET_TRAILER = "<?xpacket end=\""; 44 /** */ 45 private static final String PACKET_TRAILER2 = "\"?>"; 46 /** */ 47 private static final String RDF_XMPMETA_START = 48 "<x:xmpmeta xmlns:x=\"adobe:ns:meta/\" x:xmptk=\""; 49 /** */ 50 private static final String RDF_XMPMETA_END = "</x:xmpmeta>"; 51 /** */ 52 private static final String RDF_RDF_START = 53 "<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">"; 54 /** */ 55 private static final String RDF_RDF_END = "</rdf:RDF>"; 56 57 /** */ 58 private static final String RDF_SCHEMA_START = "<rdf:Description rdf:about="; 59 /** */ 60 private static final String RDF_SCHEMA_END = "</rdf:Description>"; 61 /** */ 62 private static final String RDF_STRUCT_START = "<rdf:Description"; 63 /** */ 64 private static final String RDF_STRUCT_END = "</rdf:Description>"; 65 /** a set of all rdf attribute qualifier */ 66 static final Set RDF_ATTR_QUALIFIER = new HashSet(Arrays.asList(new String[] { 67 XMPConst.XML_LANG, "rdf:resource", "rdf:ID", "rdf:bagID", "rdf:nodeID" })); 68 69 /** the metadata object to be serialized. */ 70 private XMPMetaImpl xmp; 71 /** the output stream to serialize to */ 72 private CountOutputStream outputStream; 73 /** this writer is used to do the actual serialisation */ 74 private OutputStreamWriter writer; 75 /** the stored serialisation options */ 76 private SerializeOptions options; 77 /** the size of one unicode char, for UTF-8 set to 1 78 * (Note: only valid for ASCII chars lower than 0x80), 79 * set to 2 in case of UTF-16 */ 80 private int unicodeSize = 1; // UTF-8 81 /** the padding in the XMP Packet, or the length of the complete packet in 82 * case of option <em>exactPacketLength</em>. */ 83 private int padding; 84 85 86 /** 87 * The actual serialisation. 88 * 89 * @param xmp the metadata object to be serialized 90 * @param out outputStream the output stream to serialize to 91 * @param options the serialization options 92 * 93 * @throws XMPException If case of wrong options or any other serialisaton error. 94 */ serialize(XMPMeta xmp, OutputStream out, SerializeOptions options)95 public void serialize(XMPMeta xmp, OutputStream out, 96 SerializeOptions options) throws XMPException 97 { 98 try 99 { 100 outputStream = new CountOutputStream(out); 101 writer = new OutputStreamWriter(outputStream, options.getEncoding()); 102 103 this.xmp = (XMPMetaImpl) xmp; 104 this.options = options; 105 this.padding = options.getPadding(); 106 107 writer = new OutputStreamWriter(outputStream, options.getEncoding()); 108 109 checkOptionsConsistence(); 110 111 // serializes the whole packet, but don't write the tail yet 112 // and flush to make sure that the written bytes are calculated correctly 113 String tailStr = serializeAsRDF(); 114 writer.flush(); 115 116 // adds padding 117 addPadding(tailStr.length()); 118 119 // writes the tail 120 write(tailStr); 121 writer.flush(); 122 123 outputStream.close(); 124 } 125 catch (IOException e) 126 { 127 throw new XMPException("Error writing to the OutputStream", XMPError.UNKNOWN); 128 } 129 } 130 131 132 /** 133 * Calulates the padding according to the options and write it to the stream. 134 * @param tailLength the length of the tail string 135 * @throws XMPException thrown if packet size is to small to fit the padding 136 * @throws IOException forwards writer errors 137 */ addPadding(int tailLength)138 private void addPadding(int tailLength) throws XMPException, IOException 139 { 140 if (options.getExactPacketLength()) 141 { 142 // the string length is equal to the length of the UTF-8 encoding 143 int minSize = outputStream.getBytesWritten() + tailLength * unicodeSize; 144 if (minSize > padding) 145 { 146 throw new XMPException("Can't fit into specified packet size", 147 XMPError.BADSERIALIZE); 148 } 149 padding -= minSize; // Now the actual amount of padding to add. 150 } 151 152 // fix rest of the padding according to Unicode unit size. 153 padding /= unicodeSize; 154 155 int newlineLen = options.getNewline().length(); 156 if (padding >= newlineLen) 157 { 158 padding -= newlineLen; // Write this newline last. 159 while (padding >= (100 + newlineLen)) 160 { 161 writeChars(100, ' '); 162 writeNewline(); 163 padding -= (100 + newlineLen); 164 } 165 writeChars(padding, ' '); 166 writeNewline(); 167 } 168 else 169 { 170 writeChars(padding, ' '); 171 } 172 } 173 174 175 /** 176 * Checks if the supplied options are consistent. 177 * @throws XMPException Thrown if options are conflicting 178 */ checkOptionsConsistence()179 protected void checkOptionsConsistence() throws XMPException 180 { 181 if (options.getEncodeUTF16BE() | options.getEncodeUTF16LE()) 182 { 183 unicodeSize = 2; 184 } 185 186 if (options.getExactPacketLength()) 187 { 188 if (options.getOmitPacketWrapper() | options.getIncludeThumbnailPad()) 189 { 190 throw new XMPException("Inconsistent options for exact size serialize", 191 XMPError.BADOPTIONS); 192 } 193 if ((options.getPadding() & (unicodeSize - 1)) != 0) 194 { 195 throw new XMPException("Exact size must be a multiple of the Unicode element", 196 XMPError.BADOPTIONS); 197 } 198 } 199 else if (options.getReadOnlyPacket()) 200 { 201 if (options.getOmitPacketWrapper() | options.getIncludeThumbnailPad()) 202 { 203 throw new XMPException("Inconsistent options for read-only packet", 204 XMPError.BADOPTIONS); 205 } 206 padding = 0; 207 } 208 else if (options.getOmitPacketWrapper()) 209 { 210 if (options.getIncludeThumbnailPad()) 211 { 212 throw new XMPException("Inconsistent options for non-packet serialize", 213 XMPError.BADOPTIONS); 214 } 215 padding = 0; 216 } 217 else 218 { 219 if (padding == 0) 220 { 221 padding = DEFAULT_PAD * unicodeSize; 222 } 223 224 if (options.getIncludeThumbnailPad()) 225 { 226 if (!xmp.doesPropertyExist(XMPConst.NS_XMP, "Thumbnails")) 227 { 228 padding += 10000 * unicodeSize; 229 } 230 } 231 } 232 } 233 234 235 /** 236 * Writes the (optional) packet header and the outer rdf-tags. 237 * @return Returns the packet end processing instraction to be written after the padding. 238 * @throws IOException Forwarded writer exceptions. 239 * @throws XMPException 240 */ serializeAsRDF()241 private String serializeAsRDF() throws IOException, XMPException 242 { 243 // Write the packet header PI. 244 if (!options.getOmitPacketWrapper()) 245 { 246 writeIndent(0); 247 write(PACKET_HEADER); 248 writeNewline(); 249 } 250 251 // Write the xmpmeta element's start tag. 252 writeIndent(0); 253 write(RDF_XMPMETA_START); 254 // Note: this flag can only be set by unit tests 255 if (!options.getOmitVersionAttribute()) 256 { 257 write(XMPMetaFactory.getVersionInfo().getMessage()); 258 } 259 write("\">"); 260 writeNewline(); 261 262 // Write the rdf:RDF start tag. 263 writeIndent(1); 264 write(RDF_RDF_START); 265 writeNewline(); 266 267 // Write all of the properties. 268 if (options.getUseCompactFormat()) 269 { 270 serializeCompactRDFSchemas(); 271 } 272 else 273 { 274 serializePrettyRDFSchemas(); 275 } 276 277 // Write the rdf:RDF end tag. 278 writeIndent(1); 279 write(RDF_RDF_END); 280 writeNewline(); 281 282 // Write the xmpmeta end tag. 283 writeIndent(0); 284 write(RDF_XMPMETA_END); 285 writeNewline(); 286 287 // Write the packet trailer PI into the tail string as UTF-8. 288 String tailStr = ""; 289 if (!options.getOmitPacketWrapper()) 290 { 291 for (int level = options.getBaseIndent(); level > 0; level--) 292 { 293 tailStr += options.getIndent(); 294 } 295 296 tailStr += PACKET_TRAILER; 297 tailStr += options.getReadOnlyPacket() ? 'r' : 'w'; 298 tailStr += PACKET_TRAILER2; 299 } 300 301 return tailStr; 302 } 303 304 305 /** 306 * Serializes the metadata in pretty-printed manner. 307 * @throws IOException Forwarded writer exceptions 308 * @throws XMPException 309 */ serializePrettyRDFSchemas()310 private void serializePrettyRDFSchemas() throws IOException, XMPException 311 { 312 if (xmp.getRoot().getChildrenLength() > 0) 313 { 314 for (Iterator it = xmp.getRoot().iterateChildren(); it.hasNext(); ) 315 { 316 XMPNode currSchema = (XMPNode) it.next(); 317 serializePrettyRDFSchema(currSchema); 318 } 319 } 320 else 321 { 322 writeIndent(2); 323 write(RDF_SCHEMA_START); // Special case an empty XMP object. 324 writeTreeName(); 325 write("/>"); 326 writeNewline(); 327 } 328 } 329 330 331 /** 332 * @throws IOException 333 */ writeTreeName()334 private void writeTreeName() throws IOException 335 { 336 write('"'); 337 String name = xmp.getRoot().getName(); 338 if (name != null) 339 { 340 appendNodeValue(name, true); 341 } 342 write('"'); 343 } 344 345 346 /** 347 * Serializes the metadata in compact manner. 348 * @throws IOException Forwarded writer exceptions 349 * @throws XMPException 350 */ serializeCompactRDFSchemas()351 private void serializeCompactRDFSchemas() throws IOException, XMPException 352 { 353 // Begin the rdf:Description start tag. 354 writeIndent(2); 355 write(RDF_SCHEMA_START); 356 writeTreeName(); 357 358 // Write all necessary xmlns attributes. 359 Set usedPrefixes = new HashSet(); 360 usedPrefixes.add("xml"); 361 usedPrefixes.add("rdf"); 362 363 for (Iterator it = xmp.getRoot().iterateChildren(); it.hasNext();) 364 { 365 XMPNode schema = (XMPNode) it.next(); 366 declareUsedNamespaces(schema, usedPrefixes, 4); 367 } 368 369 // Write the top level "attrProps" and close the rdf:Description start tag. 370 boolean allAreAttrs = true; 371 for (Iterator it = xmp.getRoot().iterateChildren(); it.hasNext();) 372 { 373 XMPNode schema = (XMPNode) it.next(); 374 allAreAttrs &= serializeCompactRDFAttrProps (schema, 3); 375 } 376 377 if (!allAreAttrs) 378 { 379 write('>'); 380 writeNewline(); 381 } 382 else 383 { 384 write("/>"); 385 writeNewline(); 386 return; // ! Done if all properties in all schema are written as attributes. 387 } 388 389 // Write the remaining properties for each schema. 390 for (Iterator it = xmp.getRoot().iterateChildren(); it.hasNext();) 391 { 392 XMPNode schema = (XMPNode) it.next(); 393 serializeCompactRDFElementProps (schema, 3); 394 } 395 396 // Write the rdf:Description end tag. 397 writeIndent(2); 398 write(RDF_SCHEMA_END); 399 writeNewline(); 400 } 401 402 403 404 /** 405 * Write each of the parent's simple unqualified properties as an attribute. Returns true if all 406 * of the properties are written as attributes. 407 * 408 * @param parentNode the parent property node 409 * @param indent the current indent level 410 * @return Returns true if all properties can be rendered as RDF attribute. 411 * @throws IOException 412 */ serializeCompactRDFAttrProps(XMPNode parentNode, int indent)413 private boolean serializeCompactRDFAttrProps(XMPNode parentNode, int indent) throws IOException 414 { 415 boolean allAreAttrs = true; 416 417 for (Iterator it = parentNode.iterateChildren(); it.hasNext();) 418 { 419 XMPNode prop = (XMPNode) it.next(); 420 421 if (canBeRDFAttrProp(prop)) 422 { 423 writeNewline(); 424 writeIndent(indent); 425 write(prop.getName()); 426 write("=\""); 427 appendNodeValue(prop.getValue(), true); 428 write('"'); 429 } 430 else 431 { 432 allAreAttrs = false; 433 } 434 } 435 return allAreAttrs; 436 } 437 438 439 /** 440 * Recursively handles the "value" for a node that must be written as an RDF 441 * property element. It does not matter if it is a top level property, a 442 * field of a struct, or an item of an array. The indent is that for the 443 * property element. The patterns bwlow ignore attribute qualifiers such as 444 * xml:lang, they don't affect the output form. 445 * 446 * <blockquote> 447 * 448 * <pre> 449 * <ns:UnqualifiedStructProperty-1 450 * ... The fields as attributes, if all are simple and unqualified 451 * /> 452 * 453 * <ns:UnqualifiedStructProperty-2 rdf:parseType="Resource"> 454 * ... The fields as elements, if none are simple and unqualified 455 * </ns:UnqualifiedStructProperty-2> 456 * 457 * <ns:UnqualifiedStructProperty-3> 458 * <rdf:Description 459 * ... The simple and unqualified fields as attributes 460 * > 461 * ... The compound or qualified fields as elements 462 * </rdf:Description> 463 * </ns:UnqualifiedStructProperty-3> 464 * 465 * <ns:UnqualifiedArrayProperty> 466 * <rdf:Bag> or Seq or Alt 467 * ... Array items as rdf:li elements, same forms as top level properties 468 * </rdf:Bag> 469 * </ns:UnqualifiedArrayProperty> 470 * 471 * <ns:QualifiedProperty rdf:parseType="Resource"> 472 * <rdf:value> ... Property "value" 473 * following the unqualified forms ... </rdf:value> 474 * ... Qualifiers looking like named struct fields 475 * </ns:QualifiedProperty> 476 * </pre> 477 * 478 * </blockquote> 479 * 480 * *** Consider numbered array items, but has compatibility problems. *** 481 * Consider qualified form with rdf:Description and attributes. 482 * 483 * @param parentNode the parent node 484 * @param indent the current indent level 485 * @throws IOException Forwards writer exceptions 486 * @throws XMPException If qualifier and element fields are mixed. 487 */ serializeCompactRDFElementProps(XMPNode parentNode, int indent)488 private void serializeCompactRDFElementProps(XMPNode parentNode, int indent) 489 throws IOException, XMPException 490 { 491 for (Iterator it = parentNode.iterateChildren(); it.hasNext();) 492 { 493 XMPNode node = (XMPNode) it.next(); 494 if (canBeRDFAttrProp (node)) 495 { 496 continue; 497 } 498 499 boolean emitEndTag = true; 500 boolean indentEndTag = true; 501 502 // Determine the XML element name, write the name part of the start tag. Look over the 503 // qualifiers to decide on "normal" versus "rdf:value" form. Emit the attribute 504 // qualifiers at the same time. 505 String elemName = node.getName(); 506 if (XMPConst.ARRAY_ITEM_NAME.equals(elemName)) 507 { 508 elemName = "rdf:li"; 509 } 510 511 writeIndent(indent); 512 write('<'); 513 write(elemName); 514 515 boolean hasGeneralQualifiers = false; 516 boolean hasRDFResourceQual = false; 517 518 for (Iterator iq = node.iterateQualifier(); iq.hasNext();) 519 { 520 XMPNode qualifier = (XMPNode) iq.next(); 521 if (!RDF_ATTR_QUALIFIER.contains(qualifier.getName())) 522 { 523 hasGeneralQualifiers = true; 524 } 525 else 526 { 527 hasRDFResourceQual = "rdf:resource".equals(qualifier.getName()); 528 write(' '); 529 write(qualifier.getName()); 530 write("=\""); 531 appendNodeValue(qualifier.getValue(), true); 532 write('"'); 533 } 534 } 535 536 537 // Process the property according to the standard patterns. 538 if (hasGeneralQualifiers) 539 { 540 serializeCompactRDFGeneralQualifier(indent, node); 541 } 542 else 543 { 544 // This node has only attribute qualifiers. Emit as a property element. 545 if (!node.getOptions().isCompositeProperty()) 546 { 547 Object[] result = serializeCompactRDFSimpleProp(node); 548 emitEndTag = ((Boolean) result[0]).booleanValue(); 549 indentEndTag = ((Boolean) result[1]).booleanValue(); 550 } 551 else if (node.getOptions().isArray()) 552 { 553 serializeCompactRDFArrayProp(node, indent); 554 } 555 else 556 { 557 emitEndTag = serializeCompactRDFStructProp( 558 node, indent, hasRDFResourceQual); 559 } 560 561 } 562 563 // Emit the property element end tag. 564 if (emitEndTag) 565 { 566 if (indentEndTag) 567 { 568 writeIndent(indent); 569 } 570 write("</"); 571 write(elemName); 572 write('>'); 573 writeNewline(); 574 } 575 576 } 577 } 578 579 580 /** 581 * Serializes a simple property. 582 * 583 * @param node an XMPNode 584 * @return Returns an array containing the flags emitEndTag and indentEndTag. 585 * @throws IOException Forwards the writer exceptions. 586 */ serializeCompactRDFSimpleProp(XMPNode node)587 private Object[] serializeCompactRDFSimpleProp(XMPNode node) throws IOException 588 { 589 // This is a simple property. 590 Boolean emitEndTag = Boolean.TRUE; 591 Boolean indentEndTag = Boolean.TRUE; 592 593 if (node.getOptions().isURI()) 594 { 595 write(" rdf:resource=\""); 596 appendNodeValue(node.getValue(), true); 597 write("\"/>"); 598 writeNewline(); 599 emitEndTag = Boolean.FALSE; 600 } 601 else if (node.getValue() == null || node.getValue().length() == 0) 602 { 603 write("/>"); 604 writeNewline(); 605 emitEndTag = Boolean.FALSE; 606 } 607 else 608 { 609 write('>'); 610 appendNodeValue (node.getValue(), false); 611 indentEndTag = Boolean.FALSE; 612 } 613 614 return new Object[] {emitEndTag, indentEndTag}; 615 } 616 617 618 /** 619 * Serializes an array property. 620 * 621 * @param node an XMPNode 622 * @param indent the current indent level 623 * @throws IOException Forwards the writer exceptions. 624 * @throws XMPException If qualifier and element fields are mixed. 625 */ serializeCompactRDFArrayProp(XMPNode node, int indent)626 private void serializeCompactRDFArrayProp(XMPNode node, int indent) throws IOException, 627 XMPException 628 { 629 // This is an array. 630 write('>'); 631 writeNewline(); 632 emitRDFArrayTag (node, true, indent + 1); 633 634 if (node.getOptions().isArrayAltText()) 635 { 636 XMPNodeUtils.normalizeLangArray (node); 637 } 638 639 serializeCompactRDFElementProps(node, indent + 2); 640 641 emitRDFArrayTag(node, false, indent + 1); 642 } 643 644 645 /** 646 * Serializes a struct property. 647 * 648 * @param node an XMPNode 649 * @param indent the current indent level 650 * @param hasRDFResourceQual Flag if the element has resource qualifier 651 * @return Returns true if an end flag shall be emitted. 652 * @throws IOException Forwards the writer exceptions. 653 * @throws XMPException If qualifier and element fields are mixed. 654 */ serializeCompactRDFStructProp(XMPNode node, int indent, boolean hasRDFResourceQual)655 private boolean serializeCompactRDFStructProp(XMPNode node, int indent, 656 boolean hasRDFResourceQual) throws XMPException, IOException 657 { 658 // This must be a struct. 659 boolean hasAttrFields = false; 660 boolean hasElemFields = false; 661 boolean emitEndTag = true; 662 663 for (Iterator ic = node.iterateChildren(); ic.hasNext(); ) 664 { 665 XMPNode field = (XMPNode) ic.next(); 666 if (canBeRDFAttrProp(field)) 667 { 668 hasAttrFields = true; 669 } 670 else 671 { 672 hasElemFields = true; 673 } 674 675 if (hasAttrFields && hasElemFields) 676 { 677 break; // No sense looking further. 678 } 679 } 680 681 if (hasRDFResourceQual && hasElemFields) 682 { 683 throw new XMPException( 684 "Can't mix rdf:resource qualifier and element fields", 685 XMPError.BADRDF); 686 } 687 688 if (!node.hasChildren()) 689 { 690 // Catch an empty struct as a special case. The case 691 // below would emit an empty 692 // XML element, which gets reparsed as a simple property 693 // with an empty value. 694 write(" rdf:parseType=\"Resource\"/>"); 695 writeNewline(); 696 emitEndTag = false; 697 698 } 699 else if (!hasElemFields) 700 { 701 // All fields can be attributes, use the 702 // emptyPropertyElt form. 703 serializeCompactRDFAttrProps(node, indent + 1); 704 write("/>"); 705 writeNewline(); 706 emitEndTag = false; 707 708 } 709 else if (!hasAttrFields) 710 { 711 // All fields must be elements, use the 712 // parseTypeResourcePropertyElt form. 713 write(" rdf:parseType=\"Resource\">"); 714 writeNewline(); 715 serializeCompactRDFElementProps(node, indent + 1); 716 717 } 718 else 719 { 720 // Have a mix of attributes and elements, use an inner rdf:Description. 721 write('>'); 722 writeNewline(); 723 writeIndent(indent + 1); 724 write(RDF_STRUCT_START); 725 serializeCompactRDFAttrProps(node, indent + 2); 726 write(">"); 727 writeNewline(); 728 serializeCompactRDFElementProps(node, indent + 1); 729 writeIndent(indent + 1); 730 write(RDF_STRUCT_END); 731 writeNewline(); 732 } 733 return emitEndTag; 734 } 735 736 737 /** 738 * Serializes the general qualifier. 739 * @param node the root node of the subtree 740 * @param indent the current indent level 741 * @throws IOException Forwards all writer exceptions. 742 * @throws XMPException If qualifier and element fields are mixed. 743 */ serializeCompactRDFGeneralQualifier(int indent, XMPNode node)744 private void serializeCompactRDFGeneralQualifier(int indent, XMPNode node) 745 throws IOException, XMPException 746 { 747 // The node has general qualifiers, ones that can't be 748 // attributes on a property element. 749 // Emit using the qualified property pseudo-struct form. The 750 // value is output by a call 751 // to SerializePrettyRDFProperty with emitAsRDFValue set. 752 write(" rdf:parseType=\"Resource\">"); 753 writeNewline(); 754 755 serializePrettyRDFProperty(node, true, indent + 1); 756 757 for (Iterator iq = node.iterateQualifier(); iq.hasNext();) 758 { 759 XMPNode qualifier = (XMPNode) iq.next(); 760 serializePrettyRDFProperty(qualifier, false, indent + 1); 761 } 762 } 763 764 765 /** 766 * Serializes one schema with all contained properties in pretty-printed 767 * manner.<br> 768 * Each schema's properties are written in a separate 769 * rdf:Description element. All of the necessary namespaces are declared in 770 * the rdf:Description element. The baseIndent is the base level for the 771 * entire serialization, that of the x:xmpmeta element. An xml:lang 772 * qualifier is written as an attribute of the property start tag, not by 773 * itself forcing the qualified property form. 774 * 775 * <blockquote> 776 * 777 * <pre> 778 * <rdf:Description rdf:about="TreeName" xmlns:ns="URI" ... > 779 * 780 * ... The actual properties of the schema, see SerializePrettyRDFProperty 781 * 782 * <!-- ns1:Alias is aliased to ns2:Actual --> ... If alias comments are wanted 783 * 784 * </rdf:Description> 785 * </pre> 786 * 787 * </blockquote> 788 * 789 * @param schemaNode a schema node 790 * @throws IOException Forwarded writer exceptions 791 * @throws XMPException 792 */ serializePrettyRDFSchema(XMPNode schemaNode)793 private void serializePrettyRDFSchema(XMPNode schemaNode) throws IOException, XMPException 794 { 795 writeIndent(2); 796 write(RDF_SCHEMA_START); 797 writeTreeName(); 798 799 Set usedPrefixes = new HashSet(); 800 usedPrefixes.add("xml"); 801 usedPrefixes.add("rdf"); 802 803 declareUsedNamespaces(schemaNode, usedPrefixes, 4); 804 805 write('>'); 806 writeNewline(); 807 808 // Write each of the schema's actual properties. 809 for (Iterator it = schemaNode.iterateChildren(); it.hasNext();) 810 { 811 XMPNode propNode = (XMPNode) it.next(); 812 serializePrettyRDFProperty(propNode, false, 3); 813 } 814 815 // Write the rdf:Description end tag. 816 writeIndent(2); 817 write(RDF_SCHEMA_END); 818 writeNewline(); 819 } 820 821 822 /** 823 * Writes all used namespaces of the subtree in node to the output. 824 * The subtree is recursivly traversed. 825 * @param node the root node of the subtree 826 * @param usedPrefixes a set containing currently used prefixes 827 * @param indent the current indent level 828 * @throws IOException Forwards all writer exceptions. 829 */ declareUsedNamespaces(XMPNode node, Set usedPrefixes, int indent)830 private void declareUsedNamespaces(XMPNode node, Set usedPrefixes, int indent) 831 throws IOException 832 { 833 if (node.getOptions().isSchemaNode()) 834 { 835 // The schema node name is the URI, the value is the prefix. 836 String prefix = node.getValue().substring(0, node.getValue().length() - 1); 837 declareNamespace(prefix, node.getName(), usedPrefixes, indent); 838 } 839 else if (node.getOptions().isStruct()) 840 { 841 for (Iterator it = node.iterateChildren(); it.hasNext();) 842 { 843 XMPNode field = (XMPNode) it.next(); 844 declareNamespace(field.getName(), null, usedPrefixes, indent); 845 } 846 } 847 848 for (Iterator it = node.iterateChildren(); it.hasNext();) 849 { 850 XMPNode child = (XMPNode) it.next(); 851 declareUsedNamespaces(child, usedPrefixes, indent); 852 } 853 854 for (Iterator it = node.iterateQualifier(); it.hasNext();) 855 { 856 XMPNode qualifier = (XMPNode) it.next(); 857 declareNamespace(qualifier.getName(), null, usedPrefixes, indent); 858 declareUsedNamespaces(qualifier, usedPrefixes, indent); 859 } 860 } 861 862 863 /** 864 * Writes one namespace declaration to the output. 865 * @param prefix a namespace prefix (without colon) or a complete qname (when namespace == null) 866 * @param namespace the a namespace 867 * @param usedPrefixes a set containing currently used prefixes 868 * @param indent the current indent level 869 * @throws IOException Forwards all writer exceptions. 870 */ declareNamespace(String prefix, String namespace, Set usedPrefixes, int indent)871 private void declareNamespace(String prefix, String namespace, Set usedPrefixes, int indent) 872 throws IOException 873 { 874 if (namespace == null) 875 { 876 // prefix contains qname, extract prefix and lookup namespace with prefix 877 QName qname = new QName(prefix); 878 if (qname.hasPrefix()) 879 { 880 prefix = qname.getPrefix(); 881 // add colon for lookup 882 namespace = XMPMetaFactory.getSchemaRegistry().getNamespaceURI(prefix + ":"); 883 // prefix w/o colon 884 declareNamespace(prefix, namespace, usedPrefixes, indent); 885 } 886 else 887 { 888 return; 889 } 890 } 891 892 if (!usedPrefixes.contains(prefix)) 893 { 894 writeNewline(); 895 writeIndent(indent); 896 write("xmlns:"); 897 write(prefix); 898 write("=\""); 899 write(namespace); 900 write('"'); 901 usedPrefixes.add(prefix); 902 } 903 } 904 905 906 /** 907 * Recursively handles the "value" for a node. It does not matter if it is a 908 * top level property, a field of a struct, or an item of an array. The 909 * indent is that for the property element. An xml:lang qualifier is written 910 * as an attribute of the property start tag, not by itself forcing the 911 * qualified property form. The patterns below mostly ignore attribute 912 * qualifiers like xml:lang. Except for the one struct case, attribute 913 * qualifiers don't affect the output form. 914 * 915 * <blockquote> 916 * 917 * <pre> 918 * <ns:UnqualifiedSimpleProperty>value</ns:UnqualifiedSimpleProperty> 919 * 920 * <ns:UnqualifiedStructProperty rdf:parseType="Resource"> 921 * (If no rdf:resource qualifier) 922 * ... Fields, same forms as top level properties 923 * </ns:UnqualifiedStructProperty> 924 * 925 * <ns:ResourceStructProperty rdf:resource="URI" 926 * ... Fields as attributes 927 * > 928 * 929 * <ns:UnqualifiedArrayProperty> 930 * <rdf:Bag> or Seq or Alt 931 * ... Array items as rdf:li elements, same forms as top level properties 932 * </rdf:Bag> 933 * </ns:UnqualifiedArrayProperty> 934 * 935 * <ns:QualifiedProperty rdf:parseType="Resource"> 936 * <rdf:value> ... Property "value" following the unqualified 937 * forms ... </rdf:value> 938 * ... Qualifiers looking like named struct fields 939 * </ns:QualifiedProperty> 940 * </pre> 941 * 942 * </blockquote> 943 * 944 * @param node the property node 945 * @param emitAsRDFValue property shall be renderes as attribute rather than tag 946 * @param indent the current indent level 947 * @throws IOException Forwards all writer exceptions. 948 * @throws XMPException If "rdf:resource" and general qualifiers are mixed. 949 */ serializePrettyRDFProperty(XMPNode node, boolean emitAsRDFValue, int indent)950 private void serializePrettyRDFProperty(XMPNode node, boolean emitAsRDFValue, int indent) 951 throws IOException, XMPException 952 { 953 boolean emitEndTag = true; 954 boolean indentEndTag = true; 955 956 // Determine the XML element name. Open the start tag with the name and 957 // attribute qualifiers. 958 959 String elemName = node.getName(); 960 if (emitAsRDFValue) 961 { 962 elemName = "rdf:value"; 963 } 964 else if (XMPConst.ARRAY_ITEM_NAME.equals(elemName)) 965 { 966 elemName = "rdf:li"; 967 } 968 969 writeIndent(indent); 970 write('<'); 971 write(elemName); 972 973 boolean hasGeneralQualifiers = false; 974 boolean hasRDFResourceQual = false; 975 976 for (Iterator it = node.iterateQualifier(); it.hasNext();) 977 { 978 XMPNode qualifier = (XMPNode) it.next(); 979 if (!RDF_ATTR_QUALIFIER.contains(qualifier.getName())) 980 { 981 hasGeneralQualifiers = true; 982 } 983 else 984 { 985 hasRDFResourceQual = "rdf:resource".equals(qualifier.getName()); 986 if (!emitAsRDFValue) 987 { 988 write(' '); 989 write(qualifier.getName()); 990 write("=\""); 991 appendNodeValue(qualifier.getValue(), true); 992 write('"'); 993 } 994 } 995 } 996 997 // Process the property according to the standard patterns. 998 999 if (hasGeneralQualifiers && !emitAsRDFValue) 1000 { 1001 // This node has general, non-attribute, qualifiers. Emit using the 1002 // qualified property form. 1003 // ! The value is output by a recursive call ON THE SAME NODE with 1004 // emitAsRDFValue set. 1005 1006 if (hasRDFResourceQual) 1007 { 1008 throw new XMPException("Can't mix rdf:resource and general qualifiers", 1009 XMPError.BADRDF); 1010 } 1011 1012 write(" rdf:parseType=\"Resource\">"); 1013 writeNewline(); 1014 1015 serializePrettyRDFProperty(node, true, indent + 1); 1016 1017 for (Iterator it = node.iterateQualifier(); it.hasNext();) 1018 { 1019 XMPNode qualifier = (XMPNode) it.next(); 1020 if (!RDF_ATTR_QUALIFIER.contains(qualifier.getName())) 1021 { 1022 serializePrettyRDFProperty(qualifier, false, indent + 1); 1023 } 1024 } 1025 } 1026 else 1027 { 1028 // This node has no general qualifiers. Emit using an unqualified form. 1029 1030 if (!node.getOptions().isCompositeProperty()) 1031 { 1032 // This is a simple property. 1033 1034 if (node.getOptions().isURI()) 1035 { 1036 write(" rdf:resource=\""); 1037 appendNodeValue(node.getValue(), true); 1038 write("\"/>"); 1039 writeNewline(); 1040 emitEndTag = false; 1041 } 1042 else if (node.getValue() == null || "".equals(node.getValue())) 1043 { 1044 write("/>"); 1045 writeNewline(); 1046 emitEndTag = false; 1047 } 1048 else 1049 { 1050 write('>'); 1051 appendNodeValue(node.getValue(), false); 1052 indentEndTag = false; 1053 } 1054 } 1055 else if (node.getOptions().isArray()) 1056 { 1057 // This is an array. 1058 write('>'); 1059 writeNewline(); 1060 emitRDFArrayTag(node, true, indent + 1); 1061 if (node.getOptions().isArrayAltText()) 1062 { 1063 XMPNodeUtils.normalizeLangArray(node); 1064 } 1065 for (Iterator it = node.iterateChildren(); it.hasNext();) 1066 { 1067 XMPNode child = (XMPNode) it.next(); 1068 serializePrettyRDFProperty(child, false, indent + 2); 1069 } 1070 emitRDFArrayTag(node, false, indent + 1); 1071 1072 1073 } 1074 else if (!hasRDFResourceQual) 1075 { 1076 // This is a "normal" struct, use the rdf:parseType="Resource" form. 1077 if (!node.hasChildren()) 1078 { 1079 write(" rdf:parseType=\"Resource\"/>"); 1080 writeNewline(); 1081 emitEndTag = false; 1082 } 1083 else 1084 { 1085 write(" rdf:parseType=\"Resource\">"); 1086 writeNewline(); 1087 for (Iterator it = node.iterateChildren(); it.hasNext();) 1088 { 1089 XMPNode child = (XMPNode) it.next(); 1090 serializePrettyRDFProperty(child, false, indent + 1); 1091 } 1092 } 1093 } 1094 else 1095 { 1096 // This is a struct with an rdf:resource attribute, use the 1097 // "empty property element" form. 1098 for (Iterator it = node.iterateChildren(); it.hasNext();) 1099 { 1100 XMPNode child = (XMPNode) it.next(); 1101 if (!canBeRDFAttrProp(child)) 1102 { 1103 throw new XMPException("Can't mix rdf:resource and complex fields", 1104 XMPError.BADRDF); 1105 } 1106 writeNewline(); 1107 writeIndent(indent + 1); 1108 write(' '); 1109 write(child.getName()); 1110 write("=\""); 1111 appendNodeValue(child.getValue(), true); 1112 write('"'); 1113 } 1114 write("/>"); 1115 writeNewline(); 1116 emitEndTag = false; 1117 } 1118 } 1119 1120 // Emit the property element end tag. 1121 if (emitEndTag) 1122 { 1123 if (indentEndTag) 1124 { 1125 writeIndent(indent); 1126 } 1127 write("</"); 1128 write(elemName); 1129 write('>'); 1130 writeNewline(); 1131 } 1132 } 1133 1134 1135 /** 1136 * Writes the array start and end tags. 1137 * 1138 * @param arrayNode an array node 1139 * @param isStartTag flag if its the start or end tag 1140 * @param indent the current indent level 1141 * @throws IOException forwards writer exceptions 1142 */ emitRDFArrayTag(XMPNode arrayNode, boolean isStartTag, int indent)1143 private void emitRDFArrayTag(XMPNode arrayNode, boolean isStartTag, int indent) 1144 throws IOException 1145 { 1146 if (isStartTag || arrayNode.hasChildren()) 1147 { 1148 writeIndent(indent); 1149 write(isStartTag ? "<rdf:" : "</rdf:"); 1150 1151 if (arrayNode.getOptions().isArrayAlternate()) 1152 { 1153 write("Alt"); 1154 } 1155 else if (arrayNode.getOptions().isArrayOrdered()) 1156 { 1157 write("Seq"); 1158 } 1159 else 1160 { 1161 write("Bag"); 1162 } 1163 1164 if (isStartTag && !arrayNode.hasChildren()) 1165 { 1166 write("/>"); 1167 } 1168 else 1169 { 1170 write(">"); 1171 } 1172 1173 writeNewline(); 1174 } 1175 } 1176 1177 1178 /** 1179 * Serializes the node value in XML encoding. Its used for tag bodies and 1180 * attributes. <em>Note:</em> The attribute is always limited by quotes, 1181 * thats why <code>&apos;</code> is never serialized. <em>Note:</em> 1182 * Control chars are written unescaped, but if the user uses others than tab, LF 1183 * and CR the resulting XML will become invalid. 1184 * 1185 * @param value the value of the node 1186 * @param forAttribute flag if value is an attribute value 1187 * @throws IOException 1188 */ appendNodeValue(String value, boolean forAttribute)1189 private void appendNodeValue(String value, boolean forAttribute) throws IOException 1190 { 1191 write (Utils.escapeXML(value, forAttribute, true)); 1192 } 1193 1194 1195 /** 1196 * A node can be serialized as RDF-Attribute, if it meets the following conditions: 1197 * <ul> 1198 * <li>is not array item 1199 * <li>don't has qualifier 1200 * <li>is no URI 1201 * <li>is no composite property 1202 * </ul> 1203 * 1204 * @param node an XMPNode 1205 * @return Returns true if the node serialized as RDF-Attribute 1206 */ canBeRDFAttrProp(XMPNode node)1207 private boolean canBeRDFAttrProp(XMPNode node) 1208 { 1209 return 1210 !node.hasQualifier() && 1211 !node.getOptions().isURI() && 1212 !node.getOptions().isCompositeProperty() && 1213 !XMPConst.ARRAY_ITEM_NAME.equals(node.getName()); 1214 } 1215 1216 1217 /** 1218 * Writes indents and automatically includes the baseindend from the options. 1219 * @param times number of indents to write 1220 * @throws IOException forwards exception 1221 */ writeIndent(int times)1222 private void writeIndent(int times) throws IOException 1223 { 1224 for (int i = options.getBaseIndent() + times; i > 0; i--) 1225 { 1226 writer.write(options.getIndent()); 1227 } 1228 } 1229 1230 1231 /** 1232 * Writes a char to the output. 1233 * @param c a char 1234 * @throws IOException forwards writer exceptions 1235 */ write(int c)1236 private void write(int c) throws IOException 1237 { 1238 writer.write(c); 1239 } 1240 1241 1242 /** 1243 * Writes a String to the output. 1244 * @param str a String 1245 * @throws IOException forwards writer exceptions 1246 */ write(String str)1247 private void write(String str) throws IOException 1248 { 1249 writer.write(str); 1250 } 1251 1252 1253 /** 1254 * Writes an amount of chars, mostly spaces 1255 * @param number number of chars 1256 * @param c a char 1257 * @throws IOException 1258 */ writeChars(int number, char c)1259 private void writeChars(int number, char c) throws IOException 1260 { 1261 for (; number > 0; number--) 1262 { 1263 writer.write(c); 1264 } 1265 } 1266 1267 1268 /** 1269 * Writes a newline according to the options. 1270 * @throws IOException Forwards exception 1271 */ writeNewline()1272 private void writeNewline() throws IOException 1273 { 1274 writer.write(options.getNewline()); 1275 } 1276 }