001/* 002 * Copyright 2011-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.data.BulkConfig; 022import com.unboundid.scim.data.ResourceFactory; 023import com.unboundid.scim.marshal.Unmarshaller; 024import com.unboundid.scim.schema.AttributeDescriptor; 025import com.unboundid.scim.schema.ResourceDescriptor; 026import com.unboundid.scim.sdk.BulkContentHandler; 027import com.unboundid.scim.sdk.Debug; 028import com.unboundid.scim.sdk.InvalidResourceException; 029import com.unboundid.scim.sdk.Resources; 030import com.unboundid.scim.sdk.SCIMAttribute; 031import com.unboundid.scim.sdk.SCIMAttributeValue; 032import com.unboundid.scim.sdk.SCIMException; 033import com.unboundid.scim.sdk.SCIMObject; 034import com.unboundid.scim.sdk.ServerErrorException; 035import org.w3c.dom.Document; 036import org.w3c.dom.Element; 037import org.w3c.dom.Node; 038import org.w3c.dom.NodeList; 039 040import javax.xml.bind.DatatypeConverter; 041import javax.xml.parsers.DocumentBuilder; 042import javax.xml.parsers.DocumentBuilderFactory; 043import java.io.BufferedInputStream; 044import java.io.File; 045import java.io.FileInputStream; 046import java.io.IOException; 047import java.io.InputStream; 048import java.io.UnsupportedEncodingException; 049import java.util.ArrayList; 050import java.util.Collections; 051import java.util.List; 052 053 054 055/** 056 * This class provides a SCIM object un-marshaller implementation to read SCIM 057 * objects from their XML representation. 058 */ 059public class XmlUnmarshaller implements Unmarshaller 060{ 061 /** 062 * Create a document builder that is better protected against attacks from 063 * XML bombs. 064 * 065 * @return A document builder. 066 * 067 * @throws javax.xml.parsers.ParserConfigurationException 068 * If the document builder could not be created. 069 */ 070 private DocumentBuilder createDocumentBuilder() 071 throws javax.xml.parsers.ParserConfigurationException 072 { 073 final DocumentBuilderFactory dbFactory = 074 DocumentBuilderFactory.newInstance(); 075 076 // Increase protection against XML bombs (DS-8081). 077 dbFactory.setFeature( 078 "http://apache.org/xml/features/disallow-doctype-decl", 079 true); 080 081 dbFactory.setNamespaceAware(true); 082 dbFactory.setIgnoringElementContentWhitespace(true); 083 dbFactory.setValidating(false); 084 085 return dbFactory.newDocumentBuilder(); 086 } 087 088 089 090 /** 091 * {@inheritDoc} 092 */ 093 public <R extends BaseResource> R unmarshal( 094 final InputStream inputStream, 095 final ResourceDescriptor resourceDescriptor, 096 final ResourceFactory<R> resourceFactory) 097 throws InvalidResourceException 098 { 099 final Document doc; 100 try 101 { 102 doc = createDocumentBuilder().parse(inputStream); 103 doc.getDocumentElement().normalize(); 104 } 105 catch (Exception e) 106 { 107 throw new InvalidResourceException("Error reading XML: " + 108 e.getMessage(), e); 109 } 110 111 final Element documentElement = doc.getDocumentElement(); 112 113 // TODO: Should we check to make sure the doc name matches the 114 // resource name? 115 //documentElement.getLocalName()); 116 if (resourceDescriptor == null) 117 { 118 throw new RuntimeException("No resource descriptor found for " + 119 documentElement.getLocalName()); 120 } 121 122 final String documentNamespaceURI = documentElement.getNamespaceURI(); 123 return unmarshal(documentNamespaceURI, documentElement.getChildNodes(), 124 resourceDescriptor, resourceFactory); 125 } 126 127 /** 128 * Read an SCIM resource from the specified node. 129 * 130 * @param documentNamespaceURI The namespace URI of XML document. 131 * @param <R> The type of resource instance. 132 * @param nodeList The attribute nodes to be read. 133 * @param resourceDescriptor The descriptor of the SCIM resource to be read. 134 * @param resourceFactory The resource factory to use to create the resource 135 * instance. 136 * 137 * @return The SCIM resource that was read. 138 * @throws com.unboundid.scim.sdk.InvalidResourceException if an error occurs. 139 */ 140 private <R extends BaseResource> R unmarshal( 141 final String documentNamespaceURI, 142 final NodeList nodeList, final ResourceDescriptor resourceDescriptor, 143 final ResourceFactory<R> resourceFactory) throws InvalidResourceException 144 { 145 SCIMObject scimObject = new SCIMObject(); 146 for (int i = 0; i < nodeList.getLength(); i++) 147 { 148 final Node element = nodeList.item(i); 149 if(element.getNodeType() != Node.ELEMENT_NODE) 150 { 151 continue; 152 } 153 154 String namespaceURI = element.getNamespaceURI(); 155 if (namespaceURI == null) 156 { 157 namespaceURI = documentNamespaceURI; // TODO: not sure about this 158 } 159 160 final AttributeDescriptor attributeDescriptor = 161 resourceDescriptor.getAttribute(namespaceURI, element.getLocalName()); 162 163 final SCIMAttribute attr; 164 if (attributeDescriptor.isMultiValued()) 165 { 166 attr = createMultiValuedAttribute(element, attributeDescriptor); 167 } 168 else if (attributeDescriptor.getDataType() == 169 AttributeDescriptor.DataType.COMPLEX) 170 { 171 attr = SCIMAttribute.create(attributeDescriptor, 172 createComplexAttribute(element, attributeDescriptor)); 173 } 174 else 175 { 176 attr = createSimpleAttribute(element, attributeDescriptor); 177 } 178 179 scimObject.addAttribute(attr); 180 } 181 return resourceFactory.createResource(resourceDescriptor, scimObject); 182 } 183 184 /** 185 * {@inheritDoc} 186 */ 187 public <R extends BaseResource> Resources<R> unmarshalResources( 188 final InputStream inputStream, 189 final ResourceDescriptor resourceDescriptor, 190 final ResourceFactory<R> resourceFactory) throws InvalidResourceException 191 { 192 final Document doc; 193 try 194 { 195 doc = createDocumentBuilder().parse(inputStream); 196 doc.getDocumentElement().normalize(); 197 } 198 catch (Exception e) 199 { 200 throw new InvalidResourceException("Error reading XML: " + 201 e.getMessage(), e); 202 } 203 204 final String documentNamespaceURI = 205 doc.getDocumentElement().getNamespaceURI(); 206 final NodeList nodeList = doc.getElementsByTagName("*"); 207 208 int totalResults = 0; 209 int startIndex = 1; 210 List<R> objects = Collections.emptyList(); 211 for (int i = 0; i < nodeList.getLength(); i++) 212 { 213 final Node element = nodeList.item(i); 214 if(element.getLocalName().equals("totalResults")) 215 { 216 totalResults = Integer.valueOf(element.getTextContent()); 217 } 218 else if(element.getLocalName().equals("startIndex")) 219 { 220 startIndex = Integer.valueOf(element.getTextContent()); 221 } 222 else if(element.getLocalName().equals("Resources")) 223 { 224 NodeList resources = element.getChildNodes(); 225 objects = new ArrayList<R>(resources.getLength()); 226 for(int j = 0; j < resources.getLength(); j++) 227 { 228 Node resource = resources.item(j); 229 if(resource.getLocalName().equals("Resource")) 230 { 231 objects.add( 232 unmarshal(documentNamespaceURI, resource.getChildNodes(), 233 resourceDescriptor, resourceFactory)); 234 } 235 } 236 } 237 } 238 239 return new Resources<R>(objects, totalResults, startIndex); 240 } 241 242 /** 243 * {@inheritDoc} 244 */ 245 public SCIMException unmarshalError(final InputStream inputStream) 246 throws InvalidResourceException 247 { 248 final Document doc; 249 try 250 { 251 doc = createDocumentBuilder().parse(inputStream); 252 doc.getDocumentElement().normalize(); 253 } 254 catch (Exception e) 255 { 256 throw new InvalidResourceException("Error reading XML: " + 257 e.getMessage(), e); 258 } 259 260 final NodeList nodeList = 261 doc.getDocumentElement().getFirstChild().getChildNodes(); 262 263 if(nodeList.getLength() >= 1) 264 { 265 String code = null; 266 String description = null; 267 NodeList nodes = nodeList.item(0).getChildNodes(); 268 for(int j = 0; j < nodes.getLength(); j++) 269 { 270 Node attr = nodes.item(j); 271 if(attr.getLocalName().equals("code")) 272 { 273 code = attr.getTextContent(); 274 } 275 else if(attr.getLocalName().equals("description")) 276 { 277 description = attr.getTextContent(); 278 } 279 } 280 return SCIMException.createException(Integer.valueOf(code), 281 description); 282 } 283 284 return null; 285 286 } 287 288 289 290 /** 291 * {@inheritDoc} 292 */ 293 public void bulkUnmarshal(final InputStream inputStream, 294 final BulkConfig bulkConfig, 295 final BulkContentHandler handler) 296 throws SCIMException 297 { 298 final XmlBulkParser xmlBulkParser = 299 new XmlBulkParser(inputStream, bulkConfig, handler); 300 xmlBulkParser.unmarshal(); 301 } 302 303 304 305 /** 306 * {@inheritDoc} 307 */ 308 public void bulkUnmarshal(final File file, 309 final BulkConfig bulkConfig, 310 final BulkContentHandler handler) 311 throws SCIMException 312 { 313 // First pass: ensure the number of operations is less than the max. 314 final BulkContentHandler preProcessHandler = new BulkContentHandler() {}; 315 try 316 { 317 final FileInputStream fileInputStream = new FileInputStream(file); 318 try 319 { 320 final BufferedInputStream bufferedInputStream = 321 new BufferedInputStream(fileInputStream); 322 try 323 { 324 final XmlBulkParser xmlBulkParser = 325 new XmlBulkParser(bufferedInputStream, bulkConfig, 326 preProcessHandler); 327 xmlBulkParser.setSkipOperations(true); 328 xmlBulkParser.unmarshal(); 329 } 330 finally 331 { 332 bufferedInputStream.close(); 333 } 334 } 335 finally 336 { 337 fileInputStream.close(); 338 } 339 } 340 catch (IOException e) 341 { 342 Debug.debugException(e); 343 throw new ServerErrorException( 344 "Error pre-processing bulk request: " + e.getMessage()); 345 } 346 347 // Second pass: Parse fully. 348 try 349 { 350 final FileInputStream fileInputStream = new FileInputStream(file); 351 try 352 { 353 final BufferedInputStream bufferedInputStream = 354 new BufferedInputStream(fileInputStream); 355 try 356 { 357 final XmlBulkParser xmlBulkParser = 358 new XmlBulkParser(bufferedInputStream, bulkConfig, handler); 359 xmlBulkParser.unmarshal(); 360 } 361 finally 362 { 363 bufferedInputStream.close(); 364 } 365 } 366 finally 367 { 368 fileInputStream.close(); 369 } 370 } 371 catch (IOException e) 372 { 373 Debug.debugException(e); 374 throw new ServerErrorException( 375 "Error parsing bulk request: " + e.getMessage()); 376 } 377 } 378 379 380 381 /** 382 * Parse a simple attribute from its representation as a DOM node. 383 * 384 * @param node The DOM node representing the attribute. 385 * @param attributeDescriptor The attribute descriptor. 386 * 387 * @return The parsed attribute. 388 */ 389 private SCIMAttribute createSimpleAttribute( 390 final Node node, 391 final AttributeDescriptor attributeDescriptor) 392 { 393 Node b64Node = node.getAttributes().getNamedItem("base64Encoded"); 394 String textContent = node.getTextContent(); 395 if(b64Node != null && Boolean.parseBoolean(b64Node.getTextContent())) 396 { 397 byte[] bytes = DatatypeConverter.parseBase64Binary(node.getTextContent()); 398 try 399 { 400 textContent = new String(bytes, "UTF-8"); 401 } 402 catch (UnsupportedEncodingException e) 403 { 404 //This should never happen with UTF-8. 405 Debug.debugException(e); 406 } 407 } 408 return SCIMAttribute.create(attributeDescriptor, 409 SCIMAttributeValue.createValue(attributeDescriptor.getDataType(), 410 textContent)); 411 } 412 413 414 415 /** 416 * Parse a multi-valued attribute from its representation as a DOM node. 417 * 418 * @param node The DOM node representing the attribute. 419 * @param attributeDescriptor The attribute descriptor. 420 * 421 * @return The parsed attribute. 422 * @throws InvalidResourceException if an error occurs. 423 */ 424 private SCIMAttribute createMultiValuedAttribute( 425 final Node node, final AttributeDescriptor attributeDescriptor) 426 throws InvalidResourceException 427 { 428 final NodeList attributes = node.getChildNodes(); 429 final List<SCIMAttributeValue> values = 430 new ArrayList<SCIMAttributeValue>(attributes.getLength()); 431 for (int i = 0; i < attributes.getLength(); i++) 432 { 433 final Node attribute = attributes.item(i); 434 if (attribute.getNodeType() != Node.ELEMENT_NODE || 435 !attribute.getLocalName().equals( 436 attributeDescriptor.getMultiValuedChildName())) 437 { 438 continue; 439 } 440 if(attribute.getChildNodes().getLength() > 1 || 441 attribute.getFirstChild().getNodeType() != Node.TEXT_NODE) 442 { 443 values.add( 444 createComplexAttribute(attribute, attributeDescriptor)); 445 } 446 else 447 { 448 Node b64Node = attribute.getAttributes().getNamedItem("base64Encoded"); 449 String textContent = attribute.getTextContent(); 450 if(b64Node != null && Boolean.parseBoolean(b64Node.getTextContent())) 451 { 452 byte[] bytes = DatatypeConverter.parseBase64Binary( 453 attribute.getTextContent()); 454 try 455 { 456 textContent = new String(bytes, "UTF-8"); 457 } 458 catch(UnsupportedEncodingException e) 459 { 460 //This should never happen with UTF-8. 461 Debug.debugException(e); 462 } 463 } 464 465 SCIMAttribute subAttr = SCIMAttribute.create( 466 attributeDescriptor.getSubAttribute("value"), 467 SCIMAttributeValue.createValue( 468 attributeDescriptor.getDataType(), textContent)); 469 values.add(SCIMAttributeValue.createComplexValue(subAttr)); 470 } 471 } 472 SCIMAttributeValue[] vals = new SCIMAttributeValue[values.size()]; 473 vals = values.toArray(vals); 474 return SCIMAttribute.create(attributeDescriptor, vals); 475 } 476 477 478 479 /** 480 * Parse a complex attribute from its representation as a DOM node. 481 * 482 * @param node The DOM node representing the attribute. 483 * @param attributeDescriptor The attribute descriptor. 484 * 485 * @return The parsed attribute. 486 * @throws InvalidResourceException if an error occurs. 487 */ 488 private SCIMAttributeValue createComplexAttribute( 489 final Node node, final AttributeDescriptor attributeDescriptor) 490 throws InvalidResourceException 491 { 492 NodeList childNodes = node.getChildNodes(); 493 List<SCIMAttribute> complexAttrs = 494 new ArrayList<SCIMAttribute>(childNodes.getLength()); 495 for (int i = 0; i < childNodes.getLength(); i++) 496 { 497 Node item1 = childNodes.item(i); 498 if (item1.getNodeType() == Node.ELEMENT_NODE) 499 { 500 if(item1.getNamespaceURI() != null && 501 !item1.getNamespaceURI().equalsIgnoreCase( 502 attributeDescriptor.getSchema())) 503 { 504 // Sub-attributes should have the same namespace URI as the complex 505 // attribute. 506 throw new InvalidResourceException("Sub-attribute " + 507 item1.getNodeName() + " does not use the same namespace as the " + 508 "containing complex attribute " + attributeDescriptor.getName()); 509 } 510 SCIMAttribute childAttr; 511 AttributeDescriptor subAttribute = 512 attributeDescriptor.getSubAttribute(item1.getLocalName()); 513 // Allow multi-valued sub-attribute as the resource schema needs this. 514 if(subAttribute.isMultiValued()) 515 { 516 childAttr = createMultiValuedAttribute(item1, subAttribute); 517 } 518 else 519 { 520 childAttr = createSimpleAttribute(item1, subAttribute); 521 } 522 complexAttrs.add(childAttr); 523 } 524 } 525 526 return SCIMAttributeValue.createComplexValue(complexAttrs); 527 } 528}