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