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