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
018/*
019 * Copyright 2011-2013 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
035package com.unboundid.scim.marshal.json;
036
037import com.unboundid.scim.data.BaseResource;
038import com.unboundid.scim.marshal.StreamMarshaller;
039import com.unboundid.scim.sdk.BulkOperation;
040import com.unboundid.scim.sdk.Debug;
041import com.unboundid.scim.sdk.Resources;
042import com.unboundid.scim.sdk.SCIMAttribute;
043import com.unboundid.scim.sdk.SCIMAttributeValue;
044import com.unboundid.scim.sdk.SCIMConstants;
045import com.unboundid.scim.sdk.SCIMException;
046import com.unboundid.scim.sdk.ServerErrorException;
047import org.json.JSONException;
048import org.json.JSONWriter;
049
050import java.io.IOException;
051import java.io.OutputStream;
052import java.io.OutputStreamWriter;
053import java.util.Collection;
054import java.util.HashSet;
055import java.util.List;
056import 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 */
064public 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());
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      if (value == null)
479      {
480        continue;
481      }
482
483      if (value.isComplex())
484      {
485        jsonWriter.object();
486        for (SCIMAttribute attribute : value.getAttributes().values())
487        {
488          if (attribute.getAttributeDescriptor().isMultiValued())
489          {
490            this.writeMultiValuedAttribute(attribute, jsonWriter);
491          }
492          else
493          {
494            this.writeSingularAttribute(attribute, jsonWriter);
495          }
496        }
497        jsonWriter.endObject();
498      }
499      else
500      {
501        if (scimAttribute.getAttributeDescriptor().getDataType() != null)
502        {
503          switch (scimAttribute.getAttributeDescriptor().getDataType())
504          {
505            case BOOLEAN:
506              jsonWriter.value(value.getBooleanValue());
507              break;
508
509            case DECIMAL:
510              jsonWriter.value(value.getDecimalValue());
511              break;
512
513            case INTEGER:
514              jsonWriter.value(value.getIntegerValue());
515              break;
516
517            case BINARY:
518            case DATETIME:
519            case STRING:
520            default:
521              jsonWriter.value(value.getStringValue());
522              break;
523          }
524        }
525        else
526        {
527          jsonWriter.value(value.getStringValue());
528        }
529      }
530    }
531    jsonWriter.endArray();
532  }
533
534
535
536  /**
537   * Write a singular attribute to an XML stream.
538   *
539   * @param scimAttribute The attribute to be written.
540   * @param jsonWriter    Output to write the attribute to.
541   *
542   * @throws org.json.JSONException Thrown if error writing to output.
543   */
544  private void writeSingularAttribute(final SCIMAttribute scimAttribute,
545                                      final JSONWriter jsonWriter)
546      throws JSONException
547  {
548    jsonWriter.key(scimAttribute.getName());
549    SCIMAttributeValue val = scimAttribute.getValue();
550    if (val.isComplex())
551    {
552      jsonWriter.object();
553      for (SCIMAttribute a : val.getAttributes().values())
554      {
555        if (a.getAttributeDescriptor().isMultiValued())
556        {
557          this.writeMultiValuedAttribute(a, jsonWriter);
558        }
559        else
560        {
561          this.writeSingularAttribute(a, jsonWriter);
562        }
563      }
564      jsonWriter.endObject();
565    }
566    else
567    {
568      if (scimAttribute.getAttributeDescriptor().getDataType() != null)
569      {
570        switch (scimAttribute.getAttributeDescriptor().getDataType())
571        {
572          case BOOLEAN:
573            jsonWriter.value(val.getBooleanValue());
574            break;
575
576          case DECIMAL:
577            jsonWriter.value(val.getDecimalValue());
578            break;
579
580          case INTEGER:
581            jsonWriter.value(val.getIntegerValue());
582            break;
583
584          case BINARY:
585          case DATETIME:
586          case STRING:
587          default:
588            jsonWriter.value(val.getStringValue());
589            break;
590        }
591      }
592      else
593      {
594        jsonWriter.value(val.getStringValue());
595      }
596    }
597  }
598}