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}