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