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 }