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