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.data.BulkConfig; 022import com.unboundid.scim.data.ResourceFactory; 023import com.unboundid.scim.marshal.BulkInputStreamWrapper; 024import com.unboundid.scim.schema.AttributeDescriptor; 025import com.unboundid.scim.schema.ResourceDescriptor; 026import com.unboundid.scim.sdk.BulkContentHandler; 027import com.unboundid.scim.sdk.BulkException; 028import com.unboundid.scim.sdk.BulkOperation; 029import com.unboundid.scim.sdk.BulkOperation.Method; 030import com.unboundid.scim.sdk.Debug; 031import com.unboundid.scim.sdk.InvalidResourceException; 032import com.unboundid.scim.sdk.SCIMAttribute; 033import com.unboundid.scim.sdk.SCIMAttributeValue; 034import com.unboundid.scim.sdk.SCIMException; 035import com.unboundid.scim.sdk.SCIMObject; 036import com.unboundid.scim.sdk.Status; 037 038import javax.xml.stream.XMLInputFactory; 039import javax.xml.stream.XMLStreamException; 040import javax.xml.stream.XMLStreamReader; 041import java.io.InputStream; 042import java.util.ArrayList; 043import java.util.List; 044 045import static javax.xml.stream.XMLStreamConstants.END_ELEMENT; 046import static javax.xml.stream.XMLStreamConstants.START_DOCUMENT; 047import static javax.xml.stream.XMLStreamConstants.START_ELEMENT; 048 049 050 051/** 052 * This class is a helper class to handle parsing of XML bulk operations. 053 */ 054public class XmlBulkParser 055{ 056 private final BulkInputStreamWrapper bulkInputStream; 057 private final BulkConfig bulkConfig; 058 private final BulkContentHandler handler; 059 private XMLStreamReader xmlStreamReader; 060 private int operationIndex = 0; 061 private String defaultNamespaceURI; 062 private boolean skipOperations; 063 064 /** 065 * Create a new instance of this bulk unmarshaller. 066 * 067 * @param inputStream The input stream containing the bulk content to be 068 * read. 069 * @param bulkConfig The bulk configuration settings to be enforced. 070 * @param handler A bulk operation listener to handle the content as it 071 * is read. 072 */ 073 public XmlBulkParser(final InputStream inputStream, 074 final BulkConfig bulkConfig, 075 final BulkContentHandler handler) 076 { 077 this.bulkInputStream = new BulkInputStreamWrapper(inputStream); 078 this.bulkConfig = bulkConfig; 079 this.handler = handler; 080 this.operationIndex = 0; 081 this.defaultNamespaceURI = null; 082 } 083 084 085 086 /** 087 * Specify whether bulk operations should be skipped. 088 * 089 * @param skipOperations {@code true} if bulk operations should be skipped. 090 */ 091 public void setSkipOperations(final boolean skipOperations) 092 { 093 this.skipOperations = skipOperations; 094 } 095 096 097 098 /** 099 * Reads a SCIM bulk request or response from the input stream. 100 * 101 * @throws SCIMException If the bulk content could not be read. 102 */ 103 public void unmarshal() 104 throws SCIMException 105 { 106 final XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance(); 107 try 108 { 109 // Increase protection against XML bombs (DS-8081). 110 xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); 111 112 xmlStreamReader = 113 xmlInputFactory.createXMLStreamReader(bulkInputStream, "UTF-8"); 114 try 115 { 116 xmlStreamReader.require(START_DOCUMENT, null, null); 117 118 while (xmlStreamReader.hasNext()) 119 { 120 switch (xmlStreamReader.next()) 121 { 122 case START_ELEMENT: 123 if (xmlStreamReader.getLocalName().equals("Bulk")) 124 { 125 if (xmlStreamReader.getNamespaceURI() != null) 126 { 127 defaultNamespaceURI = xmlStreamReader.getNamespaceURI(); 128 } 129 if (!parseBulk()) 130 { 131 return; 132 } 133 } 134 else 135 { 136 skipElement(); 137 } 138 break; 139 } 140 } 141 } 142 finally 143 { 144 xmlStreamReader.close(); 145 } 146 } 147 catch (SCIMException e) 148 { 149 throw e; 150 } 151 catch (Exception e) 152 { 153 Debug.debugException(e); 154 throw new InvalidResourceException("Error reading XML Bulk operation: " + 155 e.getMessage(), e); 156 } 157 } 158 159 160 161 /** 162 * Parse a Bulk element, and leave the reader positioned on the 163 * END_ELEMENT. 164 * 165 * @return {@code true} if operations should continue to be provided, 166 * or {@code false} if the remaining operations are of no interest. 167 * 168 * @throws XMLStreamException If the XML could not be parsed. 169 * @throws SCIMException If some other error occurred. 170 */ 171 private boolean parseBulk() 172 throws XMLStreamException, SCIMException 173 { 174 while (xmlStreamReader.hasNext()) 175 { 176 switch (xmlStreamReader.next()) 177 { 178 case START_ELEMENT: 179 if (xmlStreamReader.getLocalName().equals("failOnErrors")) 180 { 181 handler.handleFailOnErrors( 182 Integer.parseInt(xmlStreamReader.getElementText())); 183 } 184 else if (xmlStreamReader.getLocalName().equals("Operations")) 185 { 186 if (!parseOperations()) 187 { 188 return false; 189 } 190 } 191 else 192 { 193 skipElement(); 194 } 195 break; 196 197 case END_ELEMENT: 198 return true; 199 } 200 } 201 202 return true; 203 } 204 205 206 207 /** 208 * Parse an Operations element, and leave the reader positioned on the 209 * END_ELEMENT. 210 * 211 * @return {@code true} if operations should continue to be provided, 212 * or {@code false} if the remaining operations are of no interest. 213 * 214 * @throws XMLStreamException If the XML could not be parsed. 215 * @throws SCIMException If some other error occurred. 216 */ 217 private boolean parseOperations() 218 throws XMLStreamException, SCIMException 219 { 220 while (xmlStreamReader.hasNext()) 221 { 222 switch (xmlStreamReader.next()) 223 { 224 case START_ELEMENT: 225 if (xmlStreamReader.getLocalName().equals("Operation")) 226 { 227 if (operationIndex >= bulkConfig.getMaxOperations()) 228 { 229 throw SCIMException.createException( 230 413, 231 "The number of operations in the bulk operation exceeds " + 232 "maxOperations (" + bulkConfig.getMaxOperations() + ")"); 233 } 234 if (bulkInputStream.getBytesRead() > bulkConfig.getMaxPayloadSize()) 235 { 236 throw SCIMException.createException( 237 413, 238 "The size of the bulk operation exceeds the maxPayloadSize " + 239 "(" + bulkConfig.getMaxPayloadSize() + ")"); 240 } 241 if (skipOperations) 242 { 243 skipElement(); 244 } 245 else 246 { 247 try 248 { 249 handler.handleOperation(operationIndex, parseOperation()); 250 } 251 catch (BulkException e) 252 { 253 if(!handler.handleException(operationIndex, e)) 254 { 255 return false; 256 } 257 } 258 } 259 operationIndex++; 260 } 261 else 262 { 263 skipElement(); 264 } 265 break; 266 267 case END_ELEMENT: 268 return true; 269 } 270 } 271 272 return true; 273 } 274 275 276 277 /** 278 * Parse an Operation element, and leave the reader positioned on the 279 * END_ELEMENT. 280 * 281 * @return The parsed bulk operation. 282 * 283 * @throws BulkException If the operation cannot be parsed for some reason. 284 */ 285 private BulkOperation parseOperation() 286 throws BulkException 287 { 288 Method httpMethod = null; 289 String bulkId = null; 290 String version = null; 291 String path = null; 292 String location = null; 293 BaseResource resource = null; 294 Status status = null; 295 296 String endpoint = null; 297 298 loop: 299 try 300 { 301 while (xmlStreamReader.hasNext()) 302 { 303 switch (xmlStreamReader.next()) 304 { 305 case START_ELEMENT: 306 if (xmlStreamReader.getLocalName().equals("method")) 307 { 308 String method = xmlStreamReader.getElementText(); 309 try 310 { 311 httpMethod = Method.valueOf(method.toUpperCase()); 312 } 313 catch (IllegalArgumentException e) 314 { 315 //This will be handled later on in 316 //BulkContentHandler.handleOperation(). 317 } 318 } 319 else if (xmlStreamReader.getLocalName().equals("bulkId")) 320 { 321 bulkId = xmlStreamReader.getElementText(); 322 } 323 else if (xmlStreamReader.getLocalName().equals("version")) 324 { 325 version = xmlStreamReader.getElementText(); 326 } 327 else if (xmlStreamReader.getLocalName().equals("path")) 328 { 329 path = xmlStreamReader.getElementText(); 330 int startPos = 0; 331 if (path.charAt(startPos) == '/') 332 { 333 startPos++; 334 } 335 336 int endPos = path.indexOf('/', startPos); 337 if (endPos == -1) 338 { 339 endPos = path.length(); 340 } 341 342 endpoint = path.substring(startPos, endPos); 343 } 344 else if (xmlStreamReader.getLocalName().equals("location")) 345 { 346 location = xmlStreamReader.getElementText(); 347 } 348 else if (xmlStreamReader.getLocalName().equals("data")) 349 { 350 if (path == null) 351 { 352 throw new BulkException(new InvalidResourceException( 353 "Bulk operation " + operationIndex + " has data but no " + 354 "path"), 355 httpMethod, bulkId, path); 356 } 357 358 final ResourceDescriptor descriptor = 359 handler.getResourceDescriptor(endpoint); 360 if (descriptor == null) 361 { 362 throw new BulkException(new InvalidResourceException( 363 "Bulk operation " + operationIndex + " specifies an " + 364 "unknown resource endpoint '" + endpoint + "'"), 365 httpMethod, bulkId, path); 366 } 367 368 try 369 { 370 resource = parseData(descriptor, 371 BaseResource.BASE_RESOURCE_FACTORY); 372 } 373 catch (SCIMException e) 374 { 375 throw new BulkException(e, httpMethod, bulkId, path); 376 } 377 } 378 else if (xmlStreamReader.getLocalName().equals("status")) 379 { 380 try 381 { 382 status = parseStatus(); 383 } 384 catch (SCIMException e) 385 { 386 throw new BulkException(e, httpMethod, bulkId, path); 387 } 388 } 389 else 390 { 391 skipElement(); 392 } 393 break; 394 395 case END_ELEMENT: 396 break loop; 397 } 398 } 399 } 400 catch (XMLStreamException e) 401 { 402 throw new BulkException(new InvalidResourceException( 403 "Bulk operation " + operationIndex + " is malformed: " + 404 e.getMessage()), 405 httpMethod, bulkId, path); 406 } 407 408 return new BulkOperation(httpMethod, bulkId, version, path, location, 409 resource, status); 410 } 411 412 413 414 /** 415 * Parse a Status element, and leave the reader positioned on the 416 * END_ELEMENT. 417 * 418 * @return The parsed status. 419 * 420 * @throws XMLStreamException If the XML could not be parsed. 421 * @throws SCIMException If some other error occurred. 422 */ 423 private Status parseStatus() 424 throws XMLStreamException, SCIMException 425 { 426 String code = null; 427 String description = null; 428 429 loop: 430 while (xmlStreamReader.hasNext()) 431 { 432 switch (xmlStreamReader.next()) 433 { 434 case START_ELEMENT: 435 if (xmlStreamReader.getLocalName().equals("code")) 436 { 437 code = xmlStreamReader.getElementText(); 438 } 439 else if (xmlStreamReader.getLocalName().equals("description")) 440 { 441 description = xmlStreamReader.getElementText(); 442 } 443 else 444 { 445 skipElement(); 446 } 447 break; 448 449 case END_ELEMENT: 450 break loop; 451 } 452 } 453 454 return new Status(code, description); 455 } 456 457 458 459 /** 460 * Parse a data element, and leave the reader positioned on the 461 * END_ELEMENT. 462 * 463 * @param descriptor The resource descriptor for this data element. 464 * @param resourceFactory The resource factory to use to create the resource. 465 * 466 * @return The resource parsed from the data element. 467 * 468 * @throws XMLStreamException If the XML could not be parsed. 469 * @throws SCIMException If some other error occurred. 470 */ 471 private BaseResource parseData(final ResourceDescriptor descriptor, 472 final ResourceFactory resourceFactory) 473 throws XMLStreamException, SCIMException 474 { 475 final SCIMObject scimObject = new SCIMObject(); 476 477 loop: 478 while (xmlStreamReader.hasNext()) 479 { 480 switch (xmlStreamReader.next()) 481 { 482 case START_ELEMENT: 483 scimObject.addAttribute(parseAttribute(descriptor)); 484 break; 485 486 case END_ELEMENT: 487 break loop; 488 } 489 } 490 491 return resourceFactory.createResource(descriptor, scimObject); 492 } 493 494 495 496 /** 497 * Parse a SCIM attribute element, and leave the reader positioned on the 498 * END_ELEMENT. 499 * 500 * @param resourceDescriptor The resource descriptor for this attribute. 501 * 502 * @return The SCIM object parsed from the data element. 503 * 504 * @throws XMLStreamException If the XML could not be parsed. 505 * @throws SCIMException If some other error occurred. 506 */ 507 private SCIMAttribute parseAttribute( 508 final ResourceDescriptor resourceDescriptor) 509 throws XMLStreamException, SCIMException 510 { 511 String namespaceURI = xmlStreamReader.getNamespaceURI(); 512 if (namespaceURI == null) 513 { 514 namespaceURI = defaultNamespaceURI; 515 } 516 517 final AttributeDescriptor attributeDescriptor = 518 resourceDescriptor.getAttribute(namespaceURI, 519 xmlStreamReader.getLocalName()); 520 521 if (attributeDescriptor.isMultiValued()) 522 { 523 return parseMultiValuedAttribute(attributeDescriptor); 524 } 525 else if (attributeDescriptor.getDataType() == 526 AttributeDescriptor.DataType.COMPLEX) 527 { 528 return SCIMAttribute.create( 529 attributeDescriptor, 530 parseComplexAttributeValue(attributeDescriptor)); 531 } 532 else 533 { 534 return parseSimpleAttribute(attributeDescriptor); 535 } 536 } 537 538 539 540 /** 541 * Parse a SCIM simple attribute element, and leave the reader 542 * positioned on the END_ELEMENT. 543 * 544 * @param attributeDescriptor The attribute descriptor. 545 * 546 * @return The parsed attribute. 547 * 548 * @throws XMLStreamException If the XML could not be parsed. 549 * @throws SCIMException If some other error occurred. 550 */ 551 private SCIMAttribute parseSimpleAttribute( 552 final AttributeDescriptor attributeDescriptor) 553 throws XMLStreamException, SCIMException 554 { 555 return SCIMAttribute.create( 556 attributeDescriptor, 557 SCIMAttributeValue.createValue(attributeDescriptor.getDataType(), 558 handler.transformValue(operationIndex, 559 xmlStreamReader.getElementText()))); 560 } 561 562 563 564 /** 565 * Parse a SCIM multi-valued attribute element, and leave the reader 566 * positioned on the END_ELEMENT. 567 * 568 * @param attributeDescriptor The attribute descriptor. 569 * 570 * @return The parsed attribute. 571 * 572 * @throws XMLStreamException If the XML could not be parsed. 573 * @throws SCIMException If some other error occurred. 574 */ 575 private SCIMAttribute parseMultiValuedAttribute( 576 final AttributeDescriptor attributeDescriptor) 577 throws XMLStreamException, SCIMException 578 { 579 final List<SCIMAttributeValue> values = new ArrayList<SCIMAttributeValue>(); 580 581 loop: 582 while (xmlStreamReader.hasNext()) 583 { 584 switch (xmlStreamReader.next()) 585 { 586 case START_ELEMENT: 587 if (xmlStreamReader.getLocalName().equals( 588 attributeDescriptor.getMultiValuedChildName())) 589 { 590 values.add(parseComplexAttributeValue(attributeDescriptor)); 591 } 592 break; 593 594 case END_ELEMENT: 595 break loop; 596 } 597 } 598 599 SCIMAttributeValue[] vals = new SCIMAttributeValue[values.size()]; 600 return SCIMAttribute.create(attributeDescriptor, values.toArray(vals)); 601 } 602 603 604 605 /** 606 * Parse a SCIM complex attribute value element, and leave the reader 607 * positioned on the END_ELEMENT. 608 * 609 * @param attributeDescriptor The attribute descriptor. 610 * 611 * @return The parsed attribute. 612 * 613 * @throws XMLStreamException If the XML could not be parsed. 614 * @throws SCIMException If some other error occurred. 615 */ 616 private SCIMAttributeValue parseComplexAttributeValue( 617 final AttributeDescriptor attributeDescriptor) 618 throws XMLStreamException, SCIMException 619 { 620 List<SCIMAttribute> complexAttrs = new ArrayList<SCIMAttribute>(); 621 622 loop: 623 while (xmlStreamReader.hasNext()) 624 { 625 switch (xmlStreamReader.next()) 626 { 627 case START_ELEMENT: 628 if(xmlStreamReader.getNamespaceURI() != null && 629 !xmlStreamReader.getNamespaceURI().equalsIgnoreCase( 630 attributeDescriptor.getSchema())) 631 { 632 // Sub-attributes should have the same namespace URI as the complex 633 // attribute. 634 throw new InvalidResourceException("Sub-attribute " + 635 xmlStreamReader.getLocalName() + 636 " does not use the same namespace as the containing complex " + 637 "attribute " + attributeDescriptor.getName()); 638 } 639 640 final AttributeDescriptor subAttribute = 641 attributeDescriptor.getSubAttribute( 642 xmlStreamReader.getLocalName()); 643 644 // Allow multi-valued sub-attribute as the resource schema needs this. 645 final SCIMAttribute childAttr; 646 if (subAttribute.isMultiValued()) 647 { 648 childAttr = parseMultiValuedAttribute(subAttribute); 649 } 650 else 651 { 652 childAttr = parseSimpleAttribute(subAttribute); 653 } 654 complexAttrs.add(childAttr); 655 break; 656 657 case END_ELEMENT: 658 break loop; 659 } 660 } 661 662 return SCIMAttributeValue.createComplexValue(complexAttrs); 663 } 664 665 666 667 /** 668 * Skip over the current element, and leave the reader positioned on the 669 * END_ELEMENT. 670 * 671 * @throws XMLStreamException If the XML could not be parsed. 672 */ 673 private void skipElement() 674 throws XMLStreamException 675 { 676 int nesting = 1; 677 678 while (xmlStreamReader.hasNext()) 679 { 680 switch (xmlStreamReader.next()) 681 { 682 case START_ELEMENT: 683 nesting++; 684 break; 685 case END_ELEMENT: 686 if (--nesting == 0) 687 { 688 return; 689 } 690 break; 691 } 692 } 693 } 694}