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}