001/*
002 * Copyright 2010-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2010-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) 2010-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.listener;
037
038
039
040import java.net.Socket;
041import java.text.DecimalFormat;
042import java.text.SimpleDateFormat;
043import java.util.Date;
044import java.util.List;
045import java.util.concurrent.ConcurrentHashMap;
046import java.util.concurrent.atomic.AtomicLong;
047import java.util.logging.Handler;
048import java.util.logging.Level;
049import java.util.logging.LogRecord;
050
051import com.unboundid.ldap.protocol.AbandonRequestProtocolOp;
052import com.unboundid.ldap.protocol.AddRequestProtocolOp;
053import com.unboundid.ldap.protocol.AddResponseProtocolOp;
054import com.unboundid.ldap.protocol.BindRequestProtocolOp;
055import com.unboundid.ldap.protocol.BindResponseProtocolOp;
056import com.unboundid.ldap.protocol.CompareRequestProtocolOp;
057import com.unboundid.ldap.protocol.CompareResponseProtocolOp;
058import com.unboundid.ldap.protocol.DeleteRequestProtocolOp;
059import com.unboundid.ldap.protocol.DeleteResponseProtocolOp;
060import com.unboundid.ldap.protocol.ExtendedRequestProtocolOp;
061import com.unboundid.ldap.protocol.ExtendedResponseProtocolOp;
062import com.unboundid.ldap.protocol.LDAPMessage;
063import com.unboundid.ldap.protocol.ModifyRequestProtocolOp;
064import com.unboundid.ldap.protocol.ModifyResponseProtocolOp;
065import com.unboundid.ldap.protocol.ModifyDNRequestProtocolOp;
066import com.unboundid.ldap.protocol.ModifyDNResponseProtocolOp;
067import com.unboundid.ldap.protocol.SearchRequestProtocolOp;
068import com.unboundid.ldap.protocol.SearchResultDoneProtocolOp;
069import com.unboundid.ldap.protocol.SearchResultEntryProtocolOp;
070import com.unboundid.ldap.protocol.UnbindRequestProtocolOp;
071import com.unboundid.ldap.sdk.Control;
072import com.unboundid.ldap.sdk.LDAPException;
073import com.unboundid.ldap.sdk.ResultCode;
074import com.unboundid.util.NotMutable;
075import com.unboundid.util.NotNull;
076import com.unboundid.util.Nullable;
077import com.unboundid.util.ObjectPair;
078import com.unboundid.util.StaticUtils;
079import com.unboundid.util.ThreadSafety;
080import com.unboundid.util.ThreadSafetyLevel;
081import com.unboundid.util.Validator;
082import com.unboundid.util.json.JSONBuffer;
083
084
085
086/**
087 * This class provides a request handler that may be used to log each request
088 * and result using the Java logging framework.  Messages will be formatted as
089 * JSON objects.  It will be also be associated with another request handler
090 * that will actually be used to handle the request.
091 */
092@NotMutable()
093@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
094public final class JSONAccessLogRequestHandler
095       extends LDAPListenerRequestHandler
096       implements SearchEntryTransformer
097{
098  // The operation ID counter that will be used for this request handler
099  // instance.
100  @Nullable private final AtomicLong nextOperationID;
101
102  // A map used to correlate the number of search result entries returned for a
103  // particular message ID.
104  @NotNull private final ConcurrentHashMap<Integer,AtomicLong> entryCounts;
105
106  // The log handler that will be used to log the messages.
107  @NotNull private final Handler logHandler;
108
109  // The client connection with which this request handler is associated.
110  @Nullable private final LDAPListenerClientConnection clientConnection;
111
112  // The request handler that actually will be used to process any requests
113  // received.
114  @NotNull private final LDAPListenerRequestHandler requestHandler;
115
116  // The thread-local decimal formatters that will be used to format etime
117  // values.
118  @NotNull private final ThreadLocal<DecimalFormat> decimalFormatters;
119
120  // The thread-local JSON buffers that will be used to format log message
121  // objects.
122  @NotNull private final ThreadLocal<JSONBuffer> jsonBuffers;
123
124  // The thread-local date formatters that will be used to format timestamps.
125  @NotNull private final ThreadLocal<SimpleDateFormat> timestampFormatters;
126
127
128
129  /**
130   * Creates a new JSON-formatted access log request handler that will log
131   * request and result messages using the provided log handler, and will
132   * process client requests using the provided request handler.
133   *
134   * @param  logHandler      The log handler that will be used to log request
135   *                         and result messages.  Note that all messages will
136   *                         be logged at the INFO level.  It must not be
137   *                         {@code null}.  Note that the log handler will not
138   *                         be automatically closed when the associated
139   *                         listener is shut down.
140   * @param  requestHandler  The request handler that will actually be used to
141   *                         process any requests received.  It must not be
142   *                         {@code null}.
143   */
144  public JSONAccessLogRequestHandler(@NotNull final Handler logHandler,
145              @NotNull final LDAPListenerRequestHandler requestHandler)
146  {
147    Validator.ensureNotNull(logHandler, requestHandler);
148
149    this.logHandler = logHandler;
150    this.requestHandler = requestHandler;
151
152    nextOperationID = null;
153    clientConnection = null;
154    entryCounts = new ConcurrentHashMap<>(StaticUtils.computeMapCapacity(50));
155    jsonBuffers = new ThreadLocal<>();
156    timestampFormatters = new ThreadLocal<>();
157    decimalFormatters = new ThreadLocal<>();
158  }
159
160
161
162  /**
163   * Creates a new JSON-formatted access log request handler that will log
164   * request and result messages using the provided log handler, and will
165   * process client requests using the provided request handler.
166   *
167   * @param  logHandler           The log handler that will be used to log
168   *                              request and result messages.  Note that all
169   *                              messages will be logged at the INFO level.  It
170   *                              must not be {@code null}.
171   * @param  requestHandler       The request handler that will actually be used
172   *                              to process any requests received.  It must not
173   *                              be {@code null}.
174   * @param  clientConnection     The client connection with which this instance
175   *                              is associated.
176   * @param  jsonBuffers          The thread-local JSON buffers that will be
177   *                              used to format log message objects.
178   * @param  timestampFormatters  The thread-local date formatters that will be
179   *                              used to format timestamps.
180   * @param  decimalFormatters    The thread-local decimal formatters that
181   *                              will be used to format etime values.
182   */
183  private JSONAccessLogRequestHandler(@NotNull final Handler logHandler,
184               @NotNull final LDAPListenerRequestHandler requestHandler,
185               @NotNull final LDAPListenerClientConnection clientConnection,
186               @NotNull final ThreadLocal<JSONBuffer> jsonBuffers,
187               @NotNull final ThreadLocal<SimpleDateFormat> timestampFormatters,
188               @NotNull final ThreadLocal<DecimalFormat> decimalFormatters)
189  {
190    this.logHandler = logHandler;
191    this.requestHandler = requestHandler;
192    this.clientConnection = clientConnection;
193    this.jsonBuffers = jsonBuffers;
194    this.timestampFormatters = timestampFormatters;
195    this.decimalFormatters = decimalFormatters;
196
197    nextOperationID = new AtomicLong(0L);
198    entryCounts = new ConcurrentHashMap<>(StaticUtils.computeMapCapacity(50));
199  }
200
201
202
203  /**
204   * {@inheritDoc}
205   */
206  @Override()
207  @NotNull()
208  public JSONAccessLogRequestHandler newInstance(
209              @NotNull final LDAPListenerClientConnection connection)
210         throws LDAPException
211  {
212    final JSONAccessLogRequestHandler h =
213         new JSONAccessLogRequestHandler(logHandler,
214              requestHandler.newInstance(connection), connection, jsonBuffers,
215              timestampFormatters, decimalFormatters);
216    connection.addSearchEntryTransformer(h);
217
218    final JSONBuffer buffer = h.getConnectionHeader("connect");
219
220    final Socket s = connection.getSocket();
221    buffer.appendString("from-address", s.getInetAddress().getHostAddress());
222    buffer.appendNumber("from-port", s.getPort());
223    buffer.appendString("to-address", s.getLocalAddress().getHostAddress());
224    buffer.appendNumber("to-port", s.getLocalPort());
225    buffer.endObject();
226
227    logHandler.publish(new LogRecord(Level.INFO, buffer.toString()));
228    logHandler.flush();
229
230    return h;
231  }
232
233
234
235  /**
236   * {@inheritDoc}
237   */
238  @Override()
239  public void closeInstance()
240  {
241    final JSONBuffer buffer = getConnectionHeader("disconnect");
242    buffer.endObject();
243
244    logHandler.publish(new LogRecord(Level.INFO, buffer.toString()));
245    logHandler.flush();
246
247    requestHandler.closeInstance();
248  }
249
250
251
252  /**
253   * {@inheritDoc}
254   */
255  @Override()
256  public void processAbandonRequest(final int messageID,
257                   @NotNull final AbandonRequestProtocolOp request,
258                   @NotNull final List<Control> controls)
259  {
260    final JSONBuffer buffer = getRequestHeader("abandon",
261         nextOperationID.incrementAndGet(), messageID);
262
263    buffer.appendNumber("id-to-abandon", request.getIDToAbandon());
264    buffer.endObject();
265
266    logHandler.publish(new LogRecord(Level.INFO, buffer.toString()));
267    logHandler.flush();
268
269    requestHandler.processAbandonRequest(messageID, request, controls);
270  }
271
272
273
274  /**
275   * {@inheritDoc}
276   */
277  @Override()
278  @NotNull()
279  public LDAPMessage processAddRequest(final int messageID,
280                          @NotNull final AddRequestProtocolOp request,
281                          @NotNull final List<Control> controls)
282  {
283    final long opID = nextOperationID.getAndIncrement();
284    final JSONBuffer buffer = getRequestHeader("add", opID, messageID);
285
286    buffer.appendString("dn", request.getDN());
287    buffer.endObject();
288
289    logHandler.publish(new LogRecord(Level.INFO, buffer.toString()));
290    logHandler.flush();
291
292    final long startTimeNanos = System.nanoTime();
293    final LDAPMessage responseMessage = requestHandler.processAddRequest(
294         messageID, request, controls);
295    final long eTimeNanos = System.nanoTime() - startTimeNanos;
296    final AddResponseProtocolOp protocolOp =
297         responseMessage.getAddResponseProtocolOp();
298
299    generateResponse(buffer, "add", opID, messageID,
300         protocolOp.getResultCode(), protocolOp.getDiagnosticMessage(),
301         protocolOp.getMatchedDN(), protocolOp.getReferralURLs(), eTimeNanos);
302    buffer.endObject();
303
304    logHandler.publish(new LogRecord(Level.INFO, buffer.toString()));
305    logHandler.flush();
306
307    return responseMessage;
308  }
309
310
311
312  /**
313   * {@inheritDoc}
314   */
315  @Override()
316  @NotNull()
317  public LDAPMessage processBindRequest(final int messageID,
318                          @NotNull final BindRequestProtocolOp request,
319                          @NotNull final List<Control> controls)
320  {
321    final long opID = nextOperationID.getAndIncrement();
322
323    final JSONBuffer buffer = getRequestHeader("bind", opID, messageID);
324    buffer.appendNumber("ldap-version", request.getVersion());
325    buffer.appendString("dn", request.getBindDN());
326
327    switch (request.getCredentialsType())
328    {
329      case BindRequestProtocolOp.CRED_TYPE_SIMPLE:
330        buffer.appendString("authentication-type", "simple");
331        break;
332
333      case BindRequestProtocolOp.CRED_TYPE_SASL:
334        buffer.appendString("authentication-type", "sasl");
335        buffer.appendString("sasl-mechanism", request.getSASLMechanism());
336        break;
337    }
338    buffer.endObject();
339
340    logHandler.publish(new LogRecord(Level.INFO, buffer.toString()));
341    logHandler.flush();
342
343    final long startTimeNanos = System.nanoTime();
344    final LDAPMessage responseMessage = requestHandler.processBindRequest(
345         messageID, request, controls);
346    final long eTimeNanos = System.nanoTime() - startTimeNanos;
347    final BindResponseProtocolOp protocolOp =
348         responseMessage.getBindResponseProtocolOp();
349
350    generateResponse(buffer, "bind", opID, messageID,
351         protocolOp.getResultCode(), protocolOp.getDiagnosticMessage(),
352         protocolOp.getMatchedDN(), protocolOp.getReferralURLs(), eTimeNanos);
353    buffer.endObject();
354
355    logHandler.publish(new LogRecord(Level.INFO, buffer.toString()));
356    logHandler.flush();
357
358    return responseMessage;
359  }
360
361
362
363  /**
364   * {@inheritDoc}
365   */
366  @Override()
367  @NotNull()
368  public LDAPMessage processCompareRequest(final int messageID,
369                          @NotNull final CompareRequestProtocolOp request,
370                          @NotNull final List<Control> controls)
371  {
372    final long opID = nextOperationID.getAndIncrement();
373
374    final JSONBuffer buffer = getRequestHeader("compare", opID, messageID);
375    buffer.appendString("dn", request.getDN());
376    buffer.appendString("attribute-type", request.getAttributeName());
377    buffer.endObject();
378
379    logHandler.publish(new LogRecord(Level.INFO, buffer.toString()));
380    logHandler.flush();
381
382    final long startTimeNanos = System.nanoTime();
383    final LDAPMessage responseMessage = requestHandler.processCompareRequest(
384         messageID, request, controls);
385    final long eTimeNanos = System.nanoTime() - startTimeNanos;
386    final CompareResponseProtocolOp protocolOp =
387         responseMessage.getCompareResponseProtocolOp();
388
389    generateResponse(buffer, "compare", opID, messageID,
390         protocolOp.getResultCode(), protocolOp.getDiagnosticMessage(),
391         protocolOp.getMatchedDN(), protocolOp.getReferralURLs(), eTimeNanos);
392    buffer.endObject();
393
394    logHandler.publish(new LogRecord(Level.INFO, buffer.toString()));
395    logHandler.flush();
396
397    return responseMessage;
398  }
399
400
401
402  /**
403   * {@inheritDoc}
404   */
405  @Override()
406  @NotNull()
407  public LDAPMessage processDeleteRequest(final int messageID,
408                          @NotNull final DeleteRequestProtocolOp request,
409                          @NotNull final List<Control> controls)
410  {
411    final long opID = nextOperationID.getAndIncrement();
412
413    final JSONBuffer buffer = getRequestHeader("delete", opID, messageID);
414    buffer.appendString("dn", request.getDN());
415    buffer.endObject();
416
417    logHandler.publish(new LogRecord(Level.INFO, buffer.toString()));
418    logHandler.flush();
419
420    final long startTimeNanos = System.nanoTime();
421    final LDAPMessage responseMessage = requestHandler.processDeleteRequest(
422         messageID, request, controls);
423    final long eTimeNanos = System.nanoTime() - startTimeNanos;
424    final DeleteResponseProtocolOp protocolOp =
425         responseMessage.getDeleteResponseProtocolOp();
426
427    generateResponse(buffer, "delete", opID, messageID,
428         protocolOp.getResultCode(),
429         protocolOp.getDiagnosticMessage(), protocolOp.getMatchedDN(),
430         protocolOp.getReferralURLs(), eTimeNanos);
431    buffer.endObject();
432
433    logHandler.publish(new LogRecord(Level.INFO, buffer.toString()));
434    logHandler.flush();
435
436    return responseMessage;
437  }
438
439
440
441  /**
442   * {@inheritDoc}
443   */
444  @Override()
445  @NotNull()
446  public LDAPMessage processExtendedRequest(final int messageID,
447                          @NotNull final ExtendedRequestProtocolOp request,
448                          @NotNull final List<Control> controls)
449  {
450    final long opID = nextOperationID.getAndIncrement();
451
452    final JSONBuffer buffer = getRequestHeader("extended", opID, messageID);
453    buffer.appendString("request-oid", request.getOID());
454    buffer.endObject();
455
456    logHandler.publish(new LogRecord(Level.INFO, buffer.toString()));
457    logHandler.flush();
458
459    final long startTimeNanos = System.nanoTime();
460    final LDAPMessage responseMessage = requestHandler.processExtendedRequest(
461         messageID, request, controls);
462    final long eTimeNanos = System.nanoTime() - startTimeNanos;
463    final ExtendedResponseProtocolOp protocolOp =
464         responseMessage.getExtendedResponseProtocolOp();
465
466    generateResponse(buffer, "extended", opID, messageID,
467         protocolOp.getResultCode(), protocolOp.getDiagnosticMessage(),
468         protocolOp.getMatchedDN(), protocolOp.getReferralURLs(), eTimeNanos);
469
470    final String responseOID = protocolOp.getResponseOID();
471    if (responseOID != null)
472    {
473      buffer.appendString("response-oid", responseOID);
474    }
475
476    buffer.endObject();
477
478    logHandler.publish(new LogRecord(Level.INFO, buffer.toString()));
479    logHandler.flush();
480
481    return responseMessage;
482  }
483
484
485
486  /**
487   * {@inheritDoc}
488   */
489  @Override()
490  @NotNull()
491  public LDAPMessage processModifyRequest(final int messageID,
492                          @NotNull final ModifyRequestProtocolOp request,
493                          @NotNull final List<Control> controls)
494  {
495    final long opID = nextOperationID.getAndIncrement();
496
497    final JSONBuffer buffer = getRequestHeader("modify", opID, messageID);
498    buffer.appendString("dn", request.getDN());
499    buffer.endObject();
500
501    logHandler.publish(new LogRecord(Level.INFO, buffer.toString()));
502    logHandler.flush();
503
504    final long startTimeNanos = System.nanoTime();
505    final LDAPMessage responseMessage = requestHandler.processModifyRequest(
506         messageID, request, controls);
507    final long eTimeNanos = System.nanoTime() - startTimeNanos;
508    final ModifyResponseProtocolOp protocolOp =
509         responseMessage.getModifyResponseProtocolOp();
510
511    generateResponse(buffer, "modify", opID, messageID,
512         protocolOp.getResultCode(), protocolOp.getDiagnosticMessage(),
513         protocolOp.getMatchedDN(), protocolOp.getReferralURLs(), eTimeNanos);
514    buffer.endObject();
515
516    logHandler.publish(new LogRecord(Level.INFO, buffer.toString()));
517    logHandler.flush();
518
519    return responseMessage;
520  }
521
522
523
524  /**
525   * {@inheritDoc}
526   */
527  @Override()
528  @NotNull()
529  public LDAPMessage processModifyDNRequest(final int messageID,
530                          @NotNull final ModifyDNRequestProtocolOp request,
531                          @NotNull final List<Control> controls)
532  {
533    final long opID = nextOperationID.getAndIncrement();
534
535    final JSONBuffer buffer = getRequestHeader("modify-dn", opID, messageID);
536    buffer.appendString("dn", request.getDN());
537    buffer.appendString("new-rdn", request.getNewRDN());
538    buffer.appendBoolean("delete-old-rdn", request.deleteOldRDN());
539
540    final String newSuperior = request.getNewSuperiorDN();
541    if (newSuperior != null)
542    {
543      buffer.appendString("new-superior", newSuperior);
544    }
545    buffer.endObject();
546
547    logHandler.publish(new LogRecord(Level.INFO, buffer.toString()));
548    logHandler.flush();
549
550    final long startTimeNanos = System.nanoTime();
551    final LDAPMessage responseMessage = requestHandler.processModifyDNRequest(
552         messageID, request, controls);
553    final long eTimeNanos = System.nanoTime() - startTimeNanos;
554    final ModifyDNResponseProtocolOp protocolOp =
555         responseMessage.getModifyDNResponseProtocolOp();
556
557    generateResponse(buffer, "modify-dn", opID, messageID,
558         protocolOp.getResultCode(), protocolOp.getDiagnosticMessage(),
559         protocolOp.getMatchedDN(), protocolOp.getReferralURLs(), eTimeNanos);
560    buffer.endObject();
561
562    logHandler.publish(new LogRecord(Level.INFO, buffer.toString()));
563    logHandler.flush();
564
565    return responseMessage;
566  }
567
568
569
570  /**
571   * {@inheritDoc}
572   */
573  @Override()
574  @NotNull()
575  public LDAPMessage processSearchRequest(final int messageID,
576                          @NotNull final SearchRequestProtocolOp request,
577                          @NotNull final List<Control> controls)
578  {
579    final long opID = nextOperationID.getAndIncrement();
580
581    final JSONBuffer buffer = getRequestHeader("search", opID, messageID);
582    buffer.appendString("base", request.getBaseDN());
583    buffer.appendNumber("scope", request.getScope().intValue());
584    buffer.appendString("filter", request.getFilter().toString());
585
586    buffer.beginArray("requested-attributes");
587    for (final String requestedAttribute : request.getAttributes())
588    {
589      buffer.appendString(requestedAttribute);
590    }
591    buffer.endArray();
592    buffer.endObject();
593
594    logHandler.publish(new LogRecord(Level.INFO, buffer.toString()));
595    logHandler.flush();
596
597    final AtomicLong entryCounter = new AtomicLong(0L);
598    entryCounts.put(messageID, entryCounter);
599
600    try
601    {
602      final long startTimeNanos = System.nanoTime();
603      final LDAPMessage responseMessage = requestHandler.processSearchRequest(
604           messageID, request, controls);
605      final long eTimeNanos = System.nanoTime() - startTimeNanos;
606      final SearchResultDoneProtocolOp protocolOp =
607           responseMessage.getSearchResultDoneProtocolOp();
608
609      generateResponse(buffer, "search", opID, messageID,
610           protocolOp.getResultCode(), protocolOp.getDiagnosticMessage(),
611           protocolOp.getMatchedDN(), protocolOp.getReferralURLs(), eTimeNanos);
612      buffer.appendNumber("entries-returned", entryCounter.get());
613      buffer.endObject();
614
615      logHandler.publish(new LogRecord(Level.INFO, buffer.toString()));
616      logHandler.flush();
617
618      return responseMessage;
619    }
620    finally
621    {
622      entryCounts.remove(messageID);
623    }
624  }
625
626
627
628  /**
629   * {@inheritDoc}
630   */
631  @Override()
632  public void processUnbindRequest(final int messageID,
633                   @NotNull final UnbindRequestProtocolOp request,
634                   @NotNull final List<Control> controls)
635  {
636    final JSONBuffer buffer = getRequestHeader("unbind",
637         nextOperationID.getAndIncrement(), messageID);
638    buffer.endObject();
639
640    logHandler.publish(new LogRecord(Level.INFO, buffer.toString()));
641    logHandler.flush();
642
643    requestHandler.processUnbindRequest(messageID, request, controls);
644  }
645
646
647
648  /**
649   * Retrieves a JSON buffer that can be used to construct a log message.
650   *
651   * @return  A JSON buffer that can be used to construct a log message.
652   */
653  @NotNull()
654  private JSONBuffer getBuffer()
655  {
656    JSONBuffer buffer = jsonBuffers.get();
657    if (buffer == null)
658    {
659      buffer = new JSONBuffer();
660      jsonBuffers.set(buffer);
661    }
662    else
663    {
664      buffer.clear();
665    }
666
667    return buffer;
668  }
669
670
671
672  /**
673   * Adds a timestamp to the provided buffer.
674   *
675   * @param  buffer  The buffer to which the timestamp should be added.
676   */
677  private void addTimestamp(@NotNull final JSONBuffer buffer)
678  {
679    SimpleDateFormat timestampFormatter = timestampFormatters.get();
680    if (timestampFormatter == null)
681    {
682      timestampFormatter =
683           new SimpleDateFormat("yyyy'-'MM'-'dd'T'HH':'mm':'ss.SSS'Z'");
684      timestampFormatter.setTimeZone(StaticUtils.getUTCTimeZone());
685      timestampFormatters.set(timestampFormatter);
686    }
687
688    buffer.appendString("timestamp", timestampFormatter.format(new Date()));
689  }
690
691
692
693  /**
694   * Retrieves a {@code JSONBuffer} with header information for a connect log
695   * message for the specified type of operation.
696   *
697   * @param  messageType  The type of operation being requested.
698   *
699   * @return  A {@code JSONBuffer} with header information appended for the
700   *          connection;
701   */
702  @NotNull()
703  private JSONBuffer getConnectionHeader(@NotNull final String messageType)
704  {
705    final JSONBuffer buffer = getBuffer();
706    buffer.beginObject();
707    addTimestamp(buffer);
708    buffer.appendString("message-type", messageType);
709    buffer.appendNumber("connection-id", clientConnection.getConnectionID());
710
711    return buffer;
712  }
713
714
715
716  /**
717   * Retrieves a {@code JSONBuffer} with header information for a request log
718   * message for the specified type of operation.
719   *
720   * @param  opType  The type of operation being requested.
721   * @param  opID    The operation ID for the request.
722   * @param  msgID   The message ID for the request.
723   *
724   * @return  A {@code StringBuilder} with header information appended for the
725   *          request;
726   */
727  @NotNull()
728  private JSONBuffer getRequestHeader(@NotNull final String opType,
729                                      final long opID, final int msgID)
730  {
731    final JSONBuffer buffer = getBuffer();
732    buffer.beginObject();
733    addTimestamp(buffer);
734    buffer.appendString("message-type", "request");
735    buffer.appendString("operation-type", opType);
736    buffer.appendNumber("connection-id", clientConnection.getConnectionID());
737    buffer.appendNumber("operation-id", opID);
738    buffer.appendNumber("message-id", msgID);
739
740    return buffer;
741  }
742
743
744
745  /**
746   * Updates the provided JSON buffer with information about the result of
747   * processing an operation.
748   *
749   * @param  buffer             The buffer to which the information will be
750   *                            written.  It will be cleared before adding any
751   *                            content.
752   * @param  opType             The type of operation that was processed.
753   * @param  opID               The operation ID for the response.
754   * @param  msgID              The message ID for the response.
755   * @param  resultCode         The result code for the response, if any.
756   * @param  diagnosticMessage  The diagnostic message for the response, if any.
757   * @param  matchedDN          The matched DN for the response, if any.
758   * @param  referralURLs       The referral URLs for the response, if any.
759   * @param  eTimeNanos         The length of time in nanoseconds required to
760   *                            process the operation.
761   */
762  private void generateResponse(@NotNull final JSONBuffer buffer,
763                                @NotNull final String opType,
764                                final long opID, final int msgID,
765                                final int resultCode,
766                                @Nullable final String diagnosticMessage,
767                                @Nullable final String matchedDN,
768                                @NotNull final List<String> referralURLs,
769                                final long eTimeNanos)
770  {
771    buffer.clear();
772
773    buffer.beginObject();
774    addTimestamp(buffer);
775    buffer.appendString("message-type", "response");
776    buffer.appendString("operation-type", opType);
777    buffer.appendNumber("connection-id", clientConnection.getConnectionID());
778    buffer.appendNumber("operation-id", opID);
779    buffer.appendNumber("message-id", msgID);
780    buffer.appendNumber("result-code-value", resultCode);
781
782    final ResultCode rc = ResultCode.valueOf(resultCode, null, false);
783    if (rc != null)
784    {
785      buffer.appendString("result-code-name", rc.getName());
786    }
787
788    if (diagnosticMessage != null)
789    {
790      buffer.appendString("diagnostic-message", diagnosticMessage);
791    }
792
793    if (matchedDN != null)
794    {
795      buffer.appendString("matched-dn", matchedDN);
796    }
797
798    if (! referralURLs.isEmpty())
799    {
800      buffer.beginArray("referral-urls");
801      for (final String url : referralURLs)
802      {
803        buffer.appendString(url);
804      }
805      buffer.endArray();
806    }
807
808    DecimalFormat decimalFormat = decimalFormatters.get();
809    if (decimalFormat == null)
810    {
811      decimalFormat = new DecimalFormat("0.000");
812      decimalFormatters.set(decimalFormat);
813    }
814    final double eTimeMillis = eTimeNanos / 1_000_000.0d;
815    buffer.appendNumber("processing-time-millis",
816         decimalFormat.format(eTimeMillis));
817  }
818
819
820
821  /**
822   * {@inheritDoc}
823   */
824  @Override()
825  @NotNull()
826  public ObjectPair<SearchResultEntryProtocolOp,Control[]> transformEntry(
827              final int messageID,
828              @NotNull final SearchResultEntryProtocolOp entry,
829              @NotNull final Control[] controls)
830  {
831    final AtomicLong l = entryCounts.get(messageID);
832    if (l != null)
833    {
834      l.incrementAndGet();
835    }
836
837    return new ObjectPair<>(entry, controls);
838  }
839}