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}