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 }