001/*
002 * Copyright 2012-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.marshal.StreamMarshaller;
022import com.unboundid.scim.schema.AttributeDescriptor;
023import com.unboundid.scim.sdk.BulkOperation;
024import com.unboundid.scim.sdk.Debug;
025import com.unboundid.scim.sdk.Resources;
026import com.unboundid.scim.sdk.SCIMAttribute;
027import com.unboundid.scim.sdk.SCIMAttributeValue;
028import com.unboundid.scim.sdk.SCIMConstants;
029import com.unboundid.scim.sdk.SCIMException;
030import com.unboundid.scim.sdk.ServerErrorException;
031import com.unboundid.scim.sdk.StaticUtils;
032
033import javax.xml.XMLConstants;
034import javax.xml.bind.DatatypeConverter;
035import javax.xml.stream.XMLOutputFactory;
036import javax.xml.stream.XMLStreamException;
037import javax.xml.stream.XMLStreamWriter;
038import java.io.IOException;
039import java.io.OutputStream;
040import java.util.Collections;
041import java.util.List;
042import java.util.Set;
043import java.util.concurrent.atomic.AtomicBoolean;
044
045
046/**
047 * This class provides a stream marshaller implementation to write a stream of
048 * SCIM objects to their XML representation.
049 */
050public class XmlStreamMarshaller implements StreamMarshaller
051{
052  private static final String xsiURI =
053      XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI;
054
055  private final OutputStream outputStream;
056  private final XMLStreamWriter xmlStreamWriter;
057
058
059
060  /**
061   * Create a new XML marshaller that writes to the provided output stream.
062   * The resulting marshaller must be closed after use.
063   *
064   * @param outputStream  The output stream to be written by this marshaller.
065   *
066   * @throws SCIMException  If the marshaller could not be created.
067   */
068  public XmlStreamMarshaller(final OutputStream outputStream)
069      throws SCIMException
070  {
071    this.outputStream = outputStream;
072
073    try
074    {
075      final XMLOutputFactory outputFactory = XMLOutputFactory.newInstance();
076      xmlStreamWriter =
077          outputFactory.createXMLStreamWriter(outputStream, "UTF-8");
078    }
079    catch (Exception e)
080    {
081      Debug.debugException(e);
082      throw new ServerErrorException(
083          "Cannot create XML marshaller: " + e.getMessage());
084    }
085  }
086
087
088
089  /**
090   * {@inheritDoc}
091   */
092  @Override
093  public void close() throws SCIMException
094  {
095    try
096    {
097      xmlStreamWriter.close();
098    }
099    catch (XMLStreamException e)
100    {
101      Debug.debugException(e);
102    }
103
104    try
105    {
106      outputStream.close();
107    }
108    catch (IOException e)
109    {
110      Debug.debugException(e);
111    }
112  }
113
114
115
116  /**
117   * {@inheritDoc}
118   */
119  public void marshal(final BaseResource resource)
120      throws SCIMException
121  {
122    try
123    {
124      xmlStreamWriter.writeStartDocument("UTF-8", "1.0");
125      xmlStreamWriter.setDefaultNamespace(SCIMConstants.SCHEMA_URI_CORE);
126
127      final String resourceSchemaURI =
128          resource.getResourceDescriptor().getSchema();
129
130      xmlStreamWriter.writeStartElement(
131          SCIMConstants.DEFAULT_SCHEMA_PREFIX,
132          resource.getResourceDescriptor().getName(), resourceSchemaURI);
133      marshal(resource, xmlStreamWriter, null);
134      xmlStreamWriter.writeEndElement();
135
136      xmlStreamWriter.writeEndDocument();
137    }
138    catch (XMLStreamException e)
139    {
140      Debug.debugException(e);
141      throw new ServerErrorException(
142          "Cannot write resource: " + e.getMessage());
143    }
144  }
145
146
147
148  /**
149   * {@inheritDoc}
150   */
151  public void marshal(final SCIMException response)
152      throws SCIMException
153  {
154    try
155    {
156      xmlStreamWriter.writeStartDocument("UTF-8", "1.0");
157
158      xmlStreamWriter.setPrefix(SCIMConstants.DEFAULT_SCHEMA_PREFIX,
159          SCIMConstants.SCHEMA_URI_CORE);
160      xmlStreamWriter.setPrefix("xsi", xsiURI);
161      xmlStreamWriter.writeStartElement(SCIMConstants.SCHEMA_URI_CORE,
162          "Response");
163      xmlStreamWriter.writeNamespace(SCIMConstants.DEFAULT_SCHEMA_PREFIX,
164          SCIMConstants.SCHEMA_URI_CORE);
165      xmlStreamWriter.writeNamespace("xsi", xsiURI);
166
167      xmlStreamWriter.writeStartElement(
168          SCIMConstants.SCHEMA_URI_CORE, "Errors");
169
170      xmlStreamWriter.writeStartElement(
171          SCIMConstants.SCHEMA_URI_CORE, "Error");
172
173      xmlStreamWriter.writeStartElement(
174          SCIMConstants.SCHEMA_URI_CORE, "code");
175      xmlStreamWriter.writeCharacters(String.valueOf(response.getStatusCode()));
176      xmlStreamWriter.writeEndElement();
177
178      final String description = response.getMessage();
179      if (description != null)
180      {
181        xmlStreamWriter.writeStartElement(
182            SCIMConstants.SCHEMA_URI_CORE, "description");
183        AtomicBoolean base64Encoded = new AtomicBoolean(false);
184        String cleanXML = cleanStringForXML(description, base64Encoded);
185        if (base64Encoded.get())
186        {
187          xmlStreamWriter.writeAttribute("base64Encoded", "true");
188        }
189        xmlStreamWriter.writeCharacters(cleanXML);
190        xmlStreamWriter.writeEndElement();
191      }
192
193      xmlStreamWriter.writeEndElement();
194      xmlStreamWriter.writeEndElement();
195      xmlStreamWriter.writeEndElement();
196      xmlStreamWriter.writeEndDocument();
197    }
198    catch (XMLStreamException e)
199    {
200      Debug.debugException(e);
201      throw new ServerErrorException(
202          "Cannot write error response: " + e.getMessage());
203    }
204  }
205
206
207
208  /**
209   * {@inheritDoc}
210   */
211  public void marshal(final Resources<? extends BaseResource> response)
212      throws SCIMException
213  {
214    try
215    {
216      xmlStreamWriter.writeStartDocument("UTF-8", "1.0");
217
218      xmlStreamWriter.setPrefix(SCIMConstants.DEFAULT_SCHEMA_PREFIX,
219          SCIMConstants.SCHEMA_URI_CORE);
220      xmlStreamWriter.setPrefix("xsi", xsiURI);
221      xmlStreamWriter.writeStartElement(SCIMConstants.SCHEMA_URI_CORE,
222          "Response");
223      xmlStreamWriter.writeNamespace(SCIMConstants.DEFAULT_SCHEMA_PREFIX,
224          SCIMConstants.SCHEMA_URI_CORE);
225      xmlStreamWriter.writeNamespace("xsi", xsiURI);
226
227      xmlStreamWriter.writeStartElement("totalResults");
228      xmlStreamWriter.writeCharacters(
229          Long.toString(response.getTotalResults()));
230      xmlStreamWriter.writeEndElement();
231
232      xmlStreamWriter.writeStartElement("itemsPerPage");
233      xmlStreamWriter.writeCharacters(
234          Integer.toString(response.getItemsPerPage()));
235      xmlStreamWriter.writeEndElement();
236
237      xmlStreamWriter.writeStartElement("startIndex");
238      xmlStreamWriter.writeCharacters(
239          Long.toString(response.getStartIndex()));
240      xmlStreamWriter.writeEndElement();
241
242      xmlStreamWriter.writeStartElement("Resources");
243
244      for (final BaseResource resource : response)
245      {
246        xmlStreamWriter.writeStartElement("Resource");
247        marshal(resource, xmlStreamWriter, xsiURI);
248        xmlStreamWriter.writeEndElement();
249      }
250
251      xmlStreamWriter.writeEndElement();
252
253      xmlStreamWriter.writeEndElement();
254      xmlStreamWriter.writeEndDocument();
255    }
256    catch (XMLStreamException e)
257    {
258      Debug.debugException(e);
259      throw new ServerErrorException(
260          "Cannot write resources: " + e.getMessage());
261    }
262  }
263
264
265
266  /**
267   * {@inheritDoc}
268   */
269  public void writeBulkStart(final int failOnErrors,
270                             final Set<String> schemaURIs)
271      throws SCIMException
272  {
273    try
274    {
275      xmlStreamWriter.writeStartDocument("UTF-8", "1.0");
276
277      xmlStreamWriter.setPrefix(SCIMConstants.DEFAULT_SCHEMA_PREFIX,
278          SCIMConstants.SCHEMA_URI_CORE);
279      xmlStreamWriter.setPrefix("xsi", xsiURI);
280      xmlStreamWriter.writeStartElement(SCIMConstants.SCHEMA_URI_CORE,
281          "Bulk");
282      xmlStreamWriter.writeNamespace(SCIMConstants.DEFAULT_SCHEMA_PREFIX,
283          SCIMConstants.SCHEMA_URI_CORE);
284      xmlStreamWriter.writeNamespace("xsi", xsiURI);
285
286      if (failOnErrors >= 0)
287      {
288        xmlStreamWriter.writeStartElement("failOnErrors");
289        xmlStreamWriter.writeCharacters(
290            Integer.toString(failOnErrors));
291        xmlStreamWriter.writeEndElement();
292      }
293
294      xmlStreamWriter.writeStartElement("Operations");
295    }
296    catch (XMLStreamException e)
297    {
298      Debug.debugException(e);
299      throw new ServerErrorException(
300          "Cannot write start of bulk operations: " + e.getMessage());
301    }
302  }
303
304
305
306  /**
307   * {@inheritDoc}
308   */
309  public void writeBulkOperation(final BulkOperation o)
310      throws SCIMException
311  {
312    try
313    {
314      xmlStreamWriter.writeStartElement("Operation");
315      if (o.getMethod() != null)
316      {
317        xmlStreamWriter.writeStartElement("method");
318        xmlStreamWriter.writeCharacters(o.getMethod().name());
319        xmlStreamWriter.writeEndElement();
320      }
321      if (o.getBulkId() != null)
322      {
323        xmlStreamWriter.writeStartElement("bulkId");
324        xmlStreamWriter.writeCharacters(o.getBulkId());
325        xmlStreamWriter.writeEndElement();
326      }
327      if (o.getVersion() != null)
328      {
329        xmlStreamWriter.writeStartElement("version");
330        xmlStreamWriter.writeCharacters(o.getVersion());
331        xmlStreamWriter.writeEndElement();
332      }
333      if (o.getPath() != null)
334      {
335        xmlStreamWriter.writeStartElement("path");
336        AtomicBoolean base64Encoded = new AtomicBoolean(false);
337        String cleanXML = cleanStringForXML(o.getPath(), base64Encoded);
338        if(base64Encoded.get())
339        {
340          xmlStreamWriter.writeAttribute("base64Encoded", "true");
341        }
342        xmlStreamWriter.writeCharacters(cleanXML);
343        xmlStreamWriter.writeEndElement();
344      }
345      if (o.getLocation() != null)
346      {
347        xmlStreamWriter.writeStartElement("location");
348        AtomicBoolean base64Encoded = new AtomicBoolean(false);
349        String cleanXML = cleanStringForXML(o.getLocation(), base64Encoded);
350        if(base64Encoded.get())
351        {
352          xmlStreamWriter.writeAttribute("base64Encoded", "true");
353        }
354        xmlStreamWriter.writeCharacters(cleanXML);
355        xmlStreamWriter.writeEndElement();
356      }
357      if (o.getData() != null)
358      {
359        xmlStreamWriter.writeStartElement("data");
360        marshal(o.getData(), xmlStreamWriter, xsiURI);
361        xmlStreamWriter.writeEndElement();
362      }
363      if (o.getStatus() != null)
364      {
365        xmlStreamWriter.writeStartElement("status");
366        xmlStreamWriter.writeStartElement("code");
367        xmlStreamWriter.writeCharacters(o.getStatus().getCode());
368        xmlStreamWriter.writeEndElement();
369        if (o.getStatus().getDescription() != null)
370        {
371          xmlStreamWriter.writeStartElement("description");
372
373          AtomicBoolean base64Encoded = new AtomicBoolean(false);
374          String cleanXML =
375               cleanStringForXML(o.getStatus().getDescription(), base64Encoded);
376          if(base64Encoded.get())
377          {
378            xmlStreamWriter.writeAttribute("base64Encoded", "true");
379          }
380          xmlStreamWriter.writeCharacters(cleanXML);
381          xmlStreamWriter.writeEndElement();
382        }
383        xmlStreamWriter.writeEndElement();
384      }
385      xmlStreamWriter.writeEndElement();
386    }
387    catch (XMLStreamException e)
388    {
389      Debug.debugException(e);
390      throw new ServerErrorException(
391          "Cannot write bulk operation: " + e.getMessage());
392    }
393  }
394
395
396  /**
397   * {@inheritDoc}
398   */
399  public void writeBulkFinish()
400      throws SCIMException
401  {
402    try
403    {
404      xmlStreamWriter.writeEndElement();
405      xmlStreamWriter.writeEndElement();
406      xmlStreamWriter.writeEndDocument();
407    }
408    catch (XMLStreamException e)
409    {
410      Debug.debugException(e);
411      throw new ServerErrorException(
412          "Cannot write end of bulk operations: " + e.getMessage());
413    }
414  }
415
416
417
418  /**
419   * {@inheritDoc}
420   */
421  public void bulkMarshal(final int failOnErrors,
422                          final List<BulkOperation> operations)
423      throws SCIMException
424  {
425    writeBulkStart(failOnErrors, Collections.<String>emptySet());
426    for (final BulkOperation o : operations)
427    {
428      writeBulkOperation(o);
429    }
430    writeBulkFinish();
431  }
432
433
434
435  /**
436   * Write a SCIM object to an XML stream.
437   *
438   * @param resource        The SCIM resource to be written.
439   * @param xmlStreamWriter The stream to which the SCIM object should be
440   *                        written.
441   * @param xsiURI          The xsi URI to use for the type attribute.
442   * @throws XMLStreamException If the object could not be written.
443   */
444  private void marshal(final BaseResource resource,
445                       final XMLStreamWriter xmlStreamWriter,
446                       final String xsiURI)
447    throws XMLStreamException
448  {
449    final String resourceSchemaURI =
450        resource.getResourceDescriptor().getSchema();
451
452    int i = 1;
453    for (final String schemaURI :
454        resource.getResourceDescriptor().getAttributeSchemas())
455    {
456      if (schemaURI.equalsIgnoreCase(resourceSchemaURI))
457      {
458        final String prefix = SCIMConstants.DEFAULT_SCHEMA_PREFIX;
459        xmlStreamWriter.setPrefix(prefix, schemaURI);
460        xmlStreamWriter.writeNamespace(prefix, schemaURI);
461      }
462      else if (resource.getScimObject().hasSchema(schemaURI))
463      {
464        final String prefix = "ns" + String.valueOf(i++);
465        xmlStreamWriter.setPrefix(prefix, schemaURI);
466        xmlStreamWriter.writeNamespace(prefix, schemaURI);
467      }
468    }
469
470    if (xsiURI != null)
471    {
472      xmlStreamWriter.writeAttribute(xsiURI, "type",
473          SCIMConstants.DEFAULT_SCHEMA_PREFIX + ':' +
474              resource.getResourceDescriptor().getName());
475    }
476
477    // Write the resource attributes in the order defined by the
478    // resource descriptor.
479    for (String schema : resource.getScimObject().getSchemas())
480    {
481      for (SCIMAttribute a : resource.getScimObject().getAttributes(schema))
482      {
483        if (a.getAttributeDescriptor().isMultiValued())
484        {
485          writeMultiValuedAttribute(a, xmlStreamWriter);
486        }
487        else
488        {
489          writeSingularAttribute(a, xmlStreamWriter);
490        }
491      }
492    }
493  }
494
495
496
497  /**
498   * Write a multi-valued attribute to an XML stream.
499   *
500   * @param scimAttribute   The attribute to be written.
501   * @param xmlStreamWriter The stream to which the attribute should be
502   *                        written.
503   * @throws XMLStreamException If the attribute could not be written.
504   */
505  private void writeMultiValuedAttribute(final SCIMAttribute scimAttribute,
506                                         final XMLStreamWriter xmlStreamWriter)
507    throws XMLStreamException
508  {
509    final SCIMAttributeValue[] values = scimAttribute.getValues();
510
511    writeStartElement(scimAttribute, xmlStreamWriter);
512
513    for (final SCIMAttributeValue value : values)
514    {
515      if (value == null)
516      {
517        continue;
518      }
519
520      writeChildStartElement(scimAttribute, xmlStreamWriter);
521
522      if (value.isComplex())
523      {
524        // Write the subordinate attributes in the order defined by the schema.
525        for (final AttributeDescriptor descriptor :
526            scimAttribute.getAttributeDescriptor().getSubAttributes())
527        {
528          final SCIMAttribute a = value.getAttribute(descriptor.getName());
529          if (a != null)
530          {
531            if (a.getAttributeDescriptor().isMultiValued())
532            {
533              writeMultiValuedAttribute(a, xmlStreamWriter);
534            }
535            else
536            {
537              writeSingularAttribute(a, xmlStreamWriter);
538            }
539          }
540        }
541      }
542      else
543      {
544        String stringValue = value.getStringValue();
545        AtomicBoolean base64Encoded = new AtomicBoolean(false);
546        String cleanXML = cleanStringForXML(stringValue, base64Encoded);
547        if(base64Encoded.get())
548        {
549          xmlStreamWriter.writeAttribute("base64Encoded", "true");
550        }
551        xmlStreamWriter.writeCharacters(cleanXML);
552      }
553      xmlStreamWriter.writeEndElement();
554    }
555
556    xmlStreamWriter.writeEndElement();
557  }
558
559
560
561  /**
562   * Write a singular attribute to an XML stream.
563   *
564   * @param scimAttribute   The attribute to be written.
565   * @param xmlStreamWriter The stream to which the attribute should be
566   *                        written.
567   * @throws XMLStreamException If the attribute could not be written.
568   */
569  private void writeSingularAttribute(final SCIMAttribute scimAttribute,
570                                      final XMLStreamWriter xmlStreamWriter)
571    throws XMLStreamException
572  {
573    final AttributeDescriptor attributeDescriptor =
574        scimAttribute.getAttributeDescriptor();
575
576    writeStartElement(scimAttribute, xmlStreamWriter);
577
578    final SCIMAttributeValue val = scimAttribute.getValue();
579
580    if (val.isComplex())
581    {
582      // Write the subordinate attributes in the order defined by the schema.
583      for (final AttributeDescriptor ad :
584          attributeDescriptor.getSubAttributes())
585      {
586        final SCIMAttribute a = val.getAttribute(ad.getName());
587        if (a != null)
588        {
589          if (ad.isMultiValued())
590          {
591            writeMultiValuedAttribute(a, xmlStreamWriter);
592          }
593          else
594          {
595            writeSingularAttribute(a, xmlStreamWriter);
596          }
597        }
598      }
599    }
600    else
601    {
602      String stringValue = scimAttribute.getValue().getStringValue();
603      AtomicBoolean base64Encoded = new AtomicBoolean(false);
604      String cleanXML = cleanStringForXML(stringValue, base64Encoded);
605      if(base64Encoded.get())
606      {
607        xmlStreamWriter.writeAttribute("base64Encoded", "true");
608      }
609      xmlStreamWriter.writeCharacters(cleanXML);
610    }
611
612    xmlStreamWriter.writeEndElement();
613  }
614
615
616
617  /**
618   * Helper that writes namespace when needed.
619   * @param scimAttribute Attribute tag to write.
620   * @param xmlStreamWriter Writer to write with.
621   * @throws XMLStreamException thrown if error writing the tag element.
622   */
623  private void writeStartElement(final SCIMAttribute scimAttribute,
624                                 final XMLStreamWriter xmlStreamWriter)
625    throws XMLStreamException
626  {
627    if (scimAttribute.getSchema().equalsIgnoreCase(
628        SCIMConstants.SCHEMA_URI_CORE))
629    {
630      xmlStreamWriter.writeStartElement(scimAttribute.getName());
631    }
632    else
633    {
634      xmlStreamWriter.writeStartElement(scimAttribute.getSchema(),
635        scimAttribute.getName());
636    }
637  }
638
639
640
641  /**
642   * Helper that writes namespace when needed.
643   * @param scimAttribute Attribute tag to write.
644   * @param xmlStreamWriter Writer to write with.
645   * @throws XMLStreamException thrown if error writing the tag element.
646   */
647  private void writeChildStartElement(final SCIMAttribute scimAttribute,
648                                      final XMLStreamWriter xmlStreamWriter)
649    throws XMLStreamException
650  {
651    if (scimAttribute.getSchema().equalsIgnoreCase(
652        SCIMConstants.SCHEMA_URI_CORE))
653    {
654      xmlStreamWriter.writeStartElement(scimAttribute.getAttributeDescriptor().
655          getMultiValuedChildName());
656    }
657    else
658    {
659      xmlStreamWriter.writeStartElement(scimAttribute.getSchema(),
660        scimAttribute.getAttributeDescriptor().getMultiValuedChildName());
661    }
662  }
663
664
665
666  /**
667   * This method determines whether the input string contains invalid XML
668   * unicode characters as specified by the XML 1.0 standard, and thus needs
669   * to be base-64 encoded. It also replaces any unicode characters greater
670   * than 0x7F with their decimal equivalent of their unicode code point
671   * (for example the Copyright sign (0xC2A9) becomes &#49833;)
672   *
673   * The returned string is either:
674   *
675   * 1) The base-64 encoding of the UTF-8 bytes of the original input string,
676   *    iff the original input string contained any invalid XML characters,
677   *
678   *  or
679   *
680   * 2) The original input string with any characters greater than 0x7f
681   *    replaced with the corresponding unicode code point.
682   *
683   * @param input The input string value to clean
684   * @param base64Encoded An output parameter indicating whether the returned
685   *                      string is base64-encoded.
686   * @return an XML-safe version of the input string, possibly base64-encoded
687   *         if the input contained invalid XML characters.
688   */
689  private static String cleanStringForXML(final String input,
690                                          final AtomicBoolean base64Encoded)
691  {
692    if (input == null || input.isEmpty())
693    {
694      return "";
695    }
696
697    //Buffer to hold the escaped output.
698    StringBuilder output = new StringBuilder(input.length());
699
700    char c;
701    for(int i = 0; i < input.length(); i++)
702    {
703      c = input.charAt(i);
704      if((c == 0x9) ||
705         (c == 0xA) ||
706         (c == 0xD) ||
707         ((c >= 0x20) && (c <= 0xD7FF)) ||
708         ((c >= 0xE000) && (c <= 0xFFFD)) ||
709         ((c >= 0x10000) && (c <= 0x10FFFF)))
710      {
711        //It's a valid XML character, now check if it needs escaping.
712        if (c > 0x7F)
713        {
714          output.append("&#").append(Integer.toString(c, 10)).append(";");
715        }
716        else
717        {
718          output.append(c);
719        }
720        continue;
721      }
722      else
723      {
724        //It's an invalid XML character, so base64-encode the whole thing.
725        if (base64Encoded != null)
726        {
727          base64Encoded.set(true);
728        }
729        return DatatypeConverter.printBase64Binary(
730                 StaticUtils.getUTF8Bytes(input));
731      }
732    }
733
734    if(base64Encoded != null)
735    {
736      base64Encoded.set(false);
737    }
738    return output.toString();
739  }
740}