001/* 002 * Copyright 2012-2013 UnboundID Corp. 003 * 004 * This program is free software; you can redistribute it and/or modify 005 * it under the terms of the GNU General Public License (GPLv2 only) 006 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 007 * as published by the Free Software Foundation. 008 * 009 * This program is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 012 * GNU General Public License for more details. 013 * 014 * You should have received a copy of the GNU General Public License 015 * along with this program; if not, see <http://www.gnu.org/licenses>. 016 */ 017 018package com.unboundid.scim.marshal.xml; 019 020import com.unboundid.scim.data.BaseResource; 021import com.unboundid.scim.marshal.StreamMarshaller; 022import com.unboundid.scim.schema.AttributeDescriptor; 023import com.unboundid.scim.sdk.BulkOperation; 024import com.unboundid.scim.sdk.Debug; 025import com.unboundid.scim.sdk.Resources; 026import com.unboundid.scim.sdk.SCIMAttribute; 027import com.unboundid.scim.sdk.SCIMAttributeValue; 028import com.unboundid.scim.sdk.SCIMConstants; 029import com.unboundid.scim.sdk.SCIMException; 030import com.unboundid.scim.sdk.ServerErrorException; 031import com.unboundid.scim.sdk.StaticUtils; 032 033import javax.xml.XMLConstants; 034import javax.xml.bind.DatatypeConverter; 035import javax.xml.stream.XMLOutputFactory; 036import javax.xml.stream.XMLStreamException; 037import javax.xml.stream.XMLStreamWriter; 038import java.io.IOException; 039import java.io.OutputStream; 040import java.util.Collections; 041import java.util.List; 042import java.util.Set; 043import java.util.concurrent.atomic.AtomicBoolean; 044 045 046/** 047 * This class provides a stream marshaller implementation to write a stream of 048 * SCIM objects to their XML representation. 049 */ 050public class XmlStreamMarshaller implements StreamMarshaller 051{ 052 private static final String xsiURI = 053 XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI; 054 055 private final OutputStream outputStream; 056 private final XMLStreamWriter xmlStreamWriter; 057 058 059 060 /** 061 * Create a new XML marshaller that writes to the provided output stream. 062 * The resulting marshaller must be closed after use. 063 * 064 * @param outputStream The output stream to be written by this marshaller. 065 * 066 * @throws SCIMException If the marshaller could not be created. 067 */ 068 public XmlStreamMarshaller(final OutputStream outputStream) 069 throws SCIMException 070 { 071 this.outputStream = outputStream; 072 073 try 074 { 075 final XMLOutputFactory outputFactory = XMLOutputFactory.newInstance(); 076 xmlStreamWriter = 077 outputFactory.createXMLStreamWriter(outputStream, "UTF-8"); 078 } 079 catch (Exception e) 080 { 081 Debug.debugException(e); 082 throw new ServerErrorException( 083 "Cannot create XML marshaller: " + e.getMessage()); 084 } 085 } 086 087 088 089 /** 090 * {@inheritDoc} 091 */ 092 @Override 093 public void close() throws SCIMException 094 { 095 try 096 { 097 xmlStreamWriter.close(); 098 } 099 catch (XMLStreamException e) 100 { 101 Debug.debugException(e); 102 } 103 104 try 105 { 106 outputStream.close(); 107 } 108 catch (IOException e) 109 { 110 Debug.debugException(e); 111 } 112 } 113 114 115 116 /** 117 * {@inheritDoc} 118 */ 119 public void marshal(final BaseResource resource) 120 throws SCIMException 121 { 122 try 123 { 124 xmlStreamWriter.writeStartDocument("UTF-8", "1.0"); 125 xmlStreamWriter.setDefaultNamespace(SCIMConstants.SCHEMA_URI_CORE); 126 127 final String resourceSchemaURI = 128 resource.getResourceDescriptor().getSchema(); 129 130 xmlStreamWriter.writeStartElement( 131 SCIMConstants.DEFAULT_SCHEMA_PREFIX, 132 resource.getResourceDescriptor().getName(), resourceSchemaURI); 133 marshal(resource, xmlStreamWriter, null); 134 xmlStreamWriter.writeEndElement(); 135 136 xmlStreamWriter.writeEndDocument(); 137 } 138 catch (XMLStreamException e) 139 { 140 Debug.debugException(e); 141 throw new ServerErrorException( 142 "Cannot write resource: " + e.getMessage()); 143 } 144 } 145 146 147 148 /** 149 * {@inheritDoc} 150 */ 151 public void marshal(final SCIMException response) 152 throws SCIMException 153 { 154 try 155 { 156 xmlStreamWriter.writeStartDocument("UTF-8", "1.0"); 157 158 xmlStreamWriter.setPrefix(SCIMConstants.DEFAULT_SCHEMA_PREFIX, 159 SCIMConstants.SCHEMA_URI_CORE); 160 xmlStreamWriter.setPrefix("xsi", xsiURI); 161 xmlStreamWriter.writeStartElement(SCIMConstants.SCHEMA_URI_CORE, 162 "Response"); 163 xmlStreamWriter.writeNamespace(SCIMConstants.DEFAULT_SCHEMA_PREFIX, 164 SCIMConstants.SCHEMA_URI_CORE); 165 xmlStreamWriter.writeNamespace("xsi", xsiURI); 166 167 xmlStreamWriter.writeStartElement( 168 SCIMConstants.SCHEMA_URI_CORE, "Errors"); 169 170 xmlStreamWriter.writeStartElement( 171 SCIMConstants.SCHEMA_URI_CORE, "Error"); 172 173 xmlStreamWriter.writeStartElement( 174 SCIMConstants.SCHEMA_URI_CORE, "code"); 175 xmlStreamWriter.writeCharacters(String.valueOf(response.getStatusCode())); 176 xmlStreamWriter.writeEndElement(); 177 178 final String description = response.getMessage(); 179 if (description != null) 180 { 181 xmlStreamWriter.writeStartElement( 182 SCIMConstants.SCHEMA_URI_CORE, "description"); 183 AtomicBoolean base64Encoded = new AtomicBoolean(false); 184 String cleanXML = cleanStringForXML(description, base64Encoded); 185 if (base64Encoded.get()) 186 { 187 xmlStreamWriter.writeAttribute("base64Encoded", "true"); 188 } 189 xmlStreamWriter.writeCharacters(cleanXML); 190 xmlStreamWriter.writeEndElement(); 191 } 192 193 xmlStreamWriter.writeEndElement(); 194 xmlStreamWriter.writeEndElement(); 195 xmlStreamWriter.writeEndElement(); 196 xmlStreamWriter.writeEndDocument(); 197 } 198 catch (XMLStreamException e) 199 { 200 Debug.debugException(e); 201 throw new ServerErrorException( 202 "Cannot write error response: " + e.getMessage()); 203 } 204 } 205 206 207 208 /** 209 * {@inheritDoc} 210 */ 211 public void marshal(final Resources<? extends BaseResource> response) 212 throws SCIMException 213 { 214 try 215 { 216 xmlStreamWriter.writeStartDocument("UTF-8", "1.0"); 217 218 xmlStreamWriter.setPrefix(SCIMConstants.DEFAULT_SCHEMA_PREFIX, 219 SCIMConstants.SCHEMA_URI_CORE); 220 xmlStreamWriter.setPrefix("xsi", xsiURI); 221 xmlStreamWriter.writeStartElement(SCIMConstants.SCHEMA_URI_CORE, 222 "Response"); 223 xmlStreamWriter.writeNamespace(SCIMConstants.DEFAULT_SCHEMA_PREFIX, 224 SCIMConstants.SCHEMA_URI_CORE); 225 xmlStreamWriter.writeNamespace("xsi", xsiURI); 226 227 xmlStreamWriter.writeStartElement("totalResults"); 228 xmlStreamWriter.writeCharacters( 229 Long.toString(response.getTotalResults())); 230 xmlStreamWriter.writeEndElement(); 231 232 xmlStreamWriter.writeStartElement("itemsPerPage"); 233 xmlStreamWriter.writeCharacters( 234 Integer.toString(response.getItemsPerPage())); 235 xmlStreamWriter.writeEndElement(); 236 237 xmlStreamWriter.writeStartElement("startIndex"); 238 xmlStreamWriter.writeCharacters( 239 Long.toString(response.getStartIndex())); 240 xmlStreamWriter.writeEndElement(); 241 242 xmlStreamWriter.writeStartElement("Resources"); 243 244 for (final BaseResource resource : response) 245 { 246 xmlStreamWriter.writeStartElement("Resource"); 247 marshal(resource, xmlStreamWriter, xsiURI); 248 xmlStreamWriter.writeEndElement(); 249 } 250 251 xmlStreamWriter.writeEndElement(); 252 253 xmlStreamWriter.writeEndElement(); 254 xmlStreamWriter.writeEndDocument(); 255 } 256 catch (XMLStreamException e) 257 { 258 Debug.debugException(e); 259 throw new ServerErrorException( 260 "Cannot write resources: " + e.getMessage()); 261 } 262 } 263 264 265 266 /** 267 * {@inheritDoc} 268 */ 269 public void writeBulkStart(final int failOnErrors, 270 final Set<String> schemaURIs) 271 throws SCIMException 272 { 273 try 274 { 275 xmlStreamWriter.writeStartDocument("UTF-8", "1.0"); 276 277 xmlStreamWriter.setPrefix(SCIMConstants.DEFAULT_SCHEMA_PREFIX, 278 SCIMConstants.SCHEMA_URI_CORE); 279 xmlStreamWriter.setPrefix("xsi", xsiURI); 280 xmlStreamWriter.writeStartElement(SCIMConstants.SCHEMA_URI_CORE, 281 "Bulk"); 282 xmlStreamWriter.writeNamespace(SCIMConstants.DEFAULT_SCHEMA_PREFIX, 283 SCIMConstants.SCHEMA_URI_CORE); 284 xmlStreamWriter.writeNamespace("xsi", xsiURI); 285 286 if (failOnErrors >= 0) 287 { 288 xmlStreamWriter.writeStartElement("failOnErrors"); 289 xmlStreamWriter.writeCharacters( 290 Integer.toString(failOnErrors)); 291 xmlStreamWriter.writeEndElement(); 292 } 293 294 xmlStreamWriter.writeStartElement("Operations"); 295 } 296 catch (XMLStreamException e) 297 { 298 Debug.debugException(e); 299 throw new ServerErrorException( 300 "Cannot write start of bulk operations: " + e.getMessage()); 301 } 302 } 303 304 305 306 /** 307 * {@inheritDoc} 308 */ 309 public void writeBulkOperation(final BulkOperation o) 310 throws SCIMException 311 { 312 try 313 { 314 xmlStreamWriter.writeStartElement("Operation"); 315 if (o.getMethod() != null) 316 { 317 xmlStreamWriter.writeStartElement("method"); 318 xmlStreamWriter.writeCharacters(o.getMethod().name()); 319 xmlStreamWriter.writeEndElement(); 320 } 321 if (o.getBulkId() != null) 322 { 323 xmlStreamWriter.writeStartElement("bulkId"); 324 xmlStreamWriter.writeCharacters(o.getBulkId()); 325 xmlStreamWriter.writeEndElement(); 326 } 327 if (o.getVersion() != null) 328 { 329 xmlStreamWriter.writeStartElement("version"); 330 xmlStreamWriter.writeCharacters(o.getVersion()); 331 xmlStreamWriter.writeEndElement(); 332 } 333 if (o.getPath() != null) 334 { 335 xmlStreamWriter.writeStartElement("path"); 336 AtomicBoolean base64Encoded = new AtomicBoolean(false); 337 String cleanXML = cleanStringForXML(o.getPath(), base64Encoded); 338 if(base64Encoded.get()) 339 { 340 xmlStreamWriter.writeAttribute("base64Encoded", "true"); 341 } 342 xmlStreamWriter.writeCharacters(cleanXML); 343 xmlStreamWriter.writeEndElement(); 344 } 345 if (o.getLocation() != null) 346 { 347 xmlStreamWriter.writeStartElement("location"); 348 AtomicBoolean base64Encoded = new AtomicBoolean(false); 349 String cleanXML = cleanStringForXML(o.getLocation(), base64Encoded); 350 if(base64Encoded.get()) 351 { 352 xmlStreamWriter.writeAttribute("base64Encoded", "true"); 353 } 354 xmlStreamWriter.writeCharacters(cleanXML); 355 xmlStreamWriter.writeEndElement(); 356 } 357 if (o.getData() != null) 358 { 359 xmlStreamWriter.writeStartElement("data"); 360 marshal(o.getData(), xmlStreamWriter, xsiURI); 361 xmlStreamWriter.writeEndElement(); 362 } 363 if (o.getStatus() != null) 364 { 365 xmlStreamWriter.writeStartElement("status"); 366 xmlStreamWriter.writeStartElement("code"); 367 xmlStreamWriter.writeCharacters(o.getStatus().getCode()); 368 xmlStreamWriter.writeEndElement(); 369 if (o.getStatus().getDescription() != null) 370 { 371 xmlStreamWriter.writeStartElement("description"); 372 373 AtomicBoolean base64Encoded = new AtomicBoolean(false); 374 String cleanXML = 375 cleanStringForXML(o.getStatus().getDescription(), base64Encoded); 376 if(base64Encoded.get()) 377 { 378 xmlStreamWriter.writeAttribute("base64Encoded", "true"); 379 } 380 xmlStreamWriter.writeCharacters(cleanXML); 381 xmlStreamWriter.writeEndElement(); 382 } 383 xmlStreamWriter.writeEndElement(); 384 } 385 xmlStreamWriter.writeEndElement(); 386 } 387 catch (XMLStreamException e) 388 { 389 Debug.debugException(e); 390 throw new ServerErrorException( 391 "Cannot write bulk operation: " + e.getMessage()); 392 } 393 } 394 395 396 /** 397 * {@inheritDoc} 398 */ 399 public void writeBulkFinish() 400 throws SCIMException 401 { 402 try 403 { 404 xmlStreamWriter.writeEndElement(); 405 xmlStreamWriter.writeEndElement(); 406 xmlStreamWriter.writeEndDocument(); 407 } 408 catch (XMLStreamException e) 409 { 410 Debug.debugException(e); 411 throw new ServerErrorException( 412 "Cannot write end of bulk operations: " + e.getMessage()); 413 } 414 } 415 416 417 418 /** 419 * {@inheritDoc} 420 */ 421 public void bulkMarshal(final int failOnErrors, 422 final List<BulkOperation> operations) 423 throws SCIMException 424 { 425 writeBulkStart(failOnErrors, Collections.<String>emptySet()); 426 for (final BulkOperation o : operations) 427 { 428 writeBulkOperation(o); 429 } 430 writeBulkFinish(); 431 } 432 433 434 435 /** 436 * Write a SCIM object to an XML stream. 437 * 438 * @param resource The SCIM resource to be written. 439 * @param xmlStreamWriter The stream to which the SCIM object should be 440 * written. 441 * @param xsiURI The xsi URI to use for the type attribute. 442 * @throws XMLStreamException If the object could not be written. 443 */ 444 private void marshal(final BaseResource resource, 445 final XMLStreamWriter xmlStreamWriter, 446 final String xsiURI) 447 throws XMLStreamException 448 { 449 final String resourceSchemaURI = 450 resource.getResourceDescriptor().getSchema(); 451 452 int i = 1; 453 for (final String schemaURI : 454 resource.getResourceDescriptor().getAttributeSchemas()) 455 { 456 if (schemaURI.equalsIgnoreCase(resourceSchemaURI)) 457 { 458 final String prefix = SCIMConstants.DEFAULT_SCHEMA_PREFIX; 459 xmlStreamWriter.setPrefix(prefix, schemaURI); 460 xmlStreamWriter.writeNamespace(prefix, schemaURI); 461 } 462 else if (resource.getScimObject().hasSchema(schemaURI)) 463 { 464 final String prefix = "ns" + String.valueOf(i++); 465 xmlStreamWriter.setPrefix(prefix, schemaURI); 466 xmlStreamWriter.writeNamespace(prefix, schemaURI); 467 } 468 } 469 470 if (xsiURI != null) 471 { 472 xmlStreamWriter.writeAttribute(xsiURI, "type", 473 SCIMConstants.DEFAULT_SCHEMA_PREFIX + ':' + 474 resource.getResourceDescriptor().getName()); 475 } 476 477 // Write the resource attributes in the order defined by the 478 // resource descriptor. 479 for (String schema : resource.getScimObject().getSchemas()) 480 { 481 for (SCIMAttribute a : resource.getScimObject().getAttributes(schema)) 482 { 483 if (a.getAttributeDescriptor().isMultiValued()) 484 { 485 writeMultiValuedAttribute(a, xmlStreamWriter); 486 } 487 else 488 { 489 writeSingularAttribute(a, xmlStreamWriter); 490 } 491 } 492 } 493 } 494 495 496 497 /** 498 * Write a multi-valued attribute to an XML stream. 499 * 500 * @param scimAttribute The attribute to be written. 501 * @param xmlStreamWriter The stream to which the attribute should be 502 * written. 503 * @throws XMLStreamException If the attribute could not be written. 504 */ 505 private void writeMultiValuedAttribute(final SCIMAttribute scimAttribute, 506 final XMLStreamWriter xmlStreamWriter) 507 throws XMLStreamException 508 { 509 final SCIMAttributeValue[] values = scimAttribute.getValues(); 510 511 writeStartElement(scimAttribute, xmlStreamWriter); 512 513 for (final SCIMAttributeValue value : values) 514 { 515 if (value == null) 516 { 517 continue; 518 } 519 520 writeChildStartElement(scimAttribute, xmlStreamWriter); 521 522 if (value.isComplex()) 523 { 524 // Write the subordinate attributes in the order defined by the schema. 525 for (final AttributeDescriptor descriptor : 526 scimAttribute.getAttributeDescriptor().getSubAttributes()) 527 { 528 final SCIMAttribute a = value.getAttribute(descriptor.getName()); 529 if (a != null) 530 { 531 if (a.getAttributeDescriptor().isMultiValued()) 532 { 533 writeMultiValuedAttribute(a, xmlStreamWriter); 534 } 535 else 536 { 537 writeSingularAttribute(a, xmlStreamWriter); 538 } 539 } 540 } 541 } 542 else 543 { 544 String stringValue = value.getStringValue(); 545 AtomicBoolean base64Encoded = new AtomicBoolean(false); 546 String cleanXML = cleanStringForXML(stringValue, base64Encoded); 547 if(base64Encoded.get()) 548 { 549 xmlStreamWriter.writeAttribute("base64Encoded", "true"); 550 } 551 xmlStreamWriter.writeCharacters(cleanXML); 552 } 553 xmlStreamWriter.writeEndElement(); 554 } 555 556 xmlStreamWriter.writeEndElement(); 557 } 558 559 560 561 /** 562 * Write a singular attribute to an XML stream. 563 * 564 * @param scimAttribute The attribute to be written. 565 * @param xmlStreamWriter The stream to which the attribute should be 566 * written. 567 * @throws XMLStreamException If the attribute could not be written. 568 */ 569 private void writeSingularAttribute(final SCIMAttribute scimAttribute, 570 final XMLStreamWriter xmlStreamWriter) 571 throws XMLStreamException 572 { 573 final AttributeDescriptor attributeDescriptor = 574 scimAttribute.getAttributeDescriptor(); 575 576 writeStartElement(scimAttribute, xmlStreamWriter); 577 578 final SCIMAttributeValue val = scimAttribute.getValue(); 579 580 if (val.isComplex()) 581 { 582 // Write the subordinate attributes in the order defined by the schema. 583 for (final AttributeDescriptor ad : 584 attributeDescriptor.getSubAttributes()) 585 { 586 final SCIMAttribute a = val.getAttribute(ad.getName()); 587 if (a != null) 588 { 589 if (ad.isMultiValued()) 590 { 591 writeMultiValuedAttribute(a, xmlStreamWriter); 592 } 593 else 594 { 595 writeSingularAttribute(a, xmlStreamWriter); 596 } 597 } 598 } 599 } 600 else 601 { 602 String stringValue = scimAttribute.getValue().getStringValue(); 603 AtomicBoolean base64Encoded = new AtomicBoolean(false); 604 String cleanXML = cleanStringForXML(stringValue, base64Encoded); 605 if(base64Encoded.get()) 606 { 607 xmlStreamWriter.writeAttribute("base64Encoded", "true"); 608 } 609 xmlStreamWriter.writeCharacters(cleanXML); 610 } 611 612 xmlStreamWriter.writeEndElement(); 613 } 614 615 616 617 /** 618 * Helper that writes namespace when needed. 619 * @param scimAttribute Attribute tag to write. 620 * @param xmlStreamWriter Writer to write with. 621 * @throws XMLStreamException thrown if error writing the tag element. 622 */ 623 private void writeStartElement(final SCIMAttribute scimAttribute, 624 final XMLStreamWriter xmlStreamWriter) 625 throws XMLStreamException 626 { 627 if (scimAttribute.getSchema().equalsIgnoreCase( 628 SCIMConstants.SCHEMA_URI_CORE)) 629 { 630 xmlStreamWriter.writeStartElement(scimAttribute.getName()); 631 } 632 else 633 { 634 xmlStreamWriter.writeStartElement(scimAttribute.getSchema(), 635 scimAttribute.getName()); 636 } 637 } 638 639 640 641 /** 642 * Helper that writes namespace when needed. 643 * @param scimAttribute Attribute tag to write. 644 * @param xmlStreamWriter Writer to write with. 645 * @throws XMLStreamException thrown if error writing the tag element. 646 */ 647 private void writeChildStartElement(final SCIMAttribute scimAttribute, 648 final XMLStreamWriter xmlStreamWriter) 649 throws XMLStreamException 650 { 651 if (scimAttribute.getSchema().equalsIgnoreCase( 652 SCIMConstants.SCHEMA_URI_CORE)) 653 { 654 xmlStreamWriter.writeStartElement(scimAttribute.getAttributeDescriptor(). 655 getMultiValuedChildName()); 656 } 657 else 658 { 659 xmlStreamWriter.writeStartElement(scimAttribute.getSchema(), 660 scimAttribute.getAttributeDescriptor().getMultiValuedChildName()); 661 } 662 } 663 664 665 666 /** 667 * This method determines whether the input string contains invalid XML 668 * unicode characters as specified by the XML 1.0 standard, and thus needs 669 * to be base-64 encoded. It also replaces any unicode characters greater 670 * than 0x7F with their decimal equivalent of their unicode code point 671 * (for example the Copyright sign (0xC2A9) becomes 슩) 672 * 673 * The returned string is either: 674 * 675 * 1) The base-64 encoding of the UTF-8 bytes of the original input string, 676 * iff the original input string contained any invalid XML characters, 677 * 678 * or 679 * 680 * 2) The original input string with any characters greater than 0x7f 681 * replaced with the corresponding unicode code point. 682 * 683 * @param input The input string value to clean 684 * @param base64Encoded An output parameter indicating whether the returned 685 * string is base64-encoded. 686 * @return an XML-safe version of the input string, possibly base64-encoded 687 * if the input contained invalid XML characters. 688 */ 689 private static String cleanStringForXML(final String input, 690 final AtomicBoolean base64Encoded) 691 { 692 if (input == null || input.isEmpty()) 693 { 694 return ""; 695 } 696 697 //Buffer to hold the escaped output. 698 StringBuilder output = new StringBuilder(input.length()); 699 700 char c; 701 for(int i = 0; i < input.length(); i++) 702 { 703 c = input.charAt(i); 704 if((c == 0x9) || 705 (c == 0xA) || 706 (c == 0xD) || 707 ((c >= 0x20) && (c <= 0xD7FF)) || 708 ((c >= 0xE000) && (c <= 0xFFFD)) || 709 ((c >= 0x10000) && (c <= 0x10FFFF))) 710 { 711 //It's a valid XML character, now check if it needs escaping. 712 if (c > 0x7F) 713 { 714 output.append("&#").append(Integer.toString(c, 10)).append(";"); 715 } 716 else 717 { 718 output.append(c); 719 } 720 continue; 721 } 722 else 723 { 724 //It's an invalid XML character, so base64-encode the whole thing. 725 if (base64Encoded != null) 726 { 727 base64Encoded.set(true); 728 } 729 return DatatypeConverter.printBase64Binary( 730 StaticUtils.getUTF8Bytes(input)); 731 } 732 } 733 734 if(base64Encoded != null) 735 { 736 base64Encoded.set(false); 737 } 738 return output.toString(); 739 } 740}