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}