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.marshal.StreamMarshaller;
022    import com.unboundid.scim.schema.AttributeDescriptor;
023    import com.unboundid.scim.sdk.BulkOperation;
024    import com.unboundid.scim.sdk.Debug;
025    import com.unboundid.scim.sdk.Resources;
026    import com.unboundid.scim.sdk.SCIMAttribute;
027    import com.unboundid.scim.sdk.SCIMAttributeValue;
028    import com.unboundid.scim.sdk.SCIMConstants;
029    import com.unboundid.scim.sdk.SCIMException;
030    import com.unboundid.scim.sdk.ServerErrorException;
031    
032    import javax.xml.XMLConstants;
033    import javax.xml.stream.XMLOutputFactory;
034    import javax.xml.stream.XMLStreamException;
035    import javax.xml.stream.XMLStreamWriter;
036    import java.io.IOException;
037    import java.io.OutputStream;
038    import java.util.Collections;
039    import java.util.List;
040    import java.util.Set;
041    
042    
043    
044    /**
045     * This class provides a stream marshaller implementation to write a stream of
046     * SCIM objects to their XML representation.
047     */
048    public class XmlStreamMarshaller implements StreamMarshaller
049    {
050      private static final String xsiURI =
051          XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI;
052    
053      private final OutputStream outputStream;
054      private final XMLStreamWriter xmlStreamWriter;
055    
056    
057    
058      /**
059       * Create a new XML marshaller that writes to the provided output stream.
060       * The resulting marshaller must be closed after use.
061       *
062       * @param outputStream  The output stream to be written by this marshaller.
063       *
064       * @throws SCIMException  If the marshaller could not be created.
065       */
066      public XmlStreamMarshaller(final OutputStream outputStream)
067          throws SCIMException
068      {
069        this.outputStream = outputStream;
070    
071        try
072        {
073          final XMLOutputFactory outputFactory = XMLOutputFactory.newInstance();
074          xmlStreamWriter =
075              outputFactory.createXMLStreamWriter(outputStream, "UTF-8");
076        }
077        catch (Exception e)
078        {
079          Debug.debugException(e);
080          throw new ServerErrorException(
081              "Cannot create XML marshaller: " + e.getMessage());
082        }
083      }
084    
085    
086    
087      /**
088       * {@inheritDoc}
089       */
090      @Override
091      public void close() throws SCIMException
092      {
093        try
094        {
095          xmlStreamWriter.close();
096        }
097        catch (XMLStreamException e)
098        {
099          Debug.debugException(e);
100        }
101    
102        try
103        {
104          outputStream.close();
105        }
106        catch (IOException e)
107        {
108          Debug.debugException(e);
109        }
110      }
111    
112    
113    
114      /**
115       * {@inheritDoc}
116       */
117      public void marshal(final BaseResource resource)
118          throws SCIMException
119      {
120        try
121        {
122          xmlStreamWriter.writeStartDocument("UTF-8", "1.0");
123          xmlStreamWriter.setDefaultNamespace(SCIMConstants.SCHEMA_URI_CORE);
124    
125          final String resourceSchemaURI =
126              resource.getResourceDescriptor().getSchema();
127    
128          xmlStreamWriter.writeStartElement(
129              SCIMConstants.DEFAULT_SCHEMA_PREFIX,
130              resource.getResourceDescriptor().getName(), resourceSchemaURI);
131          marshal(resource, xmlStreamWriter, null);
132          xmlStreamWriter.writeEndElement();
133    
134          xmlStreamWriter.writeEndDocument();
135        }
136        catch (XMLStreamException e)
137        {
138          Debug.debugException(e);
139          throw new ServerErrorException(
140              "Cannot write resource: " + e.getMessage());
141        }
142      }
143    
144    
145    
146      /**
147       * {@inheritDoc}
148       */
149      public void marshal(final SCIMException response)
150          throws SCIMException
151      {
152        try
153        {
154          xmlStreamWriter.writeStartDocument("UTF-8", "1.0");
155    
156          xmlStreamWriter.setPrefix(SCIMConstants.DEFAULT_SCHEMA_PREFIX,
157              SCIMConstants.SCHEMA_URI_CORE);
158          xmlStreamWriter.setPrefix("xsi", xsiURI);
159          xmlStreamWriter.writeStartElement(SCIMConstants.SCHEMA_URI_CORE,
160              "Response");
161          xmlStreamWriter.writeNamespace(SCIMConstants.DEFAULT_SCHEMA_PREFIX,
162              SCIMConstants.SCHEMA_URI_CORE);
163          xmlStreamWriter.writeNamespace("xsi", xsiURI);
164    
165          xmlStreamWriter.writeStartElement(
166              SCIMConstants.SCHEMA_URI_CORE, "Errors");
167    
168          xmlStreamWriter.writeStartElement(
169              SCIMConstants.SCHEMA_URI_CORE, "Error");
170    
171          xmlStreamWriter.writeStartElement(
172              SCIMConstants.SCHEMA_URI_CORE, "code");
173          xmlStreamWriter.writeCharacters(String.valueOf(response.getStatusCode()));
174          xmlStreamWriter.writeEndElement();
175    
176          final String description = response.getMessage();
177          if (description != null)
178          {
179            xmlStreamWriter.writeStartElement(
180                SCIMConstants.SCHEMA_URI_CORE, "description");
181            xmlStreamWriter.writeCharacters(description);
182            xmlStreamWriter.writeEndElement();
183          }
184    
185          xmlStreamWriter.writeEndElement();
186          xmlStreamWriter.writeEndElement();
187          xmlStreamWriter.writeEndElement();
188          xmlStreamWriter.writeEndDocument();
189        }
190        catch (XMLStreamException e)
191        {
192          Debug.debugException(e);
193          throw new ServerErrorException(
194              "Cannot write error response: " + e.getMessage());
195        }
196      }
197    
198    
199    
200      /**
201       * {@inheritDoc}
202       */
203      public void marshal(final Resources<? extends BaseResource> response)
204          throws SCIMException
205      {
206        try
207        {
208          xmlStreamWriter.writeStartDocument("UTF-8", "1.0");
209    
210          xmlStreamWriter.setPrefix(SCIMConstants.DEFAULT_SCHEMA_PREFIX,
211              SCIMConstants.SCHEMA_URI_CORE);
212          xmlStreamWriter.setPrefix("xsi", xsiURI);
213          xmlStreamWriter.writeStartElement(SCIMConstants.SCHEMA_URI_CORE,
214              "Response");
215          xmlStreamWriter.writeNamespace(SCIMConstants.DEFAULT_SCHEMA_PREFIX,
216              SCIMConstants.SCHEMA_URI_CORE);
217          xmlStreamWriter.writeNamespace("xsi", xsiURI);
218    
219          xmlStreamWriter.writeStartElement("totalResults");
220          xmlStreamWriter.writeCharacters(
221              Long.toString(response.getTotalResults()));
222          xmlStreamWriter.writeEndElement();
223    
224          xmlStreamWriter.writeStartElement("itemsPerPage");
225          xmlStreamWriter.writeCharacters(
226              Integer.toString(response.getItemsPerPage()));
227          xmlStreamWriter.writeEndElement();
228    
229          xmlStreamWriter.writeStartElement("startIndex");
230          xmlStreamWriter.writeCharacters(
231              Long.toString(response.getStartIndex()));
232          xmlStreamWriter.writeEndElement();
233    
234          xmlStreamWriter.writeStartElement("Resources");
235    
236          for (final BaseResource resource : response)
237          {
238            xmlStreamWriter.writeStartElement("Resource");
239            marshal(resource, xmlStreamWriter, xsiURI);
240            xmlStreamWriter.writeEndElement();
241          }
242    
243          xmlStreamWriter.writeEndElement();
244    
245          xmlStreamWriter.writeEndElement();
246          xmlStreamWriter.writeEndDocument();
247        }
248        catch (XMLStreamException e)
249        {
250          Debug.debugException(e);
251          throw new ServerErrorException(
252              "Cannot write resources: " + e.getMessage());
253        }
254      }
255    
256    
257    
258      /**
259       * {@inheritDoc}
260       */
261      public void writeBulkStart(final int failOnErrors,
262                                 final Set<String> schemaURIs)
263          throws SCIMException
264      {
265        try
266        {
267          xmlStreamWriter.writeStartDocument("UTF-8", "1.0");
268    
269          xmlStreamWriter.setPrefix(SCIMConstants.DEFAULT_SCHEMA_PREFIX,
270              SCIMConstants.SCHEMA_URI_CORE);
271          xmlStreamWriter.setPrefix("xsi", xsiURI);
272          xmlStreamWriter.writeStartElement(SCIMConstants.SCHEMA_URI_CORE,
273              "Bulk");
274          xmlStreamWriter.writeNamespace(SCIMConstants.DEFAULT_SCHEMA_PREFIX,
275              SCIMConstants.SCHEMA_URI_CORE);
276          xmlStreamWriter.writeNamespace("xsi", xsiURI);
277    
278          if (failOnErrors >= 0)
279          {
280            xmlStreamWriter.writeStartElement("failOnErrors");
281            xmlStreamWriter.writeCharacters(
282                Integer.toString(failOnErrors));
283            xmlStreamWriter.writeEndElement();
284          }
285    
286          xmlStreamWriter.writeStartElement("Operations");
287        }
288        catch (XMLStreamException e)
289        {
290          Debug.debugException(e);
291          throw new ServerErrorException(
292              "Cannot write start of bulk operations: " + e.getMessage());
293        }
294      }
295    
296    
297    
298      /**
299       * {@inheritDoc}
300       */
301      public void writeBulkOperation(final BulkOperation o)
302          throws SCIMException
303      {
304        try
305        {
306          xmlStreamWriter.writeStartElement("Operation");
307          if (o.getMethod() != null)
308          {
309            xmlStreamWriter.writeStartElement("method");
310            xmlStreamWriter.writeCharacters(o.getMethod().toString());
311            xmlStreamWriter.writeEndElement();
312          }
313          if (o.getBulkId() != null)
314          {
315            xmlStreamWriter.writeStartElement("bulkId");
316            xmlStreamWriter.writeCharacters(o.getBulkId());
317            xmlStreamWriter.writeEndElement();
318          }
319          if (o.getVersion() != null)
320          {
321            xmlStreamWriter.writeStartElement("version");
322            xmlStreamWriter.writeCharacters(o.getVersion());
323            xmlStreamWriter.writeEndElement();
324          }
325          if (o.getPath() != null)
326          {
327            xmlStreamWriter.writeStartElement("path");
328            xmlStreamWriter.writeCharacters(o.getPath());
329            xmlStreamWriter.writeEndElement();
330          }
331          if (o.getLocation() != null)
332          {
333            xmlStreamWriter.writeStartElement("location");
334            xmlStreamWriter.writeCharacters(o.getLocation());
335            xmlStreamWriter.writeEndElement();
336          }
337          if (o.getData() != null)
338          {
339            xmlStreamWriter.writeStartElement("data");
340            marshal(o.getData(), xmlStreamWriter, xsiURI);
341            xmlStreamWriter.writeEndElement();
342          }
343          if (o.getStatus() != null)
344          {
345            xmlStreamWriter.writeStartElement("status");
346            xmlStreamWriter.writeStartElement("code");
347            xmlStreamWriter.writeCharacters(o.getStatus().getCode());
348            xmlStreamWriter.writeEndElement();
349            if (o.getStatus().getDescription() != null)
350            {
351              xmlStreamWriter.writeStartElement("description");
352              xmlStreamWriter.writeCharacters(o.getStatus().getDescription());
353              xmlStreamWriter.writeEndElement();
354            }
355            xmlStreamWriter.writeEndElement();
356          }
357          xmlStreamWriter.writeEndElement();
358        }
359        catch (XMLStreamException e)
360        {
361          Debug.debugException(e);
362          throw new ServerErrorException(
363              "Cannot write bulk operation: " + e.getMessage());
364        }
365      }
366    
367    
368      /**
369       * {@inheritDoc}
370       */
371      public void writeBulkFinish()
372          throws SCIMException
373      {
374        try
375        {
376          xmlStreamWriter.writeEndElement();
377          xmlStreamWriter.writeEndElement();
378          xmlStreamWriter.writeEndDocument();
379        }
380        catch (XMLStreamException e)
381        {
382          Debug.debugException(e);
383          throw new ServerErrorException(
384              "Cannot write end of bulk operations: " + e.getMessage());
385        }
386      }
387    
388    
389    
390      /**
391       * {@inheritDoc}
392       */
393      public void bulkMarshal(final int failOnErrors,
394                              final List<BulkOperation> operations)
395          throws SCIMException
396      {
397        writeBulkStart(failOnErrors, Collections.<String>emptySet());
398        for (final BulkOperation o : operations)
399        {
400          writeBulkOperation(o);
401        }
402        writeBulkFinish();
403      }
404    
405    
406    
407      /**
408       * Write a SCIM object to an XML stream.
409       *
410       * @param resource        The SCIM resource to be written.
411       * @param xmlStreamWriter The stream to which the SCIM object should be
412       *                        written.
413       * @param xsiURI          The xsi URI to use for the type attribute.
414       * @throws XMLStreamException If the object could not be written.
415       */
416      private void marshal(final BaseResource resource,
417                           final XMLStreamWriter xmlStreamWriter,
418                           final String xsiURI)
419        throws XMLStreamException
420      {
421        final String resourceSchemaURI =
422            resource.getResourceDescriptor().getSchema();
423    
424        int i = 1;
425        for (final String schemaURI :
426            resource.getResourceDescriptor().getAttributeSchemas())
427        {
428          if (schemaURI.equalsIgnoreCase(resourceSchemaURI))
429          {
430            final String prefix = SCIMConstants.DEFAULT_SCHEMA_PREFIX;
431            xmlStreamWriter.setPrefix(prefix, schemaURI);
432            xmlStreamWriter.writeNamespace(prefix, schemaURI);
433          }
434          else if (resource.getScimObject().hasSchema(schemaURI))
435          {
436            final String prefix = "ns" + String.valueOf(i++);
437            xmlStreamWriter.setPrefix(prefix, schemaURI);
438            xmlStreamWriter.writeNamespace(prefix, schemaURI);
439          }
440        }
441    
442        if (xsiURI != null)
443        {
444          xmlStreamWriter.writeAttribute(xsiURI, "type",
445              SCIMConstants.DEFAULT_SCHEMA_PREFIX + ':' +
446                  resource.getResourceDescriptor().getName());
447        }
448    
449        // Write the resource attributes in the order defined by the
450        // resource descriptor.
451        for (final AttributeDescriptor attributeDescriptor :
452            resource.getResourceDescriptor().getAttributes())
453        {
454          final SCIMAttribute a =
455              resource.getScimObject().getAttribute(
456                  attributeDescriptor.getSchema(), attributeDescriptor.getName());
457          if (a != null)
458          {
459            if (a.getAttributeDescriptor().isMultiValued())
460            {
461              writeMultiValuedAttribute(a, xmlStreamWriter);
462            }
463            else
464            {
465              writeSingularAttribute(a, xmlStreamWriter);
466            }
467          }
468        }
469      }
470    
471    
472    
473      /**
474       * Write a multi-valued attribute to an XML stream.
475       *
476       * @param scimAttribute   The attribute to be written.
477       * @param xmlStreamWriter The stream to which the attribute should be
478       *                        written.
479       * @throws XMLStreamException If the attribute could not be written.
480       */
481      private void writeMultiValuedAttribute(final SCIMAttribute scimAttribute,
482                                             final XMLStreamWriter xmlStreamWriter)
483        throws XMLStreamException
484      {
485        final SCIMAttributeValue[] values = scimAttribute.getValues();
486    
487        writeStartElement(scimAttribute, xmlStreamWriter);
488    
489        for (final SCIMAttributeValue value : values)
490        {
491          writeChildStartElement(scimAttribute, xmlStreamWriter);
492    
493          // Write the subordinate attributes in the order defined by the schema.
494          for (final AttributeDescriptor descriptor :
495              scimAttribute.getAttributeDescriptor().getSubAttributes())
496          {
497            final SCIMAttribute a = value.getAttribute(descriptor.getName());
498            if (a != null)
499            {
500              if (a.getAttributeDescriptor().isMultiValued())
501              {
502                writeMultiValuedAttribute(a, xmlStreamWriter);
503              }
504              else
505              {
506                writeSingularAttribute(a, xmlStreamWriter);
507              }
508            }
509          }
510    
511          xmlStreamWriter.writeEndElement();
512        }
513    
514        xmlStreamWriter.writeEndElement();
515      }
516    
517    
518    
519      /**
520       * Write a singular attribute to an XML stream.
521       *
522       * @param scimAttribute   The attribute to be written.
523       * @param xmlStreamWriter The stream to which the attribute should be
524       *                        written.
525       * @throws XMLStreamException If the attribute could not be written.
526       */
527      private void writeSingularAttribute(final SCIMAttribute scimAttribute,
528                                          final XMLStreamWriter xmlStreamWriter)
529        throws XMLStreamException
530      {
531        final AttributeDescriptor attributeDescriptor =
532            scimAttribute.getAttributeDescriptor();
533    
534        writeStartElement(scimAttribute, xmlStreamWriter);
535    
536        final SCIMAttributeValue val = scimAttribute.getValue();
537    
538        if (val.isComplex())
539        {
540          // Write the subordinate attributes in the order defined by the schema.
541          for (final AttributeDescriptor ad :
542              attributeDescriptor.getSubAttributes())
543          {
544            final SCIMAttribute a = val.getAttribute(ad.getName());
545            if (a != null)
546            {
547              writeSingularAttribute(a, xmlStreamWriter);
548            }
549          }
550        }
551        else
552        {
553          final String stringValue =
554              scimAttribute.getValue().getStringValue();
555          xmlStreamWriter.writeCharacters(stringValue);
556        }
557    
558        xmlStreamWriter.writeEndElement();
559      }
560    
561    
562    
563      /**
564       * Helper that writes namespace when needed.
565       * @param scimAttribute Attribute tag to write.
566       * @param xmlStreamWriter Writer to write with.
567       * @throws XMLStreamException thrown if error writing the tag element.
568       */
569      private void writeStartElement(final SCIMAttribute scimAttribute,
570                                     final XMLStreamWriter xmlStreamWriter)
571        throws XMLStreamException
572      {
573        if (scimAttribute.getSchema().equalsIgnoreCase(
574            SCIMConstants.SCHEMA_URI_CORE))
575        {
576          xmlStreamWriter.writeStartElement(scimAttribute.getName());
577        }
578        else
579        {
580          xmlStreamWriter.writeStartElement(scimAttribute.getSchema(),
581            scimAttribute.getName());
582        }
583      }
584    
585    
586    
587    
588    
589      /**
590       * Helper that writes namespace when needed.
591       * @param scimAttribute Attribute tag to write.
592       * @param xmlStreamWriter Writer to write with.
593       * @throws XMLStreamException thrown if error writing the tag element.
594       */
595      private void writeChildStartElement(final SCIMAttribute scimAttribute,
596                                          final XMLStreamWriter xmlStreamWriter)
597        throws XMLStreamException
598      {
599        if (scimAttribute.getSchema().equalsIgnoreCase(
600            SCIMConstants.SCHEMA_URI_CORE))
601        {
602          xmlStreamWriter.writeStartElement(scimAttribute.getAttributeDescriptor().
603              getMultiValuedChildName());
604        }
605        else
606        {
607          xmlStreamWriter.writeStartElement(scimAttribute.getSchema(),
608            scimAttribute.getAttributeDescriptor().getMultiValuedChildName());
609        }
610      }
611    }