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}