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