001/*
002 * Copyright 2020-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2020-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) 2020-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;
037
038
039
040import java.net.InetAddress;
041import java.text.SimpleDateFormat;
042import java.util.ArrayList;
043import java.util.Arrays;
044import java.util.Collections;
045import java.util.Date;
046import java.util.EnumSet;
047import java.util.HashSet;
048import java.util.LinkedHashMap;
049import java.util.LinkedHashSet;
050import java.util.List;
051import java.util.Map;
052import java.util.Set;
053import java.util.logging.Handler;
054import java.util.logging.Level;
055import java.util.logging.LogRecord;
056
057import com.unboundid.ldap.sdk.schema.AttributeTypeDefinition;
058import com.unboundid.ldap.sdk.schema.Schema;
059import com.unboundid.util.Debug;
060import com.unboundid.util.NotMutable;
061import com.unboundid.util.NotNull;
062import com.unboundid.util.Nullable;
063import com.unboundid.util.StaticUtils;
064import com.unboundid.util.ThreadSafety;
065import com.unboundid.util.ThreadSafetyLevel;
066import com.unboundid.util.json.JSONBuffer;
067
068
069
070/**
071 * This class provides an implementation of an LDAP connection access logger
072 * that records messages as JSON objects.
073 */
074@NotMutable()
075@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
076public final class JSONLDAPConnectionLogger
077       extends LDAPConnectionLogger
078{
079  /**
080   * The bytes that comprise the value that will be used in place of redacted
081   * attribute values.
082   */
083  @NotNull private static final String REDACTED_VALUE_STRING = "[REDACTED]";
084
085
086
087  /**
088   * The bytes that comprise the value that will be used in place of redacted
089   * attribute values.
090   */
091  @NotNull private static final byte[] REDACTED_VALUE_BYTES =
092       StaticUtils.getBytes(REDACTED_VALUE_STRING);
093
094
095
096  // Indicates whether to flush the handler after logging information about each
097  // successful for failed connection attempt.
098  private final boolean flushAfterConnectMessages;
099
100  // Indicates whether to flush the handler after logging information about each
101  // disconnect.
102  private final boolean flushAfterDisconnectMessages;
103
104  // Indicates whether to flush the handler after logging information about each
105  // request.
106  private final boolean flushAfterRequestMessages;
107
108  // Indicates whether to flush the handler after logging information about the
109  // final result for each operation.
110  private final boolean flushAfterFinalResultMessages;
111
112  // Indicates whether to flush the handler after logging information about each
113  // non-final result (including search result entries, search result
114  // references, and intermediate response messages) for each operation.
115  private final boolean flushAfterNonFinalResultMessages;
116
117  // Indicates whether to include the names of attributes provided in add
118  // requests.
119  private final boolean includeAddAttributeNames;
120
121  // Indicates whether to include the values of attributes provided in add
122  // requests.
123  private final boolean includeAddAttributeValues;
124
125  // Indicates whether to include the names of attributes targeted by modify
126  // requests.
127  private final boolean includeModifyAttributeNames;
128
129  // Indicates whether to include the values of attributes targeted by modify
130  // requests.
131  private final boolean includeModifyAttributeValues;
132
133  // Indicates whether to include the OIDs of controls included in requests and
134  // results.
135  private final boolean includeControlOIDs;
136
137  // Indicates whether to include the names of attributes provided in search
138  // result entries.
139  private final boolean includeSearchEntryAttributeNames;
140
141  // Indicates whether to include the values of attributes provided in search
142  // result entries.
143  private final boolean includeSearchEntryAttributeValues;
144
145  // Indicates whether to log successful and failed connection attempts.
146  private final boolean logConnects;
147
148  // Indicates whether to log disconnects.
149  private final boolean logDisconnects;
150
151  // Indicates whether to log intermediate response messages.
152  private final boolean logIntermediateResponses;
153
154  // Indicates whether to log operation requests for enabled operation types.
155  private final boolean logRequests;
156
157  // Indicates whether to log final operation results for enabled operation
158  // types.
159  private final boolean logFinalResults;
160
161  // Indicates whether to log search result entries.
162  private final boolean logSearchEntries;
163
164  // Indicates whether to log search result references.
165  private final boolean logSearchReferences;
166
167  // The log handler that will be used to actually log the messages.
168  @NotNull private final Handler logHandler;
169
170  // The schema to use for identifying alternate attribute type names.
171  @Nullable private final Schema schema;
172
173  // The types of operations for which requests should be logged.
174  @NotNull private final Set<OperationType> operationTypes;
175
176  // The names or OIDs of the attributes whose values should be redacted.
177  @NotNull private final Set<String> attributesToRedact;
178
179  // The full set of the names and OIDs for attributes whose values should be
180  // redacted.
181  @NotNull private final Set<String> fullAttributesToRedact;
182
183  // The set of thread-local JSON buffers that will be used for formatting log
184  // messages.
185  @NotNull private final ThreadLocal<JSONBuffer> jsonBuffers;
186
187  // The set of thread-local date formatters that will be used for formatting
188  // timestamps.
189  @NotNull private final ThreadLocal<SimpleDateFormat> timestampFormatters;
190
191
192
193  /**
194   * Creates a new instance of this LDAP connection logger that will write
195   * messages to the provided log handler using the given set of properties.
196   *
197   * @param  logHandler  The log handler that will be used to actually log the
198   *                     messages.  All messages will be logged with a level of
199   *                     {@code INFO}.
200   * @param  properties  The properties to use for this logger.
201   */
202  public JSONLDAPConnectionLogger(@NotNull final Handler logHandler,
203              @NotNull final JSONLDAPConnectionLoggerProperties properties)
204  {
205    this.logHandler = logHandler;
206
207    flushAfterConnectMessages = properties.flushAfterConnectMessages();
208    flushAfterDisconnectMessages = properties.flushAfterDisconnectMessages();
209    flushAfterRequestMessages = properties.flushAfterRequestMessages();
210    flushAfterFinalResultMessages =
211         properties.flushAfterFinalResultMessages();
212    flushAfterNonFinalResultMessages =
213         properties.flushAfterNonFinalResultMessages();
214    includeAddAttributeNames = properties.includeAddAttributeNames();
215    includeAddAttributeValues = properties.includeAddAttributeValues();
216    includeModifyAttributeNames = properties.includeModifyAttributeNames();
217    includeModifyAttributeValues = properties.includeModifyAttributeValues();
218    includeControlOIDs = properties.includeControlOIDs();
219    includeSearchEntryAttributeNames =
220         properties.includeSearchEntryAttributeNames();
221    includeSearchEntryAttributeValues =
222         properties.includeSearchEntryAttributeValues();
223    logConnects = properties.logConnects();
224    logDisconnects = properties.logDisconnects();
225    logIntermediateResponses = properties.logIntermediateResponses();
226    logRequests = properties.logRequests();
227    logFinalResults = properties.logFinalResults();
228    logSearchEntries = properties.logSearchEntries();
229    logSearchReferences = properties.logSearchReferences();
230    schema = properties.getSchema();
231
232    attributesToRedact = Collections.unmodifiableSet(new LinkedHashSet<>(
233         properties.getAttributesToRedact()));
234
235    final EnumSet<OperationType> opTypes = EnumSet.noneOf(OperationType.class);
236    opTypes.addAll(properties.getOperationTypes());
237    operationTypes = Collections.unmodifiableSet(opTypes);
238
239    jsonBuffers = new ThreadLocal<>();
240    timestampFormatters = new ThreadLocal<>();
241
242    final Set<String> fullAttrsToRedact = new HashSet<>();
243    for (final String attr : attributesToRedact)
244    {
245      fullAttrsToRedact.add(StaticUtils.toLowerCase(attr));
246
247      if (schema != null)
248      {
249        final AttributeTypeDefinition d = schema.getAttributeType(attr);
250        if (d != null)
251        {
252          fullAttrsToRedact.add(StaticUtils.toLowerCase(d.getOID()));
253          for (final String name : d.getNames())
254          {
255            fullAttrsToRedact.add(StaticUtils.toLowerCase(name));
256          }
257        }
258      }
259    }
260
261    fullAttributesToRedact = Collections.unmodifiableSet(fullAttrsToRedact);
262  }
263
264
265
266  /**
267   * Indicates whether to log successful and failed connection attempts.
268   * Connection attempts will be logged by default.
269   *
270   * @return  {@code true} if connection attempts should be logged, or
271   *          {@code false} if not.
272   */
273  public boolean logConnects()
274  {
275    return logConnects;
276  }
277
278
279
280  /**
281   * Indicates whether to log disconnects.  Disconnects will be logged by
282   * default.
283   *
284   * @return  {@code true} if disconnects should be logged, or {@code false} if
285   *          not.
286   */
287  public boolean logDisconnects()
288  {
289    return logDisconnects;
290  }
291
292
293
294  /**
295   * Indicates whether to log messages about requests for operations included
296   * in the set of operation types returned by the {@link #getOperationTypes}
297   * method.  Operation requests will be logged by default.
298   *
299   * @return  {@code true} if operation requests should be logged for
300   *          appropriate operation types, or {@code false} if not.
301   */
302  public boolean logRequests()
303  {
304    return logRequests;
305  }
306
307
308
309  /**
310   * Indicates whether to log messages about the final reults for operations
311   * included in the set of operation types returned by the
312   * {@link #getOperationTypes} method.  Final operation results will be
313   * logged by default.
314   *
315   * @return  {@code true} if operation requests should be logged for
316   *          appropriate operation types, or {@code false} if not.
317   */
318  public boolean logFinalResults()
319  {
320    return logFinalResults;
321  }
322
323
324
325  /**
326   * Indicates whether to log messages about each search result entry returned
327   * for search operations.  This property will only be used if the set returned
328   * by the  {@link #getOperationTypes} method includes
329   * {@link OperationType#SEARCH}.  Search result entries will not be logged by
330   * default.
331   *
332   * @return  {@code true} if search result entries should be logged, or
333   *          {@code false} if not.
334   */
335  public boolean logSearchEntries()
336  {
337    return logSearchEntries;
338  }
339
340
341
342  /**
343   * Indicates whether to log messages about each search result reference
344   * returned for search operations.  This property will only be used if the set
345   * returned by the  {@link #getOperationTypes} method includes
346   * {@link OperationType#SEARCH}.  Search result references will not be logged
347   * by default.
348   *
349   * @return  {@code true} if search result references should be logged, or
350   *          {@code false} if not.
351   */
352  public boolean logSearchReferences()
353  {
354    return logSearchReferences;
355  }
356
357
358
359  /**
360   * Indicates whether to log messages about each intermediate response returned
361   * in the course of processing an operation.  Intermediate response messages
362   * will be logged by default.
363   *
364   * @return  {@code true} if intermediate response messages should be logged,
365   *          or {@code false} if not.
366   */
367  public boolean logIntermediateResponses()
368  {
369    return logIntermediateResponses;
370  }
371
372
373
374  /**
375   * Retrieves the set of operation types for which to log requests and
376   * results.  All operation types will be logged by default.
377   *
378   * @return  The set of operation types for which to log requests and results.
379   */
380  @NotNull()
381  public Set<OperationType> getOperationTypes()
382  {
383    return operationTypes;
384  }
385
386
387
388  /**
389   * Indicates whether log messages about add requests should include the names
390   * of the attributes provided in the request.  Add attribute names (but not
391   * values) will be logged by default.
392   *
393   * @return  {@code true} if add attribute names should be logged, or
394   *          {@code false} if not.
395   */
396  public boolean includeAddAttributeNames()
397  {
398    return includeAddAttributeNames;
399  }
400
401
402
403  /**
404   * Indicates whether log messages about add requests should include the values
405   * of the attributes provided in the request.  This property will only be used
406   * if {@link #includeAddAttributeNames} returns {@code true}.  Values for
407   * attributes named in the set returned by the
408   * {@link #getAttributesToRedact} method will be replaced with a value of
409   * "[REDACTED]".  Add attribute names (but not values) will be
410   * logged by default.
411   *
412   * @return  {@code true} if add attribute values should be logged, or
413   *          {@code false} if not.
414   */
415  public boolean includeAddAttributeValues()
416  {
417    return includeAddAttributeValues;
418  }
419
420
421
422  /**
423   * Indicates whether log messages about modify requests should include the
424   * names of the attributes modified in the request.  Modified attribute names
425   * (but not values) will be logged by default.
426   *
427   * @return  {@code true} if modify attribute names should be logged, or
428   *          {@code false} if not.
429   */
430  public boolean includeModifyAttributeNames()
431  {
432    return includeModifyAttributeNames;
433  }
434
435
436
437  /**
438   * Indicates whether log messages about modify requests should include the
439   * values of the attributes modified in the request.  This property will only
440   * be used if {@link #includeModifyAttributeNames} returns {@code true}.
441   * Values for attributes named in the set returned by the
442   * {@link #getAttributesToRedact} method will be replaced with a value of
443   * "[REDACTED]".  Modify attribute names (but not values) will be
444   * logged by default.
445   *
446   * @return  {@code true} if modify attribute values should be logged, or
447   *          {@code false} if not.
448   */
449  public boolean includeModifyAttributeValues()
450  {
451    return includeModifyAttributeValues;
452  }
453
454
455
456  /**
457   * Indicates whether log messages about search result entries should include
458   * the names of the attributes in the returned entry.  Entry attribute names
459   * (but not values) will be logged by default.
460   *
461   * @return  {@code true} if search result entry attribute names should be
462   *          logged, or {@code false} if not.
463   */
464  public boolean includeSearchEntryAttributeNames()
465  {
466    return includeSearchEntryAttributeNames;
467  }
468
469
470
471  /**
472   * Indicates whether log messages about search result entries should include
473   * the values of the attributes in the returned entry.  This property will
474   * only be used if {@link #includeSearchEntryAttributeNames} returns
475   * {@code true}.  Values for attributes named in the set returned by the
476   * {@link #getAttributesToRedact} method will be replaced with a value of
477   * "[REDACTED]".  Entry attribute names (but not values) will be
478   * logged by default.
479   *
480   * @return  {@code true} if search result entry attribute values should be
481   *          logged, or {@code false} if not.
482   */
483  public boolean includeSearchEntryAttributeValues()
484  {
485    return includeSearchEntryAttributeValues;
486  }
487
488
489
490  /**
491   * Retrieves a set containing the names or OIDs of the attributes whose values
492   * should be redacted from log messages.  Values of the userPassword,
493   * authPassword, and unicodePWD attributes will be redacted by default.
494   *
495   * @return  A set containing the names or OIDs of the attributes whose values
496   *          should be redacted from log messages, or an empty set if no
497   *          attribute values should be redacted.
498   */
499  @NotNull()
500  public Set<String> getAttributesToRedact()
501  {
502    return attributesToRedact;
503  }
504
505
506
507  /**
508   * Indicates whether request and result log messages should include the OIDs
509   * of any controls included in that request or result.  Control OIDs will
510   * be logged by default.
511   *
512   * @return  {@code true} if request control OIDs should be logged, or
513   *          {@code false} if not.
514   */
515  public boolean includeControlOIDs()
516  {
517    return includeControlOIDs;
518  }
519
520
521
522  /**
523   * Indicates whether the log handler should be flushed after logging each
524   * successful or failed connection attempt.  By default, the handler will be
525   * flushed after logging each connection attempt.
526   *
527   * @return  {@code true} if the log handler should be flushed after logging
528   *          each connection attempt, or {@code false} if not.
529   */
530  public boolean flushAfterConnectMessages()
531  {
532    return flushAfterConnectMessages;
533  }
534
535
536
537  /**
538   * Indicates whether the log handler should be flushed after logging each
539   * disconnect.  By default, the handler will be flushed after logging each
540   * disconnect.
541   *
542   * @return  {@code true} if the log handler should be flushed after logging
543   *          each disconnect, or {@code false} if not.
544   */
545  public boolean flushAfterDisconnectMessages()
546  {
547    return flushAfterDisconnectMessages;
548  }
549
550
551
552  /**
553   * Indicates whether the log handler should be flushed after logging each
554   * request.  By default, the handler will be flushed after logging each final
555   * result, but not after logging requests or non-final results.
556   *
557   * @return  {@code true} if the log handler should be flushed after logging
558   *          each request, or {@code false} if not.
559   */
560  public boolean flushAfterRequestMessages()
561  {
562    return flushAfterRequestMessages;
563  }
564
565
566
567  /**
568   * Indicates whether the log handler should be flushed after logging each
569   * non-final result (including search result entries, search result
570   * references, and intermediate response messages).  By default, the handler
571   * will be flushed after logging each final result, but not after logging
572   * requests or non-final results.
573   *
574   * @return  {@code true} if the log handler should be flushed after logging
575   *          each non-final result, or {@code false} if not.
576   */
577  public boolean flushAfterNonFinalResultMessages()
578  {
579    return flushAfterNonFinalResultMessages;
580  }
581
582
583
584  /**
585   * Indicates whether the log handler should be flushed after logging the final
586   * result for each operation.  By default, the handler will be flushed after
587   * logging each final result, but not after logging requests or non-final
588   * results.
589   *
590   * @return  {@code true} if the log handler should be flushed after logging
591   *          each final result, or {@code false} if not.
592   */
593  public boolean flushAfterFinalResultMessages()
594  {
595    return flushAfterFinalResultMessages;
596  }
597
598
599
600  /**
601   * Retrieves the schema that will be used to identify alternate names and OIDs
602   * for attributes whose values should be redacted.  The LDAP SDK's default
603   * standard schema will be used by default.
604   *
605   * @return  The schema that will be used to identify alternate names and OIDs
606   *          for attributes whose values should be redacted, or {@code null}
607   *          if no schema should be used.
608   */
609  @Nullable()
610  public Schema getSchema()
611  {
612    return schema;
613  }
614
615
616
617  /**
618   * {@inheritDoc}
619   */
620  @Override()
621  public void logConnect(@NotNull final LDAPConnectionInfo connectionInfo,
622                         @NotNull final String host,
623                         @NotNull final InetAddress inetAddress,
624                         final int port)
625  {
626    if (logConnects)
627    {
628      final JSONBuffer buffer = startLogMessage("connect", null,
629           connectionInfo, -1);
630
631      buffer.appendString("hostname", host);
632      buffer.appendString("ip-address", inetAddress.getHostAddress());
633      buffer.appendNumber("port", port);
634
635      logMessage(buffer, flushAfterConnectMessages);
636    }
637  }
638
639
640
641  /**
642   * {@inheritDoc}
643   */
644  @Override()
645  public void logConnectFailure(
646                   @NotNull final LDAPConnectionInfo connectionInfo,
647                   @NotNull final String host, final int port,
648                   @NotNull final LDAPException connectException)
649  {
650    if (logConnects)
651    {
652      final JSONBuffer buffer = startLogMessage("connect-failure", null,
653           connectionInfo, -1);
654
655      buffer.appendString("hostname", host);
656      buffer.appendNumber("port", port);
657
658      if (connectException != null)
659      {
660        appendException(buffer, "connect-exception", connectException);
661      }
662
663      logMessage(buffer, flushAfterConnectMessages);
664    }
665  }
666
667
668
669  /**
670   * {@inheritDoc}
671   */
672  @Override()
673  public void logDisconnect(
674                   @NotNull final LDAPConnectionInfo connectionInfo,
675                   @NotNull final String host, final int port,
676                   @NotNull final DisconnectType disconnectType,
677                   @Nullable final String disconnectMessage,
678                   @Nullable final Throwable disconnectCause)
679  {
680    if (logDisconnects)
681    {
682      final JSONBuffer buffer = startLogMessage("disconnect", null,
683           connectionInfo, -1);
684
685      buffer.appendString("hostname", host);
686      buffer.appendNumber("port", port);
687      buffer.appendString("disconnect-type", disconnectType.name());
688
689      if (disconnectMessage != null)
690      {
691        buffer.appendString("disconnect-message", disconnectMessage);
692      }
693
694      if (disconnectCause != null)
695      {
696        appendException(buffer, "disconnect-cause", disconnectCause);
697      }
698
699      logMessage(buffer, flushAfterDisconnectMessages);
700    }
701  }
702
703
704
705  /**
706   * {@inheritDoc}
707   */
708  @Override()
709  public void logAbandonRequest(
710                   @NotNull final LDAPConnectionInfo connectionInfo,
711                   final int messageID,
712                   final int messageIDToAbandon,
713                   @NotNull final List<Control> requestControls)
714  {
715    if (logRequests && operationTypes.contains(OperationType.ABANDON))
716    {
717      final JSONBuffer buffer = startLogMessage("request",
718           OperationType.ABANDON, connectionInfo, messageID);
719
720      buffer.appendNumber("message-id-to-abandon", messageIDToAbandon);
721      appendControls(buffer, "control-oids", requestControls);
722
723      logMessage(buffer, flushAfterRequestMessages);
724    }
725  }
726
727
728
729  /**
730   * {@inheritDoc}
731   */
732  @Override()
733  public void logAddRequest(@NotNull final LDAPConnectionInfo connectionInfo,
734                            final int messageID,
735                            @NotNull final ReadOnlyAddRequest addRequest)
736  {
737    if (logRequests && operationTypes.contains(OperationType.ADD))
738    {
739      final JSONBuffer buffer = startLogMessage("request",
740           OperationType.ADD, connectionInfo, messageID);
741
742      appendDN(buffer, "dn", addRequest.getDN());
743
744      if (includeAddAttributeNames)
745      {
746        appendAttributes(buffer, "attributes", addRequest.getAttributes(),
747             includeAddAttributeValues);
748      }
749
750      appendControls(buffer, "control-oids", addRequest.getControls());
751
752      logMessage(buffer, flushAfterRequestMessages);
753    }
754  }
755
756
757
758  /**
759   * {@inheritDoc}
760   */
761  @Override()
762  public void logAddResult(@NotNull final LDAPConnectionInfo connectionInfo,
763                           final int requestMessageID,
764                           @NotNull final LDAPResult addResult)
765  {
766    logLDAPResult(connectionInfo, OperationType.ADD, requestMessageID,
767         addResult);
768  }
769
770
771
772  /**
773   * {@inheritDoc}
774   */
775  @Override()
776  public void logBindRequest(@NotNull final LDAPConnectionInfo connectionInfo,
777                             final int messageID,
778                             @NotNull final SimpleBindRequest bindRequest)
779  {
780    if (logRequests && operationTypes.contains(OperationType.BIND))
781    {
782      final JSONBuffer buffer = startLogMessage("request",
783           OperationType.BIND, connectionInfo, messageID);
784
785      buffer.appendString("authentication-type", "simple");
786      appendDN(buffer, "dn", bindRequest.getBindDN());
787
788      appendControls(buffer, "control-oids", bindRequest.getControls());
789
790      logMessage(buffer, flushAfterRequestMessages);
791    }
792  }
793
794
795
796  /**
797   * {@inheritDoc}
798   */
799  @Override()
800  public void logBindRequest(@NotNull final LDAPConnectionInfo connectionInfo,
801                             final int messageID,
802                             @NotNull final SASLBindRequest bindRequest)
803  {
804    if (logRequests && operationTypes.contains(OperationType.BIND))
805    {
806      final JSONBuffer buffer = startLogMessage("request",
807           OperationType.BIND, connectionInfo, messageID);
808
809      buffer.appendString("authentication-type", "SASL");
810      buffer.appendString("sasl-mechanism", bindRequest.getSASLMechanismName());
811
812      appendControls(buffer, "control-oids", bindRequest.getControls());
813
814      logMessage(buffer, flushAfterRequestMessages);
815    }
816  }
817
818
819
820  /**
821   * {@inheritDoc}
822   */
823  @Override()
824  public void logBindResult(@NotNull final LDAPConnectionInfo connectionInfo,
825                            final int requestMessageID,
826                            @NotNull final BindResult bindResult)
827  {
828    logLDAPResult(connectionInfo, OperationType.BIND, requestMessageID,
829         bindResult);
830  }
831
832
833
834  /**
835   * {@inheritDoc}
836   */
837  @Override()
838  public void logCompareRequest(
839                   @NotNull final LDAPConnectionInfo connectionInfo,
840                   final int messageID,
841                   @NotNull final ReadOnlyCompareRequest compareRequest)
842  {
843    if (logRequests && operationTypes.contains(OperationType.COMPARE))
844    {
845      final JSONBuffer buffer = startLogMessage("request",
846           OperationType.COMPARE, connectionInfo, messageID);
847
848      appendDN(buffer, "dn", compareRequest.getDN());
849      appendDN(buffer, "attribute-type", compareRequest.getAttributeName());
850
851      final String baseName = StaticUtils.toLowerCase(
852           Attribute.getBaseName(compareRequest.getAttributeName()));
853      if (fullAttributesToRedact.contains(baseName))
854      {
855        buffer.appendString("assertion-value", REDACTED_VALUE_STRING);
856      }
857      else
858      {
859        buffer.appendString("assertion-value",
860             compareRequest.getAssertionValue());
861      }
862
863      appendControls(buffer, "control-oids", compareRequest.getControls());
864
865      logMessage(buffer, flushAfterRequestMessages);
866    }
867  }
868
869
870
871  /**
872   * {@inheritDoc}
873   */
874  @Override()
875  public void logCompareResult(@NotNull final LDAPConnectionInfo connectionInfo,
876                               final int requestMessageID,
877                               @NotNull final LDAPResult compareResult)
878  {
879    logLDAPResult(connectionInfo, OperationType.COMPARE, requestMessageID,
880         compareResult);
881  }
882
883
884
885  /**
886   * {@inheritDoc}
887   */
888  @Override()
889  public void logDeleteRequest(@NotNull final LDAPConnectionInfo connectionInfo,
890                   final int messageID,
891                   @NotNull final ReadOnlyDeleteRequest deleteRequest)
892  {
893    if (logRequests && operationTypes.contains(OperationType.DELETE))
894    {
895      final JSONBuffer buffer = startLogMessage("request",
896           OperationType.DELETE, connectionInfo, messageID);
897
898      appendDN(buffer, "dn", deleteRequest.getDN());
899      appendControls(buffer, "control-oids", deleteRequest.getControls());
900
901      logMessage(buffer, flushAfterRequestMessages);
902    }
903  }
904
905
906
907  /**
908   * {@inheritDoc}
909   */
910  @Override()
911  public void logDeleteResult(@NotNull final LDAPConnectionInfo connectionInfo,
912                              final int requestMessageID,
913                              @NotNull final LDAPResult deleteResult)
914  {
915    logLDAPResult(connectionInfo, OperationType.DELETE, requestMessageID,
916         deleteResult);
917  }
918
919
920
921  /**
922   * {@inheritDoc}
923   */
924  @Override()
925  public void logExtendedRequest(
926                   @NotNull final LDAPConnectionInfo connectionInfo,
927                   final int messageID,
928                   @NotNull final ExtendedRequest extendedRequest)
929  {
930    if (logRequests && operationTypes.contains(OperationType.EXTENDED))
931    {
932      final JSONBuffer buffer = startLogMessage("request",
933           OperationType.EXTENDED, connectionInfo, messageID);
934
935      buffer.appendString("oid", extendedRequest.getOID());
936      buffer.appendBoolean("has-value",  (extendedRequest.getValue() != null));
937
938      appendControls(buffer, "control-oids", extendedRequest.getControls());
939
940      logMessage(buffer, flushAfterRequestMessages);
941    }
942  }
943
944
945
946  /**
947   * {@inheritDoc}
948   */
949  @Override()
950  public void logExtendedResult(
951                   @NotNull final LDAPConnectionInfo connectionInfo,
952                   final int requestMessageID,
953                   @NotNull final ExtendedResult extendedResult)
954  {
955    logLDAPResult(connectionInfo, OperationType.EXTENDED, requestMessageID,
956         extendedResult);
957  }
958
959
960
961  /**
962   * {@inheritDoc}
963   */
964  @Override()
965  public void logModifyRequest(@NotNull final LDAPConnectionInfo connectionInfo,
966                   final int messageID,
967                   @NotNull final ReadOnlyModifyRequest modifyRequest)
968  {
969    if (logRequests && operationTypes.contains(OperationType.MODIFY))
970    {
971      final JSONBuffer buffer = startLogMessage("request",
972           OperationType.MODIFY, connectionInfo, messageID);
973
974      appendDN(buffer, "dn", modifyRequest.getDN());
975
976      if (includeModifyAttributeNames)
977      {
978        final List<Modification> mods = modifyRequest.getModifications();
979
980        if (includeModifyAttributeValues)
981        {
982          buffer.beginArray("modifications");
983          for (final Modification m : mods)
984          {
985            buffer.beginObject();
986
987            final String name = m.getAttributeName();
988            buffer.appendString("attribute-name", name);
989            buffer.appendString("modification-type",
990                 m.getModificationType().getName());
991
992            buffer.beginArray("attribute-values");
993            final String baseName =
994                 StaticUtils.toLowerCase(Attribute.getBaseName(name));
995            if (fullAttributesToRedact.contains(baseName))
996            {
997              for (final String value : m.getValues())
998              {
999                buffer.appendString(REDACTED_VALUE_STRING);
1000              }
1001            }
1002            else
1003            {
1004              for (final String value : m.getValues())
1005              {
1006                buffer.appendString(value);
1007              }
1008            }
1009
1010            buffer.endArray();
1011            buffer.endObject();
1012          }
1013
1014          buffer.endArray();
1015        }
1016        else
1017        {
1018          final Map<String,String> modifiedAttributes = new LinkedHashMap<>(
1019               StaticUtils.computeMapCapacity(mods.size()));
1020          for (final Modification m : modifyRequest.getModifications())
1021          {
1022            final String name = m.getAttributeName();
1023            final String lowerName =  StaticUtils.toLowerCase(name);
1024            if (! modifiedAttributes.containsKey(lowerName))
1025            {
1026              modifiedAttributes.put(lowerName, name);
1027            }
1028          }
1029
1030          buffer.beginArray("modified-attributes");
1031          for (final String attributeName : modifiedAttributes.values())
1032          {
1033            buffer.appendString(attributeName);
1034          }
1035
1036          buffer.endArray();
1037        }
1038      }
1039
1040      appendControls(buffer, "control-oids", modifyRequest.getControls());
1041
1042      logMessage(buffer, flushAfterRequestMessages);
1043    }
1044  }
1045
1046
1047
1048  /**
1049   * {@inheritDoc}
1050   */
1051  @Override()
1052  public void logModifyResult(@NotNull final LDAPConnectionInfo connectionInfo,
1053                              final int requestMessageID,
1054                              @NotNull final LDAPResult modifyResult)
1055  {
1056    logLDAPResult(connectionInfo, OperationType.MODIFY, requestMessageID,
1057         modifyResult);
1058  }
1059
1060
1061
1062  /**
1063   * {@inheritDoc}
1064   */
1065  @Override()
1066  public void logModifyDNRequest(
1067                   @NotNull final LDAPConnectionInfo connectionInfo,
1068                   final int messageID,
1069                   @NotNull final ReadOnlyModifyDNRequest modifyDNRequest)
1070  {
1071    if (logRequests && operationTypes.contains(OperationType.MODIFY_DN))
1072    {
1073      final JSONBuffer buffer = startLogMessage("request",
1074           OperationType.MODIFY_DN, connectionInfo, messageID);
1075
1076      appendDN(buffer, "dn", modifyDNRequest.getDN());
1077      appendDN(buffer, "new-rdn", modifyDNRequest.getNewRDN());
1078      buffer.appendBoolean("delete-old-rdn", modifyDNRequest.deleteOldRDN());
1079
1080      final String newSuperiorDN = modifyDNRequest.getNewSuperiorDN();
1081      if (newSuperiorDN != null)
1082      {
1083        appendDN(buffer, "new-superior-dn", newSuperiorDN);
1084      }
1085
1086      appendControls(buffer, "control-oids", modifyDNRequest.getControls());
1087
1088      logMessage(buffer, flushAfterRequestMessages);
1089    }
1090  }
1091
1092
1093
1094  /**
1095   * {@inheritDoc}
1096   */
1097  @Override()
1098  public void logModifyDNResult(
1099                   @NotNull final LDAPConnectionInfo connectionInfo,
1100                   final int requestMessageID,
1101                   @NotNull final LDAPResult modifyDNResult)
1102  {
1103    logLDAPResult(connectionInfo, OperationType.MODIFY_DN, requestMessageID,
1104         modifyDNResult);
1105  }
1106
1107
1108
1109  /**
1110   * {@inheritDoc}
1111   */
1112  @Override()
1113  public void logSearchRequest(@NotNull final LDAPConnectionInfo connectionInfo,
1114                   final int messageID,
1115                   @NotNull final ReadOnlySearchRequest searchRequest)
1116  {
1117    if (logRequests && operationTypes.contains(OperationType.SEARCH))
1118    {
1119      final JSONBuffer buffer = startLogMessage("request",
1120           OperationType.SEARCH, connectionInfo, messageID);
1121
1122      appendDN(buffer, "base-dn", searchRequest.getBaseDN());
1123
1124      buffer.appendString("scope", searchRequest.getScope().getName());
1125      buffer.appendString("dereference-policy",
1126           searchRequest.getDereferencePolicy().getName());
1127      buffer.appendNumber("size-limit", searchRequest.getSizeLimit());
1128      buffer.appendNumber("time-limit-seconds",
1129           searchRequest.getTimeLimitSeconds());
1130      buffer.appendBoolean("types-only", searchRequest.typesOnly());
1131      buffer.appendString("filter",
1132           redactFilter(searchRequest.getFilter()).toString());
1133
1134      buffer.beginArray("requested-attributes");
1135      for (final String attributeName : searchRequest.getAttributeList())
1136      {
1137        buffer.appendString(attributeName);
1138      }
1139      buffer.endArray();
1140
1141      appendControls(buffer, "control-oids", searchRequest.getControls());
1142
1143      logMessage(buffer, flushAfterRequestMessages);
1144    }
1145  }
1146
1147
1148
1149  /**
1150   * {@inheritDoc}
1151   */
1152  @Override()
1153  public void logSearchEntry(@NotNull final LDAPConnectionInfo connectionInfo,
1154                             final int requestMessageID,
1155                             @NotNull final SearchResultEntry searchEntry)
1156  {
1157    if (logSearchEntries && operationTypes.contains(OperationType.SEARCH))
1158    {
1159      final JSONBuffer buffer = startLogMessage("search-entry",
1160           OperationType.SEARCH, connectionInfo, requestMessageID);
1161
1162      appendDN(buffer, "dn", searchEntry.getDN());
1163
1164      if (includeSearchEntryAttributeNames)
1165      {
1166        appendAttributes(buffer, "attributes",
1167             new ArrayList<>(searchEntry.getAttributes()),
1168             includeSearchEntryAttributeValues);
1169      }
1170
1171      appendControls(buffer, "control-oids", searchEntry.getControls());
1172
1173      logMessage(buffer, flushAfterRequestMessages);
1174    }
1175  }
1176
1177
1178
1179  /**
1180   * {@inheritDoc}
1181   */
1182  @Override()
1183  public void logSearchReference(
1184                   @NotNull final LDAPConnectionInfo connectionInfo,
1185                   final int requestMessageID,
1186                   @NotNull final SearchResultReference searchReference)
1187  {
1188    if (logSearchReferences && operationTypes.contains(OperationType.SEARCH))
1189    {
1190      final JSONBuffer buffer = startLogMessage("search-reference",
1191           OperationType.SEARCH, connectionInfo, requestMessageID);
1192
1193      buffer.beginArray("referral-urls");
1194      for (final String url : searchReference.getReferralURLs())
1195      {
1196        buffer.appendString(url);
1197      }
1198      buffer.endArray();
1199
1200      appendControls(buffer, "control-oids", searchReference.getControls());
1201
1202      logMessage(buffer, flushAfterRequestMessages);
1203    }
1204  }
1205
1206
1207
1208  /**
1209   * {@inheritDoc}
1210   */
1211  @Override()
1212  public void logSearchResult(@NotNull final LDAPConnectionInfo connectionInfo,
1213                               final int requestMessageID,
1214                               @NotNull final SearchResult searchResult)
1215  {
1216    logLDAPResult(connectionInfo, OperationType.SEARCH, requestMessageID,
1217         searchResult);
1218  }
1219
1220
1221
1222  /**
1223   * {@inheritDoc}
1224   */
1225  @Override()
1226  public void logUnbindRequest(@NotNull final LDAPConnectionInfo connectionInfo,
1227                               final int messageID,
1228                               @NotNull final List<Control> requestControls)
1229  {
1230    if (logRequests && operationTypes.contains(OperationType.UNBIND))
1231    {
1232      final JSONBuffer buffer = startLogMessage("request",
1233           OperationType.UNBIND, connectionInfo, messageID);
1234
1235      appendControls(buffer, "control-oids", requestControls);
1236
1237      logMessage(buffer, flushAfterRequestMessages);
1238    }
1239  }
1240
1241
1242
1243  /**
1244   * {@inheritDoc}
1245   */
1246  @Override()
1247  public void logIntermediateResponse(
1248                   @NotNull final LDAPConnectionInfo connectionInfo,
1249                   final int messageID,
1250                   @NotNull final IntermediateResponse intermediateResponse)
1251  {
1252    if (logIntermediateResponses)
1253    {
1254      final JSONBuffer buffer = startLogMessage("intermediate-response", null,
1255           connectionInfo, messageID);
1256
1257      final String oid = intermediateResponse.getOID();
1258      if (oid != null)
1259      {
1260        buffer.appendString("oid", oid);
1261      }
1262
1263      buffer.appendBoolean("has-value",
1264           (intermediateResponse.getValue() != null));
1265
1266      appendControls(buffer, "control-oids",
1267           intermediateResponse.getControls());
1268
1269      logMessage(buffer, flushAfterRequestMessages);
1270    }
1271  }
1272
1273
1274
1275  /**
1276   * Starts generating a log message.
1277   *
1278   * @param  messageType     The message type for the log message.  It must not
1279   *                         be {@code null}.
1280   * @param  operationType   The operation type for the log message.  It may be
1281   *                         {@code null} if there is no associated operation
1282   *                         type.
1283   * @param  connectionInfo  Information about the connection with which the
1284   *                         message is associated.  It must not be
1285   *                         {@code null}.
1286   * @param  messageID       The LDAP message ID for the associated operation.
1287   *                         This will be ignored if the value is less than
1288   *                         zero.
1289   *
1290   * @return  A JSON buffer that may be used to construct the remainder of the
1291   *          log message.
1292   */
1293  @NotNull()
1294  private JSONBuffer startLogMessage(@NotNull final String messageType,
1295                          @Nullable final OperationType operationType,
1296                          @NotNull final LDAPConnectionInfo connectionInfo,
1297                          final int messageID)
1298  {
1299    JSONBuffer buffer = jsonBuffers.get();
1300    if (buffer == null)
1301    {
1302      buffer = new JSONBuffer();
1303      jsonBuffers.set(buffer);
1304    }
1305    else
1306    {
1307      buffer.clear();
1308    }
1309
1310    buffer.beginObject();
1311
1312    SimpleDateFormat timestampFormatter = timestampFormatters.get();
1313    if (timestampFormatter == null)
1314    {
1315      timestampFormatter =
1316           new SimpleDateFormat("yyyy'-'MM'-'dd'T'HH':'mm':'ss.SSS'Z'");
1317      timestampFormatter.setTimeZone(StaticUtils.getUTCTimeZone());
1318      timestampFormatters.set(timestampFormatter);
1319    }
1320
1321    buffer.appendString("timestamp", timestampFormatter.format(new Date()));
1322    buffer.appendString("message-type", messageType);
1323
1324    if (operationType != null)
1325    {
1326      switch (operationType)
1327      {
1328        case ABANDON:
1329          buffer.appendString("operation-type", "abandon");
1330          break;
1331        case ADD:
1332          buffer.appendString("operation-type", "add");
1333          break;
1334        case BIND:
1335          buffer.appendString("operation-type", "bind");
1336          break;
1337        case COMPARE:
1338          buffer.appendString("operation-type", "compare");
1339          break;
1340        case DELETE:
1341          buffer.appendString("operation-type", "delete");
1342          break;
1343        case EXTENDED:
1344          buffer.appendString("operation-type", "extended");
1345          break;
1346        case MODIFY:
1347          buffer.appendString("operation-type", "modify");
1348          break;
1349        case MODIFY_DN:
1350          buffer.appendString("operation-type", "modify-dn");
1351          break;
1352        case SEARCH:
1353          buffer.appendString("operation-type", "search");
1354          break;
1355        case UNBIND:
1356          buffer.appendString("operation-type", "unbind");
1357          break;
1358      }
1359    }
1360
1361    buffer.appendNumber("connection-id", connectionInfo.getConnectionID());
1362
1363    final String connectionName = connectionInfo.getConnectionName();
1364    if (connectionName != null)
1365    {
1366      buffer.appendString("connection-name", connectionName);
1367    }
1368
1369    final String connectionPoolName = connectionInfo.getConnectionPoolName();
1370    if (connectionPoolName != null)
1371    {
1372      buffer.appendString("connection-pool-name", connectionPoolName);
1373    }
1374
1375    if (messageID >= 0)
1376    {
1377      buffer.appendNumber("ldap-message-id", messageID);
1378    }
1379
1380    return buffer;
1381  }
1382
1383
1384
1385  /**
1386   * Appends information about an exception to the provided buffer.
1387   *
1388   * @param  buffer     The buffer to which the exception should be appended.
1389   *                    It must not be {@code null}.
1390   * @param  fieldName  The name of the field to use for the exception
1391   *                    object that is appended to the buffer.  It must not be
1392   *                    {@code null}.
1393   * @param  exception  The exception to be appended.  It must not be
1394   *                    {@code null}.
1395   */
1396  private void appendException(@NotNull final JSONBuffer buffer,
1397                               @NotNull final String fieldName,
1398                               @NotNull final Throwable exception)
1399  {
1400    buffer.beginObject(fieldName);
1401
1402    buffer.appendString("exception-class", exception.getClass().getName());
1403
1404    final String message = exception.getMessage();
1405    if (message != null)
1406    {
1407      buffer.appendString("message", message);
1408    }
1409
1410    buffer.beginArray("stack-trace-frames");
1411    for (final StackTraceElement frame : exception.getStackTrace())
1412    {
1413      buffer.beginObject();
1414
1415      buffer.appendString("class", frame.getClassName());
1416      buffer.appendString("method", frame.getMethodName());
1417
1418      final String fileName = frame.getFileName();
1419      if (fileName != null)
1420      {
1421        buffer.appendString("file", fileName);
1422      }
1423
1424      if (frame.isNativeMethod())
1425      {
1426        buffer.appendBoolean("is-native-method", true);
1427      }
1428      else
1429      {
1430        final int lineNumber = frame.getLineNumber();
1431        if (lineNumber > 0)
1432        {
1433          buffer.appendNumber("line-number", lineNumber);
1434        }
1435      }
1436
1437      buffer.endObject();
1438    }
1439    buffer.endArray();
1440
1441    final Throwable cause = exception.getCause();
1442    if (cause != null)
1443    {
1444      appendException(buffer, "caused-by", cause);
1445    }
1446
1447    buffer.endObject();
1448  }
1449
1450
1451
1452  /**
1453   * Appends information about the given set of controls to the provided buffer,
1454   * if control OIDs should be included in log messages.
1455   *
1456   * @param  buffer     The buffer to which the information should be appended.
1457   *                    It must not be {@code null}.
1458   * @param  fieldName  The name to use for the JSON field.  It must not be
1459   *                    {@code null}.
1460   * @param  controls   The controls to be appended.  It must not be
1461   *                    {@code null} but may be empty.
1462   */
1463  private void appendControls(@NotNull final JSONBuffer buffer,
1464                              @NotNull final String fieldName,
1465                              @NotNull final Control... controls)
1466  {
1467    if (includeControlOIDs && (controls.length > 0))
1468    {
1469      buffer.beginArray(fieldName);
1470      for (final Control c : controls)
1471      {
1472        buffer.appendString(c.getOID());
1473      }
1474      buffer.endArray();
1475    }
1476  }
1477
1478
1479
1480  /**
1481   * Appends information about the given set of controls to the provided buffer,
1482   * if control OIDs should be included in log messages.
1483   *
1484   * @param  buffer     The buffer to which the information should be appended.
1485   *                    It must not be {@code null}.
1486   * @param  fieldName  The name to use for the JSON field.  It must not be
1487   *                    {@code null}.
1488   * @param  controls   The controls to be appended.  It must not be
1489   *                    {@code null} but may be empty.
1490   */
1491  private void appendControls(@NotNull final JSONBuffer buffer,
1492                              @NotNull final String fieldName,
1493                              @NotNull final List<Control> controls)
1494  {
1495    if (includeControlOIDs && (! controls.isEmpty()))
1496    {
1497      buffer.beginArray(fieldName);
1498      for (final Control c : controls)
1499      {
1500        buffer.appendString(c.getOID());
1501      }
1502      buffer.endArray();
1503    }
1504  }
1505
1506
1507
1508  /**
1509   * Appends a DN to the provided buffer, redacting any attribute values as
1510   * appropriate.
1511   *
1512   * @param  buffer     The buffer to which the information should be appended.
1513   *                    It must not be {@code null}.
1514   * @param  fieldName  The name to use for the JSON field.  It must not be
1515   *                    {@code null}.
1516   * @param  dn         The DN to be appended.  It must not be {@code null} but
1517   *                    may be empty.
1518   */
1519  private void appendDN(@NotNull final JSONBuffer buffer,
1520                        @NotNull final String fieldName,
1521                        @NotNull final String dn)
1522  {
1523    if (fullAttributesToRedact.isEmpty())
1524    {
1525      buffer.appendString(fieldName, dn);
1526      return;
1527    }
1528
1529    final DN parsedDN;
1530    try
1531    {
1532      parsedDN = new DN(dn);
1533    }
1534    catch (final Exception e)
1535    {
1536      Debug.debugException(e);
1537      buffer.appendString(fieldName, dn);
1538      return;
1539    }
1540
1541    boolean redactionNeeded = false;
1542    final RDN[] originalRDNs = parsedDN.getRDNs();
1543    for (final RDN rdn : originalRDNs)
1544    {
1545      for (final String attributeName : rdn.getAttributeNames())
1546      {
1547        if (fullAttributesToRedact.contains(
1548             StaticUtils.toLowerCase(attributeName)))
1549        {
1550          redactionNeeded = true;
1551          break;
1552        }
1553      }
1554    }
1555
1556    if (redactionNeeded)
1557    {
1558      final RDN[] newRDNs = new RDN[originalRDNs.length];
1559      for (int i=0; i < originalRDNs.length; i++)
1560      {
1561        final RDN rdn = originalRDNs[i];
1562        final String[] names = rdn.getAttributeNames();
1563        final byte[][] values = new byte[names.length][];
1564        for (int j=0; j < names.length; j++)
1565        {
1566          final String lowerName = StaticUtils.toLowerCase(names[j]);
1567          if (fullAttributesToRedact.contains(lowerName))
1568          {
1569            values[j] = REDACTED_VALUE_BYTES;
1570          }
1571          else
1572          {
1573            values[j] = rdn.getByteArrayAttributeValues()[j];
1574          }
1575        }
1576
1577        newRDNs[i] = new RDN(names, values, rdn.getSchema());
1578      }
1579
1580      buffer.appendString(fieldName, new DN(newRDNs).toString());
1581    }
1582    else
1583    {
1584      buffer.appendString(fieldName, dn);
1585    }
1586  }
1587
1588
1589
1590  /**
1591   * Appends the given list of attributes to the provided buffer, redacting any
1592   * values as appropriate.
1593   *
1594   * @param  buffer         The buffer to which the information should be
1595   *                        appended.  It must not be {@code null}.
1596   * @param  fieldName      The name of the field to use for the attribute
1597   *                        array.  It must not be {@code null}.
1598   * @param  attributes     The attributes to be appended.  It must not be
1599   *                        {@code null}, but may be empty.
1600   * @param  includeValues  Indicates whether to include the values of the
1601   *                        attributes.
1602   */
1603  private void appendAttributes(@NotNull final JSONBuffer buffer,
1604                                @NotNull final String fieldName,
1605                                @NotNull final List<Attribute> attributes,
1606                                final boolean includeValues)
1607  {
1608    buffer.beginArray(fieldName);
1609
1610    for (final Attribute attribute : attributes)
1611    {
1612      if (includeValues)
1613      {
1614        buffer.beginObject();
1615        buffer.appendString("name", attribute.getName());
1616        buffer.beginArray("values");
1617
1618        final String baseName =
1619             StaticUtils.toLowerCase(attribute.getBaseName());
1620        if (fullAttributesToRedact.contains(baseName))
1621        {
1622          for (final String value : attribute.getValues())
1623          {
1624            buffer.appendString(REDACTED_VALUE_STRING);
1625          }
1626        }
1627        else
1628        {
1629          for (final String value : attribute.getValues())
1630          {
1631            buffer.appendString(value);
1632          }
1633        }
1634
1635        buffer.endArray();
1636        buffer.endObject();
1637      }
1638      else
1639      {
1640        buffer.appendString(attribute.getName());
1641      }
1642    }
1643
1644    buffer.endArray();
1645  }
1646
1647
1648
1649  /**
1650   * Redacts the provided filter, if necessary.
1651   *
1652   * @param  filter  The filter to be redacted.  It must not be {@code null}.
1653   *
1654   * @return  The redacted filter.
1655   */
1656  @NotNull()
1657  private Filter redactFilter(@NotNull final Filter filter)
1658  {
1659    switch (filter.getFilterType())
1660    {
1661      case Filter.FILTER_TYPE_AND:
1662        final Filter[] currentANDComps = filter.getComponents();
1663        final Filter[] newANDComps = new Filter[currentANDComps.length];
1664        for (int i=0; i < currentANDComps.length; i++)
1665        {
1666          newANDComps[i] = redactFilter(currentANDComps[i]);
1667        }
1668        return Filter.createANDFilter(newANDComps);
1669
1670      case Filter.FILTER_TYPE_OR:
1671        final Filter[] currentORComps = filter.getComponents();
1672        final Filter[] newORComps = new Filter[currentORComps.length];
1673        for (int i=0; i < currentORComps.length; i++)
1674        {
1675          newORComps[i] = redactFilter(currentORComps[i]);
1676        }
1677        return Filter.createORFilter(newORComps);
1678
1679      case Filter.FILTER_TYPE_NOT:
1680        return Filter.createNOTFilter(redactFilter(filter.getNOTComponent()));
1681
1682      case Filter.FILTER_TYPE_EQUALITY:
1683        return Filter.createEqualityFilter(filter.getAttributeName(),
1684             redactAssertionValue(filter));
1685
1686      case Filter.FILTER_TYPE_GREATER_OR_EQUAL:
1687        return Filter.createGreaterOrEqualFilter(filter.getAttributeName(),
1688             redactAssertionValue(filter));
1689
1690      case Filter.FILTER_TYPE_LESS_OR_EQUAL:
1691        return Filter.createLessOrEqualFilter(filter.getAttributeName(),
1692             redactAssertionValue(filter));
1693
1694      case Filter.FILTER_TYPE_APPROXIMATE_MATCH:
1695        return Filter.createApproximateMatchFilter(filter.getAttributeName(),
1696             redactAssertionValue(filter));
1697
1698      case Filter.FILTER_TYPE_EXTENSIBLE_MATCH:
1699        return Filter.createExtensibleMatchFilter(filter.getAttributeName(),
1700             filter.getMatchingRuleID(), filter.getDNAttributes(),
1701             redactAssertionValue(filter));
1702
1703      case Filter.FILTER_TYPE_SUBSTRING:
1704        final String baseName = StaticUtils.toLowerCase(Attribute.getBaseName(
1705             filter.getAttributeName()));
1706        if (fullAttributesToRedact.contains(baseName))
1707        {
1708          final String[] redactedSubAnyStrings =
1709               new String[filter.getSubAnyStrings().length];
1710          Arrays.fill(redactedSubAnyStrings, REDACTED_VALUE_STRING);
1711
1712          return Filter.createSubstringFilter(filter.getAttributeName(),
1713               filter.getSubInitialString() == null
1714                    ? null
1715                    : REDACTED_VALUE_STRING,
1716               redactedSubAnyStrings,
1717               filter.getSubFinalString() == null
1718                    ? null
1719                    : REDACTED_VALUE_STRING);
1720        }
1721        else
1722        {
1723          return Filter.createSubstringFilter(filter.getAttributeName(),
1724               filter.getSubInitialString(), filter.getSubAnyStrings(),
1725               filter.getSubFinalString());
1726        }
1727
1728      case Filter.FILTER_TYPE_PRESENCE:
1729      default:
1730        return filter;
1731    }
1732  }
1733
1734
1735
1736  /**
1737   * Retrieves an assertion value to use for a redacted filter.
1738   *
1739   * @param  filter  The filter for which to obtain the assertion value.
1740   *
1741   * @return  The assertion value to use for a redacted filter.
1742   */
1743  @NotNull()
1744  private String redactAssertionValue(@NotNull final Filter filter)
1745  {
1746    final String attributeName = filter.getAttributeName();
1747    if (attributeName == null)
1748    {
1749      return filter.getAssertionValue();
1750    }
1751
1752    final String baseName =
1753         StaticUtils.toLowerCase(Attribute.getBaseName(attributeName));
1754    if (fullAttributesToRedact.contains(baseName))
1755    {
1756      return REDACTED_VALUE_STRING;
1757    }
1758    else
1759    {
1760      return filter.getAssertionValue();
1761    }
1762  }
1763
1764
1765
1766  /**
1767   * Logs a final result message for the provided result.  If the result is a
1768   * {@code BindResult}, an {@code ExtendedResult}, or a {@code SearchResult},
1769   * then additional information about that type of result may also be included.
1770   *
1771   * @param  connectionInfo  Information about the connection with which the
1772   *                         result is associated.  It must not be
1773   *                         {@code null}.
1774   * @param  operationType   The operation type for the log message.  It must
1775   *                         not be {@code null}.
1776   * @param  messageID       The LDAP message ID for the associated operation.
1777   * @param  result          The result to be logged.
1778   */
1779  private void logLDAPResult(@NotNull final LDAPConnectionInfo connectionInfo,
1780                             @NotNull final OperationType operationType,
1781                             final int messageID,
1782                             @NotNull final LDAPResult result)
1783  {
1784    if (logFinalResults && operationTypes.contains(operationType))
1785    {
1786      final JSONBuffer buffer = startLogMessage("result", operationType,
1787           connectionInfo, messageID);
1788
1789      buffer.appendNumber("result-code-value",
1790           result.getResultCode().intValue());
1791      buffer.appendString("result-code-name", result.getResultCode().getName());
1792
1793      final String diagnosticMessage = result.getDiagnosticMessage();
1794      if (diagnosticMessage != null)
1795      {
1796        buffer.appendString("diagnostic-message", diagnosticMessage);
1797      }
1798
1799      final String matchedDN = result.getMatchedDN();
1800      if (matchedDN != null)
1801      {
1802        buffer.appendString("matched-dn", matchedDN);
1803      }
1804
1805      final String[] referralURLs = result.getReferralURLs();
1806      if ((referralURLs != null) && (referralURLs.length > 0))
1807      {
1808        buffer.beginArray("referral-urls");
1809        for (final String url : referralURLs)
1810        {
1811          buffer.appendString(url);
1812        }
1813        buffer.endArray();
1814      }
1815
1816      if (result instanceof BindResult)
1817      {
1818        final BindResult bindResult = (BindResult) result;
1819        if (bindResult.getServerSASLCredentials() != null)
1820        {
1821          buffer.appendBoolean("has-server-sasl-credentials", true);
1822        }
1823      }
1824      else if (result instanceof ExtendedResult)
1825      {
1826        final ExtendedResult extendedResult = (ExtendedResult) result;
1827        final String oid = extendedResult.getOID();
1828        if (oid != null)
1829        {
1830          buffer.appendString("oid", oid);
1831        }
1832
1833        buffer.appendBoolean("has-value", (extendedResult.getValue() != null));
1834      }
1835
1836      appendControls(buffer, "control-oids", result.getResponseControls());
1837
1838      logMessage(buffer, flushAfterFinalResultMessages);
1839    }
1840  }
1841
1842
1843
1844  /**
1845   * Finalizes the message and writes it to the log handler, optionally flushing
1846   * the handler after the message has been written.
1847   *
1848   * @param  buffer        The buffer containing the message to be written.
1849   * @param  flushHandler  Indicates whether to flush the handler after the
1850   *                       message has been written.
1851   */
1852  private void logMessage(@NotNull final JSONBuffer buffer,
1853                          final boolean flushHandler)
1854  {
1855    buffer.endObject();
1856
1857    logHandler.publish(new LogRecord(Level.INFO, buffer.toString()));
1858
1859    if (flushHandler)
1860    {
1861      logHandler.flush();
1862    }
1863  }
1864}