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    /*
019     * Copyright 2011-2012 UnboundID Corp.
020     *
021     * This program is free software; you can redistribute it and/or modify
022     * it under the terms of the GNU General Public License (GPLv2 only)
023     * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
024     * as published by the Free Software Foundation.
025     *
026     * This program is distributed in the hope that it will be useful,
027     * but WITHOUT ANY WARRANTY; without even the implied warranty of
028     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
029     * GNU General Public License for more details.
030     *
031     * You should have received a copy of the GNU General Public License
032     * along with this program; if not, see <http://www.gnu.org/licenses>.
033     */
034    
035    package com.unboundid.scim.marshal.json;
036    
037    import com.unboundid.scim.data.BaseResource;
038    import com.unboundid.scim.marshal.StreamMarshaller;
039    import com.unboundid.scim.sdk.BulkOperation;
040    import com.unboundid.scim.sdk.Debug;
041    import com.unboundid.scim.sdk.Resources;
042    import com.unboundid.scim.sdk.SCIMAttribute;
043    import com.unboundid.scim.sdk.SCIMAttributeValue;
044    import com.unboundid.scim.sdk.SCIMConstants;
045    import com.unboundid.scim.sdk.SCIMException;
046    import com.unboundid.scim.sdk.ServerErrorException;
047    import org.json.JSONException;
048    import org.json.JSONWriter;
049    
050    import java.io.IOException;
051    import java.io.OutputStream;
052    import java.io.OutputStreamWriter;
053    import java.util.Collection;
054    import java.util.HashSet;
055    import java.util.List;
056    import java.util.Set;
057    
058    
059    
060    /**
061     * This class provides a SCIM object marshaller implementation to write a
062     * stream of SCIM objects to their JSON representation.
063     */
064    public class JsonStreamMarshaller implements StreamMarshaller
065    {
066      private final OutputStreamWriter outputStreamWriter;
067      private final JSONWriter jsonWriter;
068    
069    
070    
071      /**
072       * Create a JSON marshaller that writes to the given output stream.
073       * The resulting marshaller must be closed after use.
074       *
075       * @param outputStream  The ouput stream to write to.
076       *
077       * @throws SCIMException  If the marshaller could not be created.
078       */
079      public JsonStreamMarshaller(final OutputStream outputStream)
080          throws SCIMException
081      {
082        try
083        {
084          outputStreamWriter = new OutputStreamWriter(outputStream);
085          jsonWriter = new JSONWriter(outputStreamWriter);
086        }
087        catch (Exception e)
088        {
089          Debug.debugException(e);
090          throw new ServerErrorException(
091              "Cannot create JSON marshaller: " + e.getMessage());
092        }
093      }
094    
095    
096    
097      /**
098       * {@inheritDoc}
099       */
100      @Override
101      public void close()
102          throws SCIMException
103      {
104        try
105        {
106          outputStreamWriter.close();
107        }
108        catch (IOException e)
109        {
110          Debug.debugException(e);
111          throw new ServerErrorException(
112              "Cannot close marshaller: " + e.getMessage());
113        }
114      }
115    
116    
117    
118      /**
119       * {@inheritDoc}
120       */
121      public void marshal(final BaseResource resource)
122          throws SCIMException
123      {
124        try
125        {
126          marshal(resource, true);
127        }
128        catch (JSONException e)
129        {
130          Debug.debugException(e);
131          throw new ServerErrorException(
132              "Cannot write resource: " + e.getMessage());
133        }
134      }
135    
136    
137    
138      /**
139       * Write a SCIM resource to a JSON writer.
140       *
141       * @param resource   The SCIM resource to be written.
142       * @param includeSchemas  Indicates whether the schemas should be written
143       *                        at the start of the object.
144       * @throws org.json.JSONException Thrown if error writing to output.
145       */
146      private void marshal(final BaseResource resource,
147                           final boolean includeSchemas)
148          throws JSONException
149      {
150        jsonWriter.object();
151    
152        final Set<String> schemas = new HashSet<String>(
153            resource.getResourceDescriptor().getAttributeSchemas());
154        if (includeSchemas)
155        {
156          // Write out the schemas for this object.
157          jsonWriter.key(SCIMConstants.SCHEMAS_ATTRIBUTE_NAME);
158          jsonWriter.array();
159          for (final String schema : schemas)
160          {
161            jsonWriter.value(schema);
162          }
163          jsonWriter.endArray();
164        }
165    
166        // first write out core schema, then if any extensions write them
167        // out in their own json object keyed by the schema name
168    
169        for (final SCIMAttribute attribute : resource.getScimObject()
170            .getAttributes(SCIMConstants.SCHEMA_URI_CORE))
171        {
172          if (attribute.getAttributeDescriptor().isMultiValued())
173          {
174            this.writeMultiValuedAttribute(attribute, jsonWriter);
175          }
176          else
177          {
178            this.writeSingularAttribute(attribute, jsonWriter);
179          }
180        }
181    
182        // write out any custom schemas
183        for (final String schema : schemas)
184        {
185          if (!schema.equalsIgnoreCase(SCIMConstants.SCHEMA_URI_CORE))
186          {
187            Collection<SCIMAttribute> attributes =
188                resource.getScimObject().getAttributes(schema);
189            if(!attributes.isEmpty())
190            {
191              jsonWriter.key(schema);
192              jsonWriter.object();
193              for (SCIMAttribute attribute : attributes)
194              {
195                if (attribute.getAttributeDescriptor().isMultiValued())
196                {
197                  this.writeMultiValuedAttribute(attribute, jsonWriter);
198                }
199                else
200                {
201                  this.writeSingularAttribute(attribute, jsonWriter);
202                }
203              }
204              jsonWriter.endObject();
205            }
206          }
207        }
208        jsonWriter.endObject();
209      }
210    
211      /**
212       * {@inheritDoc}
213       */
214      public void marshal(final Resources<? extends BaseResource> response)
215          throws SCIMException
216      {
217        try
218        {
219          jsonWriter.object();
220          jsonWriter.key("totalResults");
221          jsonWriter.value(response.getTotalResults());
222    
223          jsonWriter.key("itemsPerPage");
224          jsonWriter.value(response.getItemsPerPage());
225    
226          jsonWriter.key("startIndex");
227          jsonWriter.value(response.getStartIndex());
228    
229          // Figure out what schemas are referenced by the resources.
230          final Set<String> schemaURIs = new HashSet<String>();
231          for (final BaseResource resource : response)
232          {
233            schemaURIs.addAll(
234                resource.getResourceDescriptor().getAttributeSchemas());
235          }
236    
237          // Write the schemas.
238          jsonWriter.key(SCIMConstants.SCHEMAS_ATTRIBUTE_NAME);
239          jsonWriter.array();
240          for (final String schemaURI : schemaURIs)
241          {
242            jsonWriter.value(schemaURI);
243          }
244          jsonWriter.endArray();
245    
246          // Write the resources.
247          jsonWriter.key("Resources");
248          jsonWriter.array();
249          for (final BaseResource resource : response)
250          {
251            marshal(resource, false);
252          }
253          jsonWriter.endArray();
254    
255          jsonWriter.endObject();
256        }
257        catch (JSONException e)
258        {
259          Debug.debugException(e);
260          throw new ServerErrorException(
261              "Cannot write resources response: " + e.getMessage());
262        }
263      }
264    
265    
266    
267      /**
268       * {@inheritDoc}
269       */
270      public void marshal(final SCIMException response)
271          throws SCIMException
272      {
273        try
274        {
275          jsonWriter.object();
276          jsonWriter.key("Errors");
277          jsonWriter.array();
278    
279          jsonWriter.object();
280    
281          jsonWriter.key("code");
282          jsonWriter.value(String.valueOf(response.getStatusCode()));
283    
284          final String description = response.getMessage();
285          if (description != null)
286          {
287            jsonWriter.key("description");
288            jsonWriter.value(description);
289          }
290    
291          jsonWriter.endObject();
292    
293          jsonWriter.endArray();
294    
295          jsonWriter.endObject();
296        }
297        catch (JSONException e)
298        {
299          Debug.debugException(e);
300          throw new ServerErrorException(
301              "Cannot write error response: " + e.getMessage());
302        }
303      }
304    
305    
306    
307      /**
308       * {@inheritDoc}
309       */
310      public void writeBulkStart(final int failOnErrors,
311                                 final Set<String> schemaURIs)
312          throws SCIMException
313      {
314        try
315        {
316          jsonWriter.object();
317    
318          if (failOnErrors >= 0)
319          {
320            jsonWriter.key("failOnErrors");
321            jsonWriter.value(failOnErrors);
322          }
323    
324          // Write the schemas.
325          jsonWriter.key(SCIMConstants.SCHEMAS_ATTRIBUTE_NAME);
326          jsonWriter.array();
327          for (final String schemaURI : schemaURIs)
328          {
329            jsonWriter.value(schemaURI);
330          }
331          jsonWriter.endArray();
332    
333          // Write the operations.
334          jsonWriter.key("Operations");
335          jsonWriter.array();
336        }
337        catch (JSONException e)
338        {
339          Debug.debugException(e);
340          throw new ServerErrorException(
341              "Cannot write start of bulk operations: " + e.getMessage());
342        }
343      }
344    
345    
346    
347      /**
348       * {@inheritDoc}
349       */
350      public void writeBulkOperation(final BulkOperation o)
351          throws SCIMException
352      {
353        try
354        {
355          jsonWriter.object();
356          if (o.getMethod() != null)
357          {
358            jsonWriter.key("method");
359            jsonWriter.value(o.getMethod().toString());
360          }
361          if (o.getBulkId() != null)
362          {
363            jsonWriter.key("bulkId");
364            jsonWriter.value(o.getBulkId());
365          }
366          if (o.getVersion() != null)
367          {
368            jsonWriter.key("version");
369            jsonWriter.value(o.getVersion());
370          }
371          if (o.getPath() != null)
372          {
373            jsonWriter.key("path");
374            jsonWriter.value(o.getPath());
375          }
376          if (o.getLocation() != null)
377          {
378            jsonWriter.key("location");
379            jsonWriter.value(o.getLocation());
380          }
381          if (o.getData() != null)
382          {
383            jsonWriter.key("data");
384            marshal(o.getData(), true);
385          }
386          if (o.getStatus() != null)
387          {
388            jsonWriter.key("status");
389            jsonWriter.object();
390            jsonWriter.key("code");
391            jsonWriter.value(o.getStatus().getCode());
392            if (o.getStatus().getDescription() != null)
393            {
394              jsonWriter.key("description");
395              jsonWriter.value(o.getStatus().getDescription());
396            }
397            jsonWriter.endObject();
398          }
399          jsonWriter.endObject();
400        }
401        catch (JSONException e)
402        {
403          Debug.debugException(e);
404          throw new ServerErrorException(
405              "Cannot write bulk operation: " + e.getMessage());
406        }
407      }
408    
409    
410      /**
411       * {@inheritDoc}
412       */
413      public void writeBulkFinish()
414          throws SCIMException
415      {
416        try
417        {
418          jsonWriter.endArray();
419          jsonWriter.endObject();
420        }
421        catch (JSONException e)
422        {
423          Debug.debugException(e);
424          throw new ServerErrorException(
425              "Cannot write end of bulk operations: " + e.getMessage());
426        }
427      }
428    
429    
430    
431      /**
432       * {@inheritDoc}
433       */
434      public void bulkMarshal(final int failOnErrors,
435                              final List<BulkOperation> operations)
436          throws SCIMException
437      {
438        // Figure out what schemas are referenced by the resources.
439        final Set<String> schemaURIs = new HashSet<String>();
440        for (final BulkOperation o : operations)
441        {
442          final BaseResource resource = o.getData();
443          if (resource != null)
444          {
445            schemaURIs.addAll(
446                o.getData().getResourceDescriptor().getAttributeSchemas());
447          }
448        }
449    
450        writeBulkStart(failOnErrors, schemaURIs);
451        for (final BulkOperation o : operations)
452        {
453          writeBulkOperation(o);
454        }
455        writeBulkFinish();
456      }
457    
458    
459    
460      /**
461       * Write a multi-valued attribute to an XML stream.
462       *
463       * @param scimAttribute The attribute to be written.
464       * @param jsonWriter    Output to write the attribute to.
465       *
466       * @throws JSONException Thrown if error writing to output.
467       */
468      private void writeMultiValuedAttribute(final SCIMAttribute scimAttribute,
469                                             final JSONWriter jsonWriter)
470          throws JSONException
471      {
472    
473        SCIMAttributeValue[] values = scimAttribute.getValues();
474        jsonWriter.key(scimAttribute.getName());
475        jsonWriter.array();
476        for (SCIMAttributeValue value : values)
477        {
478          jsonWriter.object();
479          for (SCIMAttribute attribute : value.getAttributes().values())
480          {
481            if (attribute.getAttributeDescriptor().isMultiValued())
482            {
483              this.writeMultiValuedAttribute(attribute, jsonWriter);
484            }
485            else
486            {
487              this.writeSingularAttribute(attribute, jsonWriter);
488            }
489          }
490          jsonWriter.endObject();
491        }
492        jsonWriter.endArray();
493      }
494    
495    
496    
497      /**
498       * Write a singular attribute to an XML stream.
499       *
500       * @param scimAttribute The attribute to be written.
501       * @param jsonWriter    Output to write the attribute to.
502       *
503       * @throws org.json.JSONException Thrown if error writing to output.
504       */
505      private void writeSingularAttribute(final SCIMAttribute scimAttribute,
506                                          final JSONWriter jsonWriter)
507          throws JSONException
508      {
509        jsonWriter.key(scimAttribute.getName());
510        SCIMAttributeValue val = scimAttribute.getValue();
511        if (val.isComplex())
512        {
513          jsonWriter.object();
514          for (SCIMAttribute a : val.getAttributes().values())
515          {
516            this.writeSingularAttribute(a, jsonWriter);
517          }
518          jsonWriter.endObject();
519        }
520        else
521        {
522          if (scimAttribute.getAttributeDescriptor().getDataType() != null)
523          {
524            switch (scimAttribute.getAttributeDescriptor().getDataType())
525            {
526              case BOOLEAN:
527                jsonWriter.value(val.getBooleanValue());
528                break;
529    
530              case DECIMAL:
531                jsonWriter.value(val.getDecimalValue());
532                break;
533    
534              case INTEGER:
535                jsonWriter.value(val.getIntegerValue());
536                break;
537    
538              case BINARY:
539              case DATETIME:
540              case STRING:
541              default:
542                jsonWriter.value(val.getStringValue());
543                break;
544            }
545          }
546          else
547          {
548            jsonWriter.value(val.getStringValue());
549          }
550        }
551      }
552    }