001/*
002 * Copyright 2011-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.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        // Try to find the appropriate schema
158        namespaceURI =
159                resourceDescriptor.findAttributeSchema(element.getLocalName(),
160                                                       documentNamespaceURI);
161        if (namespaceURI == null)
162        {
163          // Fall back to this if we couldn't find it above
164          namespaceURI = documentNamespaceURI;
165        }
166      }
167
168      final AttributeDescriptor attributeDescriptor =
169          resourceDescriptor.getAttribute(namespaceURI, element.getLocalName());
170
171      final SCIMAttribute attr;
172      if (attributeDescriptor.isMultiValued())
173      {
174        attr = createMultiValuedAttribute(element, attributeDescriptor);
175      }
176      else if (attributeDescriptor.getDataType() ==
177          AttributeDescriptor.DataType.COMPLEX)
178      {
179        attr = SCIMAttribute.create(attributeDescriptor,
180            createComplexAttribute(element, attributeDescriptor));
181      }
182      else
183      {
184        attr = createSimpleAttribute(element, attributeDescriptor);
185      }
186
187      scimObject.addAttribute(attr);
188    }
189    return resourceFactory.createResource(resourceDescriptor, scimObject);
190  }
191
192  /**
193   * {@inheritDoc}
194   */
195  public <R extends BaseResource> Resources<R> unmarshalResources(
196      final InputStream inputStream,
197      final ResourceDescriptor resourceDescriptor,
198      final ResourceFactory<R> resourceFactory) throws InvalidResourceException
199  {
200    final Document doc;
201    try
202    {
203      doc = createDocumentBuilder().parse(inputStream);
204      doc.getDocumentElement().normalize();
205    }
206    catch (Exception e)
207    {
208      throw new InvalidResourceException("Error reading XML: " +
209          e.getMessage(), e);
210    }
211
212    final String documentNamespaceURI =
213        doc.getDocumentElement().getNamespaceURI();
214    final NodeList nodeList = doc.getElementsByTagName("*");
215
216    int totalResults = 0;
217    int startIndex = 1;
218    List<R> objects = Collections.emptyList();
219    for (int i = 0; i < nodeList.getLength(); i++)
220    {
221      final Node element = nodeList.item(i);
222      if(element.getLocalName().equals("totalResults"))
223      {
224        totalResults = Integer.valueOf(element.getTextContent());
225      }
226      else if(element.getLocalName().equals("startIndex"))
227      {
228        startIndex = Integer.valueOf(element.getTextContent());
229      }
230      else if(element.getLocalName().equals("Resources"))
231      {
232        NodeList resources = element.getChildNodes();
233        objects = new ArrayList<R>(resources.getLength());
234        for(int j = 0; j < resources.getLength(); j++)
235        {
236          Node resource = resources.item(j);
237          if(resource.getLocalName().equals("Resource"))
238          {
239            objects.add(
240                unmarshal(documentNamespaceURI, resource.getChildNodes(),
241                    resourceDescriptor, resourceFactory));
242          }
243        }
244      }
245    }
246
247    return new Resources<R>(objects, totalResults, startIndex);
248  }
249
250
251  /**
252   * {@inheritDoc}
253   */
254  public SCIMException unmarshalError(final InputStream inputStream)
255      throws InvalidResourceException
256  {
257    final Document doc;
258    try
259    {
260      doc = createDocumentBuilder().parse(inputStream);
261      doc.getDocumentElement().normalize();
262    }
263    catch (Exception e)
264    {
265      throw new InvalidResourceException("Error reading XML: " +
266          e.getMessage(), e);
267    }
268
269    final NodeList nodeList =
270        doc.getDocumentElement().getFirstChild().getChildNodes();
271
272    if(nodeList.getLength() >= 1)
273    {
274      String code = null;
275      String description = null;
276      NodeList nodes = nodeList.item(0).getChildNodes();
277      for(int j = 0; j < nodes.getLength(); j++)
278      {
279        Node attr = nodes.item(j);
280        if(attr.getLocalName().equals("code"))
281        {
282          code = attr.getTextContent();
283        }
284        else if(attr.getLocalName().equals("description"))
285        {
286          description = attr.getTextContent();
287        }
288      }
289      return SCIMException.createException(Integer.valueOf(code),
290          description);
291    }
292
293    return null;
294
295  }
296
297
298
299  /**
300   * {@inheritDoc}
301   */
302  public void bulkUnmarshal(final InputStream inputStream,
303                            final BulkConfig bulkConfig,
304                            final BulkContentHandler handler)
305      throws SCIMException
306  {
307    final XmlBulkParser xmlBulkParser =
308        new XmlBulkParser(inputStream, bulkConfig, handler);
309    xmlBulkParser.unmarshal();
310  }
311
312
313
314  /**
315   * {@inheritDoc}
316   */
317  public void bulkUnmarshal(final File file,
318                            final BulkConfig bulkConfig,
319                            final BulkContentHandler handler)
320      throws SCIMException
321  {
322    // First pass: ensure the number of operations is less than the max.
323    final BulkContentHandler preProcessHandler = new BulkContentHandler() {};
324    try
325    {
326      final FileInputStream fileInputStream = new FileInputStream(file);
327      try
328      {
329        final BufferedInputStream bufferedInputStream =
330            new BufferedInputStream(fileInputStream);
331        try
332        {
333          final XmlBulkParser xmlBulkParser =
334              new XmlBulkParser(bufferedInputStream, bulkConfig,
335                                preProcessHandler);
336          xmlBulkParser.setSkipOperations(true);
337          xmlBulkParser.unmarshal();
338        }
339        finally
340        {
341          bufferedInputStream.close();
342        }
343      }
344      finally
345      {
346        fileInputStream.close();
347      }
348    }
349    catch (IOException e)
350    {
351      Debug.debugException(e);
352      throw new ServerErrorException(
353          "Error pre-processing bulk request: " + e.getMessage());
354    }
355
356    // Second pass: Parse fully.
357    try
358    {
359      final FileInputStream fileInputStream = new FileInputStream(file);
360      try
361      {
362        final BufferedInputStream bufferedInputStream =
363            new BufferedInputStream(fileInputStream);
364        try
365        {
366          final XmlBulkParser xmlBulkParser =
367              new XmlBulkParser(bufferedInputStream, bulkConfig, handler);
368          xmlBulkParser.unmarshal();
369        }
370        finally
371        {
372          bufferedInputStream.close();
373        }
374      }
375      finally
376      {
377        fileInputStream.close();
378      }
379    }
380    catch (IOException e)
381    {
382      Debug.debugException(e);
383      throw new ServerErrorException(
384          "Error parsing bulk request: " + e.getMessage());
385    }
386  }
387
388
389  /**
390   * Parse a simple attribute from its representation as a DOM node.
391   *
392   * @param node                The DOM node representing the attribute.
393   * @param attributeDescriptor The attribute descriptor.
394   *
395   * @return The parsed attribute.
396   */
397  private SCIMAttribute createSimpleAttribute(
398      final Node node,
399      final AttributeDescriptor attributeDescriptor)
400  {
401    Node b64Node = node.getAttributes().getNamedItem("base64Encoded");
402    String textContent = node.getTextContent();
403    if(b64Node != null && Boolean.parseBoolean(b64Node.getTextContent()))
404    {
405      byte[] bytes = DatatypeConverter.parseBase64Binary(node.getTextContent());
406      try
407      {
408        textContent = new String(bytes, "UTF-8");
409      }
410      catch (UnsupportedEncodingException e)
411      {
412        //This should never happen with UTF-8.
413        Debug.debugException(e);
414      }
415    }
416    return SCIMAttribute.create(attributeDescriptor,
417            SCIMAttributeValue.createValue(attributeDescriptor.getDataType(),
418                    textContent));
419  }
420
421
422
423  /**
424   * Parse a multi-valued attribute from its representation as a DOM node.
425   *
426   * @param node                The DOM node representing the attribute.
427   * @param attributeDescriptor The attribute descriptor.
428   *
429   * @return The parsed attribute.
430   * @throws InvalidResourceException if an error occurs.
431   */
432  private SCIMAttribute createMultiValuedAttribute(
433      final Node node, final AttributeDescriptor attributeDescriptor)
434      throws InvalidResourceException
435  {
436    final NodeList attributes = node.getChildNodes();
437    final List<SCIMAttributeValue> values =
438        new ArrayList<SCIMAttributeValue>(attributes.getLength());
439    for (int i = 0; i < attributes.getLength(); i++)
440    {
441      final Node attribute = attributes.item(i);
442      if (attribute.getNodeType() != Node.ELEMENT_NODE ||
443          !attribute.getLocalName().equals(
444              attributeDescriptor.getMultiValuedChildName()))
445      {
446        continue;
447      }
448      if(attribute.getChildNodes().getLength() > 1 ||
449          attribute.getFirstChild().getNodeType() != Node.TEXT_NODE)
450      {
451        values.add(
452            createComplexAttribute(attribute, attributeDescriptor));
453      }
454      else
455      {
456        Node b64Node = attribute.getAttributes().getNamedItem("base64Encoded");
457        String textContent = attribute.getTextContent();
458        if(b64Node != null && Boolean.parseBoolean(b64Node.getTextContent()))
459        {
460          byte[] bytes = DatatypeConverter.parseBase64Binary(
461                            attribute.getTextContent());
462          try
463          {
464            textContent = new String(bytes, "UTF-8");
465          }
466          catch(UnsupportedEncodingException e)
467          {
468            //This should never happen with UTF-8.
469            Debug.debugException(e);
470          }
471        }
472
473        SCIMAttribute subAttr = SCIMAttribute.create(
474            attributeDescriptor.getSubAttribute("value"),
475                SCIMAttributeValue.createValue(
476                        attributeDescriptor.getDataType(), textContent));
477        values.add(SCIMAttributeValue.createComplexValue(subAttr));
478      }
479    }
480    SCIMAttributeValue[] vals = new SCIMAttributeValue[values.size()];
481    vals = values.toArray(vals);
482    return SCIMAttribute.create(attributeDescriptor, vals);
483  }
484
485
486
487  /**
488   * Parse a complex attribute from its representation as a DOM node.
489   *
490   * @param node                The DOM node representing the attribute.
491   * @param attributeDescriptor The attribute descriptor.
492   *
493   * @return The parsed attribute.
494   * @throws InvalidResourceException if an error occurs.
495   */
496  private SCIMAttributeValue createComplexAttribute(
497      final Node node, final AttributeDescriptor attributeDescriptor)
498      throws InvalidResourceException
499  {
500    NodeList childNodes = node.getChildNodes();
501    List<SCIMAttribute> complexAttrs =
502        new ArrayList<SCIMAttribute>(childNodes.getLength());
503    for (int i = 0; i < childNodes.getLength(); i++)
504    {
505      Node item1 = childNodes.item(i);
506      if (item1.getNodeType() == Node.ELEMENT_NODE)
507      {
508        if(item1.getNamespaceURI() != null &&
509            !item1.getNamespaceURI().equalsIgnoreCase(
510            attributeDescriptor.getSchema()))
511        {
512          // Sub-attributes should have the same namespace URI as the complex
513          // attribute.
514          throw new InvalidResourceException("Sub-attribute " +
515              item1.getNodeName() + " does not use the same namespace as the " +
516              "containing complex attribute " + attributeDescriptor.getName());
517        }
518        SCIMAttribute childAttr;
519        AttributeDescriptor subAttribute =
520            attributeDescriptor.getSubAttribute(item1.getLocalName());
521        // Allow multi-valued sub-attribute as the resource schema needs this.
522        if(subAttribute.isMultiValued())
523        {
524          childAttr = createMultiValuedAttribute(item1, subAttribute);
525        }
526        else
527        {
528          childAttr = createSimpleAttribute(item1, subAttribute);
529        }
530        complexAttrs.add(childAttr);
531      }
532    }
533
534    return SCIMAttributeValue.createComplexValue(complexAttrs);
535  }
536}