001/* 002 * Copyright 2017-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2017-2024 Ping Identity Corporation 007 * 008 * Licensed under the Apache License, Version 2.0 (the "License"); 009 * you may not use this file except in compliance with the License. 010 * You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, software 015 * distributed under the License is distributed on an "AS IS" BASIS, 016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 017 * See the License for the specific language governing permissions and 018 * limitations under the License. 019 */ 020/* 021 * Copyright (C) 2017-2024 Ping Identity Corporation 022 * 023 * This program is free software; you can redistribute it and/or modify 024 * it under the terms of the GNU General Public License (GPLv2 only) 025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 026 * as published by the Free Software Foundation. 027 * 028 * This program is distributed in the hope that it will be useful, 029 * but WITHOUT ANY WARRANTY; without even the implied warranty of 030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 031 * GNU General Public License for more details. 032 * 033 * You should have received a copy of the GNU General Public License 034 * along with this program; if not, see <http://www.gnu.org/licenses>. 035 */ 036package com.unboundid.ldap.sdk.unboundidds.tools; 037 038 039 040import java.io.OutputStream; 041import java.util.ArrayList; 042import java.util.List; 043 044import com.unboundid.ldap.sdk.Attribute; 045import com.unboundid.ldap.sdk.BindResult; 046import com.unboundid.ldap.sdk.CompareResult; 047import com.unboundid.ldap.sdk.Control; 048import com.unboundid.ldap.sdk.Entry; 049import com.unboundid.ldap.sdk.ExtendedResult; 050import com.unboundid.ldap.sdk.LDAPConnection; 051import com.unboundid.ldap.sdk.LDAPException; 052import com.unboundid.ldap.sdk.LDAPResult; 053import com.unboundid.ldap.sdk.LDAPRuntimeException; 054import com.unboundid.ldap.sdk.ResultCode; 055import com.unboundid.ldap.sdk.SearchResult; 056import com.unboundid.ldap.sdk.SearchResultEntry; 057import com.unboundid.ldap.sdk.SearchResultReference; 058import com.unboundid.util.Base64; 059import com.unboundid.util.Debug; 060import com.unboundid.util.NotNull; 061import com.unboundid.util.Nullable; 062import com.unboundid.util.ThreadSafety; 063import com.unboundid.util.ThreadSafetyLevel; 064import com.unboundid.util.json.JSONBuffer; 065import com.unboundid.util.json.JSONException; 066import com.unboundid.util.json.JSONObject; 067 068 069 070/** 071 * This class provides an {@link LDAPResultWriter} instance that formats results 072 * in JSON. 073 * <BR> 074 * <BLOCKQUOTE> 075 * <B>NOTE:</B> This class, and other classes within the 076 * {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only 077 * supported for use against Ping Identity, UnboundID, and 078 * Nokia/Alcatel-Lucent 8661 server products. These classes provide support 079 * for proprietary functionality or for external specifications that are not 080 * considered stable or mature enough to be guaranteed to work in an 081 * interoperable way with other types of LDAP servers. 082 * </BLOCKQUOTE> 083 */ 084@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 085public final class JSONLDAPResultWriter 086 extends LDAPResultWriter 087{ 088 // A list that may be used in the course of formatting result lines. 089 @NotNull private final ArrayList<String> formattedLines; 090 091 // The JSON buffer used to construct the formatted output. 092 @NotNull private final JSONBuffer jsonBuffer; 093 094 095 096 /** 097 * Creates a new instance of this LDAP result writer. 098 * 099 * @param outputStream The output stream to which output will be written. 100 */ 101 public JSONLDAPResultWriter(@NotNull final OutputStream outputStream) 102 { 103 super(outputStream); 104 105 formattedLines = new ArrayList<>(10); 106 jsonBuffer = new JSONBuffer(null, 0, true); 107 } 108 109 110 111 /** 112 * {@inheritDoc} 113 */ 114 @Override() 115 public void writeComment(@NotNull final String comment) 116 { 117 // Comments will not be written in this format. 118 } 119 120 121 122 /** 123 * {@inheritDoc} 124 */ 125 @Override() 126 public void writeHeader() 127 { 128 // No header is required for this format. 129 } 130 131 132 133 /** 134 * {@inheritDoc} 135 */ 136 @Override() 137 public void writeSearchResultEntry(@NotNull final SearchResultEntry entry) 138 { 139 jsonBuffer.clear(); 140 toJSON(entry, jsonBuffer, formattedLines); 141 println(jsonBuffer.toString()); 142 } 143 144 145 146 /** 147 * Encodes the provided entry as a JSON object. 148 * 149 * @param entry The entry to be encoded as a JSON object. It must not be 150 * {@code null}. 151 * 152 * @return The JSON object containing the encoded representation of the 153 * entry. 154 */ 155 @NotNull() 156 public static JSONObject toJSON(@NotNull final Entry entry) 157 { 158 try 159 { 160 final JSONBuffer jsonBuffer = new JSONBuffer(); 161 toJSON(entry, jsonBuffer); 162 return jsonBuffer.toJSONObject(); 163 } 164 catch (final JSONException e) 165 { 166 // This should never happen. 167 Debug.debugException(e); 168 throw new LDAPRuntimeException(new LDAPException( 169 ResultCode.ENCODING_ERROR, e.getMessage(), e)); 170 } 171 } 172 173 174 175 /** 176 * Appends a JSON object representation of the provided entry to the given 177 * buffer. 178 * 179 * @param entry The entry to be encoded as a JSON object. It must not 180 * be {@code null}. 181 * @param jsonBuffer The JSON buffer to which the encoded representation 182 * of the entry is to be appended. It must not be 183 * {@code null}. 184 */ 185 public static void toJSON(@NotNull final Entry entry, 186 @NotNull final JSONBuffer jsonBuffer) 187 { 188 toJSON(entry, jsonBuffer, null); 189 } 190 191 192 193 /** 194 * Appends a JSON object representation of the provided entry to the given 195 * buffer. 196 * 197 * @param entry The entry to be encoded as a JSON object. It must 198 * not be {@code null}. 199 * @param jsonBuffer The JSON buffer to which the encoded representation 200 * of the entry is to be appended. It must not be 201 * {@code null}. 202 * @param formattedLines A list that will be used for temporary storage 203 * during processing. It must not be {@code null}, 204 * must be updatable, and must not contain any data 205 * that you care about being preserved. 206 */ 207 private static void toJSON(@NotNull final Entry entry, 208 @NotNull final JSONBuffer jsonBuffer, 209 @Nullable final List<String> formattedLines) 210 { 211 jsonBuffer.beginObject(); 212 jsonBuffer.appendString("result-type", "entry"); 213 jsonBuffer.appendString("dn", entry.getDN()); 214 215 jsonBuffer.beginArray("attributes"); 216 for (final Attribute a : entry.getAttributes()) 217 { 218 jsonBuffer.beginObject(); 219 jsonBuffer.appendString("name", a.getName()); 220 jsonBuffer.beginArray("values"); 221 222 for (final String value : a.getValues()) 223 { 224 jsonBuffer.appendString(value); 225 } 226 jsonBuffer.endArray(); 227 jsonBuffer.endObject(); 228 } 229 jsonBuffer.endArray(); 230 231 if (entry instanceof SearchResultEntry) 232 { 233 final SearchResultEntry searchResultEntry = (SearchResultEntry) entry; 234 final Control[] controls = searchResultEntry.getControls(); 235 if ((controls != null) && (controls.length > 0)) 236 { 237 if (formattedLines == null) 238 { 239 handleControls(controls, jsonBuffer, new ArrayList<String>()); 240 } 241 else 242 { 243 handleControls(controls, jsonBuffer, formattedLines); 244 } 245 } 246 } 247 248 jsonBuffer.endObject(); 249 } 250 251 252 253 /** 254 * {@inheritDoc} 255 */ 256 @Override() 257 public void writeSearchResultReference( 258 @NotNull final SearchResultReference ref) 259 { 260 jsonBuffer.clear(); 261 toJSON(ref, jsonBuffer, formattedLines); 262 println(jsonBuffer.toString()); 263 } 264 265 266 267 /** 268 * Encodes the provided search result reference as a JSON object. 269 * 270 * @param ref The search result reference to be encoded as a JSON object. 271 * It must not be {@code null}. 272 * 273 * @return The JSON object containing the encoded representation of the 274 * search result reference. 275 */ 276 @NotNull() 277 public static JSONObject toJSON( 278 @NotNull final SearchResultReference ref) 279 { 280 try 281 { 282 final JSONBuffer jsonBuffer = new JSONBuffer(); 283 toJSON(ref, jsonBuffer); 284 return jsonBuffer.toJSONObject(); 285 } 286 catch (final JSONException e) 287 { 288 // This should never happen. 289 Debug.debugException(e); 290 throw new LDAPRuntimeException(new LDAPException( 291 ResultCode.ENCODING_ERROR, e.getMessage(), e)); 292 } 293 } 294 295 296 297 /** 298 * Appends a JSON object representation of the provided search result 299 * reference to the given buffer. 300 * 301 * @param ref The search result reference to be encoded as a JSON 302 * object. It must not be {@code null}. 303 * @param jsonBuffer The JSON buffer to which the encoded representation 304 * of the reference is to be appended. It must not be 305 * {@code null}. 306 */ 307 public static void toJSON(@NotNull final SearchResultReference ref, 308 @NotNull final JSONBuffer jsonBuffer) 309 { 310 toJSON(ref, jsonBuffer, null); 311 } 312 313 314 315 /** 316 * Appends a JSON object representation of the provided search result 317 * reference to the given buffer. 318 * 319 * @param ref The search result reference to be encoded as a JSON 320 * object. It must not be {@code null}. 321 * @param jsonBuffer The JSON buffer to which the encoded representation 322 * of the reference is to be appended. It must not be 323 * {@code null}. 324 * @param formattedLines A list that will be used for temporary storage 325 * during processing. It must not be {@code null}, 326 * must be updatable, and must not contain any data 327 * that you care about being preserved. 328 */ 329 private static void toJSON(@NotNull final SearchResultReference ref, 330 @NotNull final JSONBuffer jsonBuffer, 331 @Nullable final List<String> formattedLines) 332 { 333 jsonBuffer.beginObject(); 334 jsonBuffer.appendString("result-type", "reference"); 335 336 jsonBuffer.beginArray("referral-urls"); 337 for (final String url : ref.getReferralURLs()) 338 { 339 jsonBuffer.appendString(url); 340 } 341 jsonBuffer.endArray(); 342 343 final Control[] controls = ref.getControls(); 344 if ((controls != null) && (controls.length > 0)) 345 { 346 if (formattedLines == null) 347 { 348 handleControls(controls, jsonBuffer, new ArrayList<String>()); 349 } 350 else 351 { 352 handleControls(controls, jsonBuffer, formattedLines); 353 } 354 } 355 356 jsonBuffer.endObject(); 357 358 } 359 360 361 362 /** 363 * {@inheritDoc} 364 */ 365 @Override() 366 public void writeResult(@NotNull final LDAPResult result) 367 { 368 jsonBuffer.clear(); 369 toJSON(result, jsonBuffer, formattedLines); 370 println(jsonBuffer.toString()); 371 } 372 373 374 375 /** 376 * Encodes the provided LDAP result as a JSON object. 377 * 378 * @param result The LDAP result to be encoded as a JSON object. It must 379 * not be {@code null}. 380 * 381 * @return The JSON object containing the encoded representation of the 382 * LDAP result. 383 */ 384 @NotNull() 385 public static JSONObject toJSON(@NotNull final LDAPResult result) 386 { 387 try 388 { 389 final JSONBuffer jsonBuffer = new JSONBuffer(); 390 toJSON(result, jsonBuffer); 391 return jsonBuffer.toJSONObject(); 392 } 393 catch (final JSONException e) 394 { 395 // This should never happen. 396 Debug.debugException(e); 397 throw new LDAPRuntimeException(new LDAPException( 398 ResultCode.ENCODING_ERROR, e.getMessage(), e)); 399 } 400 } 401 402 403 404 /** 405 * Appends a JSON object representation of the provided entry to the given 406 * buffer. 407 * 408 * @param result The LDAP result to be encoded as a JSON object. It 409 * must not be {@code null}. 410 * @param jsonBuffer The JSON buffer to which the encoded representation 411 * of the LDAP result is to be appended. It must not be 412 * {@code null}. 413 */ 414 public static void toJSON(@NotNull final LDAPResult result, 415 @NotNull final JSONBuffer jsonBuffer) 416 { 417 toJSON(result, jsonBuffer, null); 418 } 419 420 421 422 /** 423 * Appends a JSON object representation of the provided LDAP result to the 424 * given buffer. 425 * 426 * @param result The LDAP result to be encoded as a JSON object. It 427 * must not be {@code null}. 428 * @param jsonBuffer The JSON buffer to which the encoded representation 429 * of the LDAP result is to be appended. It must not 430 * be {@code null}. 431 * @param formattedLines A list that will be used for temporary storage 432 * during processing. It must not be {@code null}, 433 * must be updatable, and must not contain any data 434 * that you care about being preserved. 435 */ 436 private static void toJSON(@NotNull final LDAPResult result, 437 @NotNull final JSONBuffer jsonBuffer, 438 @Nullable final List<String> formattedLines) 439 { 440 jsonBuffer.beginObject(); 441 442 if (result instanceof SearchResult) 443 { 444 jsonBuffer.appendString("result-type", "search-result"); 445 } 446 else if (result instanceof BindResult) 447 { 448 jsonBuffer.appendString("result-type", "bind-result"); 449 } 450 else if (result instanceof CompareResult) 451 { 452 jsonBuffer.appendString("result-type", "compare-result"); 453 } 454 else if (result instanceof ExtendedResult) 455 { 456 jsonBuffer.appendString("result-type", "extended-result"); 457 } 458 else 459 { 460 jsonBuffer.appendString("result-type", "ldap-result"); 461 } 462 463 jsonBuffer.appendNumber("result-code", result.getResultCode().intValue()); 464 jsonBuffer.appendString("result-code-name", 465 result.getResultCode().getName()); 466 467 final String diagnosticMessage = result.getDiagnosticMessage(); 468 if (diagnosticMessage != null) 469 { 470 jsonBuffer.appendString("diagnostic-message", diagnosticMessage); 471 } 472 473 final String matchedDN = result.getMatchedDN(); 474 if (matchedDN != null) 475 { 476 jsonBuffer.appendString("matched-dn", matchedDN); 477 } 478 479 final String[] referralURLs = result.getReferralURLs(); 480 if ((referralURLs != null) && (referralURLs.length > 0)) 481 { 482 jsonBuffer.beginArray("referral-urls"); 483 for (final String url : referralURLs) 484 { 485 jsonBuffer.appendString(url); 486 } 487 jsonBuffer.endArray(); 488 } 489 490 if (result instanceof SearchResult) 491 { 492 final SearchResult searchResult = (SearchResult) result; 493 jsonBuffer.appendNumber("entries-returned", searchResult.getEntryCount()); 494 jsonBuffer.appendNumber("references-returned", 495 searchResult.getReferenceCount()); 496 } 497 else if (result instanceof ExtendedResult) 498 { 499 final ExtendedResult extendedResult = (ExtendedResult) result; 500 final String oid = extendedResult.getOID(); 501 if (oid != null) 502 { 503 jsonBuffer.appendString("oid", oid); 504 } 505 506 if (extendedResult.hasValue()) 507 { 508 jsonBuffer.appendString("base64-encoded-value", 509 Base64.encode(extendedResult.getValue().getValue())); 510 } 511 } 512 513 final Control[] controls = result.getResponseControls(); 514 if ((controls != null) && (controls.length > 0)) 515 { 516 if (formattedLines == null) 517 { 518 handleControls(controls, jsonBuffer, new ArrayList<String>()); 519 } 520 else 521 { 522 handleControls(controls, jsonBuffer, formattedLines); 523 } 524 } 525 526 jsonBuffer.endObject(); 527 } 528 529 530 531 /** 532 * {@inheritDoc} 533 */ 534 @Override() 535 public void writeUnsolicitedNotification( 536 @NotNull final LDAPConnection connection, 537 @NotNull final ExtendedResult notification) 538 { 539 jsonBuffer.clear(); 540 jsonBuffer.beginObject(); 541 542 jsonBuffer.appendString("result-type", "unsolicited-notification"); 543 544 final String oid = notification.getOID(); 545 if (oid != null) 546 { 547 jsonBuffer.appendString("oid", oid); 548 } 549 550 if (notification.hasValue()) 551 { 552 jsonBuffer.appendString("base64-encoded-value", 553 Base64.encode(notification.getValue().getValue())); 554 } 555 556 jsonBuffer.appendNumber("result-code", 557 notification.getResultCode().intValue()); 558 jsonBuffer.appendString("result-code-name", 559 notification.getResultCode().getName()); 560 561 final String diagnosticMessage = notification.getDiagnosticMessage(); 562 if (diagnosticMessage != null) 563 { 564 jsonBuffer.appendString("diagnostic-message", diagnosticMessage); 565 } 566 567 final String matchedDN = notification.getMatchedDN(); 568 if (matchedDN != null) 569 { 570 jsonBuffer.appendString("matched-dn", matchedDN); 571 } 572 573 final String[] referralURLs = notification.getReferralURLs(); 574 if ((referralURLs != null) && (referralURLs.length > 0)) 575 { 576 jsonBuffer.beginArray("referral-urls"); 577 for (final String url : referralURLs) 578 { 579 jsonBuffer.appendString(url); 580 } 581 jsonBuffer.endArray(); 582 } 583 584 handleControls(notification.getResponseControls()); 585 586 formattedLines.clear(); 587 ResultUtils.formatUnsolicitedNotification(formattedLines, notification, 588 false, 0, Integer.MAX_VALUE); 589 jsonBuffer.beginArray("formatted-unsolicited-notification-lines"); 590 for (final String line : formattedLines) 591 { 592 jsonBuffer.appendString(line.trim()); 593 } 594 jsonBuffer.endArray(); 595 596 jsonBuffer.endObject(); 597 598 println(jsonBuffer.toString()); 599 } 600 601 602 603 /** 604 * Handles the necessary processing for the provided set of controls. 605 * 606 * @param controls The controls to be processed. It may be {@code null} or 607 * empty if there are no controls to be processed. 608 */ 609 private void handleControls(@Nullable final Control[] controls) 610 { 611 handleControls(controls, jsonBuffer, formattedLines); 612 } 613 614 615 616 /** 617 * Handles the necessary processing for the provided set of controls. 618 * 619 * @param controls The controls to be processed. It must not be 620 * {@code null} or emtpy. 621 * @param jsonBuffer The buffer to which the encoded representation of 622 * the controls should be appended. It must not be 623 * {@code null}. 624 * @param formattedLines A list that will be used for temporary storage 625 * during processing. It must not be {@code null}, 626 * must be updatable, and must not contain any data 627 * that you care about being preserved. 628 */ 629 private static void handleControls(@Nullable final Control[] controls, 630 @NotNull final JSONBuffer jsonBuffer, 631 @NotNull final List<String> formattedLines) 632 { 633 jsonBuffer.beginArray("controls"); 634 635 for (final Control c : controls) 636 { 637 jsonBuffer.beginObject(); 638 jsonBuffer.appendString("oid", c.getOID()); 639 jsonBuffer.appendBoolean("criticality", c.isCritical()); 640 641 if (c.hasValue()) 642 { 643 jsonBuffer.appendString("base64-encoded-value", 644 Base64.encode(c.getValue().getValue())); 645 } 646 647 formattedLines.clear(); 648 ResultUtils.formatResponseControl(formattedLines, c, false, 0, 649 Integer.MAX_VALUE); 650 jsonBuffer.beginArray("formatted-control-lines"); 651 for (final String line : formattedLines) 652 { 653 jsonBuffer.appendString(line.trim()); 654 } 655 jsonBuffer.endArray(); 656 657 jsonBuffer.endObject(); 658 } 659 660 jsonBuffer.endArray(); 661 } 662}