001/*
002 * Copyright 2010-2014 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2010-2014 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 */
021package com.unboundid.ldap.listener;
022
023
024
025import java.io.IOException;
026import java.io.OutputStream;
027import java.net.Socket;
028import java.util.ArrayList;
029import java.util.List;
030import java.util.concurrent.CopyOnWriteArrayList;
031import java.util.concurrent.atomic.AtomicBoolean;
032import javax.net.ssl.SSLSocket;
033import javax.net.ssl.SSLSocketFactory;
034
035import com.unboundid.asn1.ASN1Buffer;
036import com.unboundid.asn1.ASN1StreamReader;
037import com.unboundid.ldap.protocol.AddResponseProtocolOp;
038import com.unboundid.ldap.protocol.BindResponseProtocolOp;
039import com.unboundid.ldap.protocol.CompareResponseProtocolOp;
040import com.unboundid.ldap.protocol.DeleteResponseProtocolOp;
041import com.unboundid.ldap.protocol.ExtendedResponseProtocolOp;
042import com.unboundid.ldap.protocol.IntermediateResponseProtocolOp;
043import com.unboundid.ldap.protocol.LDAPMessage;
044import com.unboundid.ldap.protocol.ModifyResponseProtocolOp;
045import com.unboundid.ldap.protocol.ModifyDNResponseProtocolOp;
046import com.unboundid.ldap.protocol.SearchResultDoneProtocolOp;
047import com.unboundid.ldap.protocol.SearchResultEntryProtocolOp;
048import com.unboundid.ldap.protocol.SearchResultReferenceProtocolOp;
049import com.unboundid.ldap.sdk.Attribute;
050import com.unboundid.ldap.sdk.Control;
051import com.unboundid.ldap.sdk.Entry;
052import com.unboundid.ldap.sdk.ExtendedResult;
053import com.unboundid.ldap.sdk.LDAPException;
054import com.unboundid.ldap.sdk.LDAPRuntimeException;
055import com.unboundid.ldap.sdk.ResultCode;
056import com.unboundid.util.Debug;
057import com.unboundid.util.InternalUseOnly;
058import com.unboundid.util.ObjectPair;
059import com.unboundid.util.StaticUtils;
060import com.unboundid.util.ThreadSafety;
061import com.unboundid.util.ThreadSafetyLevel;
062import com.unboundid.util.Validator;
063import com.unboundid.util.ssl.SSLUtil;
064
065import static com.unboundid.ldap.listener.ListenerMessages.*;
066
067
068
069/**
070 * This class provides an object which will be used to represent a connection to
071 * a client accepted by an {@link LDAPListener}, although connections may also
072 * be created independently if they were accepted in some other way.  Each
073 * connection has its own thread that will be used to read requests from the
074 * client, and connections created outside of an {@code LDAPListener} instance,
075 * then the thread must be explicitly started.
076 */
077@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
078public final class LDAPListenerClientConnection
079       extends Thread
080{
081  /**
082   * A pre-allocated empty array of controls.
083   */
084  private static final Control[] EMPTY_CONTROL_ARRAY = new Control[0];
085
086
087
088  // The buffer used to hold responses to be sent to the client.
089  private final ASN1Buffer asn1Buffer;
090
091  // The ASN.1 stream reader used to read requests from the client.
092  private volatile ASN1StreamReader asn1Reader;
093
094  // Indicates whether to suppress the next call to sendMessage to send a
095  // response to the client.
096  private final AtomicBoolean suppressNextResponse;
097
098  // The set of intermediate response transformers for this connection.
099  private final CopyOnWriteArrayList<IntermediateResponseTransformer>
100       intermediateResponseTransformers;
101
102  // The set of search result entry transformers for this connection.
103  private final CopyOnWriteArrayList<SearchEntryTransformer>
104       searchEntryTransformers;
105
106  // The set of search result reference transformers for this connection.
107  private final CopyOnWriteArrayList<SearchReferenceTransformer>
108       searchReferenceTransformers;
109
110  // The listener that accepted this connection.
111  private final LDAPListener listener;
112
113  // The exception handler to use for this connection, if any.
114  private final LDAPListenerExceptionHandler exceptionHandler;
115
116  // The request handler to use for this connection.
117  private final LDAPListenerRequestHandler requestHandler;
118
119  // The connection ID assigned to this connection.
120  private final long connectionID;
121
122  // The output stream used to write responses to the client.
123  private volatile OutputStream outputStream;
124
125  // The socket used to communicate with the client.
126  private volatile Socket socket;
127
128
129
130  /**
131   * Creates a new LDAP listener client connection that will communicate with
132   * the client using the provided socket.  The {@link #start} method must be
133   * called to start listening for requests from the client.
134   *
135   * @param  listener          The listener that accepted this client
136   *                           connection.  It may be {@code null} if this
137   *                           connection was not accepted by a listener.
138   * @param  socket            The socket that may be used to communicate with
139   *                           the client.  It must not be {@code null}.
140   * @param  requestHandler    The request handler that will be used to process
141   *                           requests read from the client.  The
142   *                           {@link LDAPListenerRequestHandler#newInstance}
143   *                           method will be called on the provided object to
144   *                           obtain a new instance to use for this connection.
145   *                           The provided request handler must not be
146   *                           {@code null}.
147   * @param  exceptionHandler  The disconnect handler to be notified when this
148   *                           connection is closed.  It may be {@code null} if
149   *                           no disconnect handler should be used.
150   *
151   * @throws  LDAPException  If a problem occurs while preparing this client
152   *                         connection. for use.  If this is thrown, then the
153   *                         provided socket will be closed.
154   */
155  public LDAPListenerClientConnection(final LDAPListener listener,
156              final Socket socket,
157              final LDAPListenerRequestHandler requestHandler,
158              final LDAPListenerExceptionHandler exceptionHandler)
159         throws LDAPException
160  {
161    Validator.ensureNotNull(socket, requestHandler);
162
163    setName("LDAPListener client connection reader for connection from " +
164         socket.getInetAddress().getHostAddress() + ':' +
165         socket.getPort() + " to " + socket.getLocalAddress().getHostAddress() +
166         ':' + socket.getLocalPort());
167
168    this.listener         = listener;
169    this.socket           = socket;
170    this.exceptionHandler = exceptionHandler;
171
172    intermediateResponseTransformers =
173         new CopyOnWriteArrayList<IntermediateResponseTransformer>();
174    searchEntryTransformers =
175         new CopyOnWriteArrayList<SearchEntryTransformer>();
176    searchReferenceTransformers =
177         new CopyOnWriteArrayList<SearchReferenceTransformer>();
178
179    if (listener == null)
180    {
181      connectionID = -1L;
182    }
183    else
184    {
185      connectionID = listener.nextConnectionID();
186    }
187
188    try
189    {
190      final LDAPListenerConfig config;
191      if (listener == null)
192      {
193        config = new LDAPListenerConfig(0, requestHandler);
194      }
195      else
196      {
197        config = listener.getConfig();
198      }
199
200      socket.setKeepAlive(config.useKeepAlive());
201      socket.setReuseAddress(config.useReuseAddress());
202      socket.setSoLinger(config.useLinger(), config.getLingerTimeoutSeconds());
203      socket.setTcpNoDelay(config.useTCPNoDelay());
204
205      final int sendBufferSize = config.getSendBufferSize();
206      if (sendBufferSize > 0)
207      {
208        socket.setSendBufferSize(sendBufferSize);
209      }
210
211      asn1Reader = new ASN1StreamReader(socket.getInputStream());
212    }
213    catch (final IOException ioe)
214    {
215      Debug.debugException(ioe);
216
217      try
218      {
219        socket.close();
220      }
221      catch (final Exception e)
222      {
223        Debug.debugException(e);
224      }
225
226      throw new LDAPException(ResultCode.CONNECT_ERROR,
227           ERR_CONN_CREATE_IO_EXCEPTION.get(
228                StaticUtils.getExceptionMessage(ioe)),
229           ioe);
230    }
231
232    try
233    {
234      outputStream = socket.getOutputStream();
235    }
236    catch (final IOException ioe)
237    {
238      Debug.debugException(ioe);
239
240      try
241      {
242        asn1Reader.close();
243      }
244      catch (final Exception e)
245      {
246        Debug.debugException(e);
247      }
248
249      try
250      {
251        socket.close();
252      }
253      catch (final Exception e)
254      {
255        Debug.debugException(e);
256      }
257
258      throw new LDAPException(ResultCode.CONNECT_ERROR,
259           ERR_CONN_CREATE_IO_EXCEPTION.get(
260                StaticUtils.getExceptionMessage(ioe)),
261           ioe);
262    }
263
264    try
265    {
266      this.requestHandler = requestHandler.newInstance(this);
267    }
268    catch (final LDAPException le)
269    {
270      Debug.debugException(le);
271
272      try
273      {
274        asn1Reader.close();
275      }
276      catch (final Exception e)
277      {
278        Debug.debugException(e);
279      }
280
281      try
282      {
283        outputStream.close();
284      }
285      catch (final Exception e)
286      {
287        Debug.debugException(e);
288      }
289
290      try
291      {
292        socket.close();
293      }
294      catch (final Exception e)
295      {
296        Debug.debugException(e);
297      }
298
299      throw le;
300    }
301
302    asn1Buffer           = new ASN1Buffer();
303    suppressNextResponse = new AtomicBoolean(false);
304  }
305
306
307
308  /**
309   * Closes the connection to the client.
310   *
311   * @throws  IOException  If a problem occurs while closing the socket.
312   */
313  public synchronized void close()
314         throws IOException
315  {
316    try
317    {
318      requestHandler.closeInstance();
319    }
320    catch (final Exception e)
321    {
322      Debug.debugException(e);
323    }
324
325    try
326    {
327      asn1Reader.close();
328    }
329    catch (final Exception e)
330    {
331      Debug.debugException(e);
332    }
333
334    try
335    {
336      outputStream.close();
337    }
338    catch (final Exception e)
339    {
340      Debug.debugException(e);
341    }
342
343    socket.close();
344  }
345
346
347
348  /**
349   * Closes the connection to the client as a result of an exception encountered
350   * during processing.  Any associated exception handler will be notified
351   * prior to the connection closure.
352   *
353   * @param  le  The exception providing information about the reason that this
354   *             connection will be terminated.
355   */
356  void close(final LDAPException le)
357  {
358    if (exceptionHandler == null)
359    {
360      Debug.debugException(le);
361    }
362    else
363    {
364      try
365      {
366        exceptionHandler.connectionTerminated(this, le);
367      }
368      catch (final Exception e)
369      {
370        Debug.debugException(e);
371      }
372    }
373
374    try
375    {
376      close();
377    }
378    catch (final Exception e)
379    {
380      Debug.debugException(e);
381    }
382  }
383
384
385
386  /**
387   * Operates in a loop, waiting for a request to arrive from the client and
388   * handing it off to the request handler for processing.  This method is for
389   * internal use only and must not be invoked by external callers.
390   */
391  @InternalUseOnly()
392  @Override()
393  public void run()
394  {
395    try
396    {
397      while (true)
398      {
399        final LDAPMessage requestMessage;
400        try
401        {
402          requestMessage = LDAPMessage.readFrom(asn1Reader, false);
403          if (requestMessage == null)
404          {
405            // This indicates that the client has closed the connection without
406            // an unbind request.  It's not all that nice, but it isn't an error
407            // so we won't notify the exception handler.
408            try
409            {
410              close();
411            }
412            catch (final IOException ioe)
413            {
414              Debug.debugException(ioe);
415            }
416
417            return;
418          }
419        }
420        catch (final LDAPException le)
421        {
422          Debug.debugException(le);
423          close(le);
424          return;
425        }
426
427        try
428        {
429          final int messageID = requestMessage.getMessageID();
430          final List<Control> controls = requestMessage.getControls();
431
432          LDAPMessage responseMessage;
433          switch (requestMessage.getProtocolOpType())
434          {
435            case LDAPMessage.PROTOCOL_OP_TYPE_ABANDON_REQUEST:
436              requestHandler.processAbandonRequest(messageID,
437                   requestMessage.getAbandonRequestProtocolOp(), controls);
438              responseMessage = null;
439              break;
440
441            case LDAPMessage.PROTOCOL_OP_TYPE_ADD_REQUEST:
442              try
443              {
444                responseMessage = requestHandler.processAddRequest(messageID,
445                     requestMessage.getAddRequestProtocolOp(), controls);
446              }
447              catch (final Exception e)
448              {
449                Debug.debugException(e);
450                responseMessage = new LDAPMessage(messageID,
451                     new AddResponseProtocolOp(
452                          ResultCode.OTHER_INT_VALUE, null,
453                          ERR_CONN_REQUEST_HANDLER_FAILURE.get(
454                               StaticUtils.getExceptionMessage(e)),
455                          null));
456              }
457              break;
458
459            case LDAPMessage.PROTOCOL_OP_TYPE_BIND_REQUEST:
460              try
461              {
462                responseMessage = requestHandler.processBindRequest(messageID,
463                     requestMessage.getBindRequestProtocolOp(), controls);
464              }
465              catch (final Exception e)
466              {
467                Debug.debugException(e);
468                responseMessage = new LDAPMessage(messageID,
469                     new BindResponseProtocolOp(
470                          ResultCode.OTHER_INT_VALUE, null,
471                          ERR_CONN_REQUEST_HANDLER_FAILURE.get(
472                               StaticUtils.getExceptionMessage(e)),
473                          null, null));
474              }
475              break;
476
477            case LDAPMessage.PROTOCOL_OP_TYPE_COMPARE_REQUEST:
478              try
479              {
480                responseMessage = requestHandler.processCompareRequest(
481                     messageID, requestMessage.getCompareRequestProtocolOp(),
482                     controls);
483              }
484              catch (final Exception e)
485              {
486                Debug.debugException(e);
487                responseMessage = new LDAPMessage(messageID,
488                     new CompareResponseProtocolOp(
489                          ResultCode.OTHER_INT_VALUE, null,
490                          ERR_CONN_REQUEST_HANDLER_FAILURE.get(
491                               StaticUtils.getExceptionMessage(e)),
492                          null));
493              }
494              break;
495
496            case LDAPMessage.PROTOCOL_OP_TYPE_DELETE_REQUEST:
497              try
498              {
499                responseMessage = requestHandler.processDeleteRequest(messageID,
500                     requestMessage.getDeleteRequestProtocolOp(), controls);
501              }
502              catch (final Exception e)
503              {
504                Debug.debugException(e);
505                responseMessage = new LDAPMessage(messageID,
506                     new DeleteResponseProtocolOp(
507                          ResultCode.OTHER_INT_VALUE, null,
508                          ERR_CONN_REQUEST_HANDLER_FAILURE.get(
509                               StaticUtils.getExceptionMessage(e)),
510                          null));
511              }
512              break;
513
514            case LDAPMessage.PROTOCOL_OP_TYPE_EXTENDED_REQUEST:
515              try
516              {
517                responseMessage = requestHandler.processExtendedRequest(
518                     messageID, requestMessage.getExtendedRequestProtocolOp(),
519                     controls);
520              }
521              catch (final Exception e)
522              {
523                Debug.debugException(e);
524                responseMessage = new LDAPMessage(messageID,
525                     new ExtendedResponseProtocolOp(
526                          ResultCode.OTHER_INT_VALUE, null,
527                          ERR_CONN_REQUEST_HANDLER_FAILURE.get(
528                               StaticUtils.getExceptionMessage(e)),
529                          null, null, null));
530              }
531              break;
532
533            case LDAPMessage.PROTOCOL_OP_TYPE_MODIFY_REQUEST:
534              try
535              {
536                responseMessage = requestHandler.processModifyRequest(messageID,
537                     requestMessage.getModifyRequestProtocolOp(), controls);
538              }
539              catch (final Exception e)
540              {
541                Debug.debugException(e);
542                responseMessage = new LDAPMessage(messageID,
543                     new ModifyResponseProtocolOp(
544                          ResultCode.OTHER_INT_VALUE, null,
545                          ERR_CONN_REQUEST_HANDLER_FAILURE.get(
546                               StaticUtils.getExceptionMessage(e)),
547                          null));
548              }
549              break;
550
551            case LDAPMessage.PROTOCOL_OP_TYPE_MODIFY_DN_REQUEST:
552              try
553              {
554                responseMessage = requestHandler.processModifyDNRequest(
555                     messageID, requestMessage.getModifyDNRequestProtocolOp(),
556                     controls);
557              }
558              catch (final Exception e)
559              {
560                Debug.debugException(e);
561                responseMessage = new LDAPMessage(messageID,
562                     new ModifyDNResponseProtocolOp(
563                          ResultCode.OTHER_INT_VALUE, null,
564                          ERR_CONN_REQUEST_HANDLER_FAILURE.get(
565                               StaticUtils.getExceptionMessage(e)),
566                          null));
567              }
568              break;
569
570            case LDAPMessage.PROTOCOL_OP_TYPE_SEARCH_REQUEST:
571              try
572              {
573                responseMessage = requestHandler.processSearchRequest(messageID,
574                     requestMessage.getSearchRequestProtocolOp(), controls);
575              }
576              catch (final Exception e)
577              {
578                Debug.debugException(e);
579                responseMessage = new LDAPMessage(messageID,
580                     new SearchResultDoneProtocolOp(
581                          ResultCode.OTHER_INT_VALUE, null,
582                          ERR_CONN_REQUEST_HANDLER_FAILURE.get(
583                               StaticUtils.getExceptionMessage(e)),
584                          null));
585              }
586              break;
587
588            case LDAPMessage.PROTOCOL_OP_TYPE_UNBIND_REQUEST:
589              requestHandler.processUnbindRequest(messageID,
590                   requestMessage.getUnbindRequestProtocolOp(), controls);
591              close();
592              return;
593
594            default:
595              close(new LDAPException(ResultCode.PROTOCOL_ERROR,
596                   ERR_CONN_INVALID_PROTOCOL_OP_TYPE.get(StaticUtils.toHex(
597                        requestMessage.getProtocolOpType()))));
598              return;
599          }
600
601          if (responseMessage != null)
602          {
603            try
604            {
605              sendMessage(responseMessage);
606            }
607            catch (final LDAPException le)
608            {
609              Debug.debugException(le);
610              close(le);
611              return;
612            }
613          }
614        }
615        catch (final Exception e)
616        {
617          close(new LDAPException(ResultCode.LOCAL_ERROR,
618               ERR_CONN_EXCEPTION_IN_REQUEST_HANDLER.get(
619                    String.valueOf(requestMessage),
620                    StaticUtils.getExceptionMessage(e))));
621          return;
622        }
623      }
624    }
625    finally
626    {
627      if (listener != null)
628      {
629        listener.connectionClosed(this);
630      }
631    }
632  }
633
634
635
636  /**
637   * Sends the provided message to the client.
638   *
639   * @param  message  The message to be written to the client.
640   *
641   * @throws  LDAPException  If a problem occurs while attempting to send the
642   *                         response to the client.
643   */
644  private synchronized void sendMessage(final LDAPMessage message)
645          throws LDAPException
646  {
647    // If we should suppress this response (which will only be because the
648    // response has already been sent through some other means, for example as
649    // part of StartTLS processing), then do so.
650    if (suppressNextResponse.compareAndSet(true, false))
651    {
652      return;
653    }
654
655    asn1Buffer.clear();
656
657    try
658    {
659      message.writeTo(asn1Buffer);
660    }
661    catch (final LDAPRuntimeException lre)
662    {
663      Debug.debugException(lre);
664      lre.throwLDAPException();
665    }
666
667    try
668    {
669      asn1Buffer.writeTo(outputStream);
670    }
671    catch (final IOException ioe)
672    {
673      Debug.debugException(ioe);
674
675      throw new LDAPException(ResultCode.LOCAL_ERROR,
676           ERR_CONN_SEND_MESSAGE_EXCEPTION.get(
677                StaticUtils.getExceptionMessage(ioe)),
678           ioe);
679    }
680    finally
681    {
682      if (asn1Buffer.zeroBufferOnClear())
683      {
684        asn1Buffer.clear();
685      }
686    }
687  }
688
689
690
691  /**
692   * Sends a search result entry message to the client with the provided
693   * information.
694   *
695   * @param  messageID   The message ID for the LDAP message to send to the
696   *                     client.  It must match the message ID of the associated
697   *                     search request.
698   * @param  protocolOp  The search result entry protocol op to include in the
699   *                     LDAP message to send to the client.  It must not be
700   *                     {@code null}.
701   * @param  controls    The set of controls to include in the response message.
702   *                     It may be empty or {@code null} if no controls should
703   *                     be included.
704   *
705   * @throws  LDAPException  If a problem occurs while attempting to send the
706   *                         provided response message.  If an exception is
707   *                         thrown, then the client connection will have been
708   *                         terminated.
709   */
710  public void sendSearchResultEntry(final int messageID,
711                   final SearchResultEntryProtocolOp protocolOp,
712                   final Control... controls)
713         throws LDAPException
714  {
715    if (searchEntryTransformers.isEmpty())
716    {
717      sendMessage(new LDAPMessage(messageID, protocolOp, controls));
718    }
719    else
720    {
721      Control[] c;
722      SearchResultEntryProtocolOp op = protocolOp;
723      if (controls == null)
724      {
725        c = EMPTY_CONTROL_ARRAY;
726      }
727      else
728      {
729        c = controls;
730      }
731
732      for (final SearchEntryTransformer t : searchEntryTransformers)
733      {
734        try
735        {
736          final ObjectPair<SearchResultEntryProtocolOp,Control[]> p =
737               t.transformEntry(messageID, op, c);
738          if (p == null)
739          {
740            return;
741          }
742
743          op = p.getFirst();
744          c  = p.getSecond();
745        }
746        catch (final Exception e)
747        {
748          Debug.debugException(e);
749          sendMessage(new LDAPMessage(messageID, protocolOp, c));
750          throw new LDAPException(ResultCode.LOCAL_ERROR,
751               ERR_CONN_SEARCH_ENTRY_TRANSFORMER_EXCEPTION.get(
752                    t.getClass().getName(), String.valueOf(op),
753                    StaticUtils.getExceptionMessage(e)),
754               e);
755        }
756      }
757
758      sendMessage(new LDAPMessage(messageID, op, c));
759    }
760  }
761
762
763
764  /**
765   * Sends a search result entry message to the client with the provided
766   * information.
767   *
768   * @param  messageID  The message ID for the LDAP message to send to the
769   *                    client.  It must match the message ID of the associated
770   *                    search request.
771   * @param  entry      The entry to return to the client.  It must not be
772   *                    {@code null}.
773   * @param  controls   The set of controls to include in the response message.
774   *                    It may be empty or {@code null} if no controls should be
775   *                    included.
776   *
777   * @throws  LDAPException  If a problem occurs while attempting to send the
778   *                         provided response message.  If an exception is
779   *                         thrown, then the client connection will have been
780   *                         terminated.
781   */
782  public void sendSearchResultEntry(final int messageID, final Entry entry,
783                                    final Control... controls)
784         throws LDAPException
785  {
786    sendSearchResultEntry(messageID,
787         new SearchResultEntryProtocolOp(entry.getDN(),
788              new ArrayList<Attribute>(entry.getAttributes())),
789         controls);
790  }
791
792
793
794  /**
795   * Sends a search result reference message to the client with the provided
796   * information.
797   *
798   * @param  messageID   The message ID for the LDAP message to send to the
799   *                     client.  It must match the message ID of the associated
800   *                     search request.
801   * @param  protocolOp  The search result reference protocol op to include in
802   *                     the LDAP message to send to the client.
803   * @param  controls    The set of controls to include in the response message.
804   *                     It may be empty or {@code null} if no controls should
805   *                     be included.
806   *
807   * @throws  LDAPException  If a problem occurs while attempting to send the
808   *                         provided response message.  If an exception is
809   *                         thrown, then the client connection will have been
810   *                         terminated.
811   */
812  public void sendSearchResultReference(final int messageID,
813                   final SearchResultReferenceProtocolOp protocolOp,
814                   final Control... controls)
815         throws LDAPException
816  {
817    if (searchReferenceTransformers.isEmpty())
818    {
819      sendMessage(new LDAPMessage(messageID, protocolOp, controls));
820    }
821    else
822    {
823      Control[] c;
824      SearchResultReferenceProtocolOp op = protocolOp;
825      if (controls == null)
826      {
827        c = EMPTY_CONTROL_ARRAY;
828      }
829      else
830      {
831        c = controls;
832      }
833
834      for (final SearchReferenceTransformer t : searchReferenceTransformers)
835      {
836        try
837        {
838          final ObjectPair<SearchResultReferenceProtocolOp,Control[]> p =
839               t.transformReference(messageID, op, c);
840          if (p == null)
841          {
842            return;
843          }
844
845          op = p.getFirst();
846          c  = p.getSecond();
847        }
848        catch (final Exception e)
849        {
850          Debug.debugException(e);
851          sendMessage(new LDAPMessage(messageID, protocolOp, c));
852          throw new LDAPException(ResultCode.LOCAL_ERROR,
853               ERR_CONN_SEARCH_REFERENCE_TRANSFORMER_EXCEPTION.get(
854                    t.getClass().getName(), String.valueOf(op),
855                    StaticUtils.getExceptionMessage(e)),
856               e);
857        }
858      }
859
860      sendMessage(new LDAPMessage(messageID, op, c));
861    }
862  }
863
864
865
866  /**
867   * Sends an intermediate response message to the client with the provided
868   * information.
869   *
870   * @param  messageID   The message ID for the LDAP message to send to the
871   *                     client.  It must match the message ID of the associated
872   *                     search request.
873   * @param  protocolOp  The intermediate response protocol op to include in the
874   *                     LDAP message to send to the client.
875   * @param  controls    The set of controls to include in the response message.
876   *                     It may be empty or {@code null} if no controls should
877   *                     be included.
878   *
879   * @throws  LDAPException  If a problem occurs while attempting to send the
880   *                         provided response message.  If an exception is
881   *                         thrown, then the client connection will have been
882   *                         terminated.
883   */
884  public void sendIntermediateResponse(final int messageID,
885                   final IntermediateResponseProtocolOp protocolOp,
886                   final Control... controls)
887         throws LDAPException
888  {
889    if (intermediateResponseTransformers.isEmpty())
890    {
891      sendMessage(new LDAPMessage(messageID, protocolOp, controls));
892    }
893    else
894    {
895      Control[] c;
896      IntermediateResponseProtocolOp op = protocolOp;
897      if (controls == null)
898      {
899        c = EMPTY_CONTROL_ARRAY;
900      }
901      else
902      {
903        c = controls;
904      }
905
906      for (final IntermediateResponseTransformer t :
907           intermediateResponseTransformers)
908      {
909        try
910        {
911          final ObjectPair<IntermediateResponseProtocolOp,Control[]> p =
912               t.transformIntermediateResponse(messageID, op, c);
913          if (p == null)
914          {
915            return;
916          }
917
918          op = p.getFirst();
919          c  = p.getSecond();
920        }
921        catch (final Exception e)
922        {
923          Debug.debugException(e);
924          sendMessage(new LDAPMessage(messageID, protocolOp, c));
925          throw new LDAPException(ResultCode.LOCAL_ERROR,
926               ERR_CONN_INTERMEDIATE_RESPONSE_TRANSFORMER_EXCEPTION.get(
927                    t.getClass().getName(), String.valueOf(op),
928                    StaticUtils.getExceptionMessage(e)),
929               e);
930        }
931      }
932
933      sendMessage(new LDAPMessage(messageID, op, c));
934    }
935  }
936
937
938
939  /**
940   * Sends an unsolicited notification message to the client with the provided
941   * extended result.
942   *
943   * @param  result  The extended result to use for the unsolicited
944   *                 notification.
945   *
946   * @throws  LDAPException  If a problem occurs while attempting to send the
947   *                         unsolicited notification.  If an exception is
948   *                         thrown, then the client connection will have been
949   *                         terminated.
950   */
951  public void sendUnsolicitedNotification(final ExtendedResult result)
952         throws LDAPException
953  {
954    sendUnsolicitedNotification(
955         new ExtendedResponseProtocolOp(result.getResultCode().intValue(),
956              result.getMatchedDN(), result.getDiagnosticMessage(),
957              StaticUtils.toList(result.getReferralURLs()), result.getOID(),
958              result.getValue()),
959         result.getResponseControls()
960    );
961  }
962
963
964
965  /**
966   * Sends an unsolicited notification message to the client with the provided
967   * information.
968   *
969   * @param  extendedResponse  The extended response to use for the unsolicited
970   *                           notification.
971   * @param  controls          The set of controls to include with the
972   *                           unsolicited notification.  It may be empty or
973   *                           {@code null} if no controls should be included.
974   *
975   * @throws  LDAPException  If a problem occurs while attempting to send the
976   *                         unsolicited notification.  If an exception is
977   *                         thrown, then the client connection will have been
978   *                         terminated.
979   */
980  public void sendUnsolicitedNotification(
981                   final ExtendedResponseProtocolOp extendedResponse,
982                   final Control... controls)
983         throws LDAPException
984  {
985    sendMessage(new LDAPMessage(0, extendedResponse, controls));
986  }
987
988
989
990  /**
991   * Retrieves the socket used to communicate with the client.
992   *
993   * @return  The socket used to communicate with the client.
994   */
995  public synchronized Socket getSocket()
996  {
997    return socket;
998  }
999
1000
1001
1002  /**
1003   * Attempts to convert this unencrypted connection to one that uses TLS
1004   * encryption, as would be used during the course of invoking the StartTLS
1005   * extended operation.  If this is called, then the response that would have
1006   * been returned from the associated request will be suppressed, so the
1007   * returned output stream must be used to send the appropriate response to
1008   * the client.
1009   *
1010   * @param  f  The SSL socket factory that will be used to convert the existing
1011   *            {@code Socket} to an {@code SSLSocket}.
1012   *
1013   * @return  An output stream that can be used to send a clear-text message to
1014   *          the client (e.g., the StartTLS response message).
1015   *
1016   * @throws  LDAPException  If a problem is encountered while trying to convert
1017   *                         the existing socket to an SSL socket.  If this is
1018   *                         thrown, then the connection will have been closed.
1019   */
1020  public synchronized OutputStream convertToTLS(final SSLSocketFactory f)
1021         throws LDAPException
1022  {
1023    final OutputStream clearOutputStream = outputStream;
1024
1025    final Socket origSocket = socket;
1026    final String hostname   = origSocket.getInetAddress().getHostName();
1027    final int port          = origSocket.getPort();
1028
1029    try
1030    {
1031      synchronized (f)
1032      {
1033        socket = f.createSocket(socket, hostname, port, true);
1034        SSLUtil.applyEnabledSSLProtocols(socket);
1035      }
1036      ((SSLSocket) socket).setUseClientMode(false);
1037      outputStream = socket.getOutputStream();
1038      asn1Reader = new ASN1StreamReader(socket.getInputStream());
1039      suppressNextResponse.set(true);
1040      return clearOutputStream;
1041    }
1042    catch (final Exception e)
1043    {
1044      Debug.debugException(e);
1045
1046      final LDAPException le = new LDAPException(ResultCode.LOCAL_ERROR,
1047           ERR_CONN_CONVERT_TO_TLS_FAILURE.get(
1048                StaticUtils.getExceptionMessage(e)),
1049           e);
1050
1051      close(le);
1052
1053      throw le;
1054    }
1055  }
1056
1057
1058
1059  /**
1060   * Retrieves the connection ID that has been assigned to this connection by
1061   * the associated listener.
1062   *
1063   * @return  The connection ID that has been assigned to this connection by
1064   *          the associated listener, or -1 if it is not associated with a
1065   *          listener.
1066   */
1067  public long getConnectionID()
1068  {
1069    return connectionID;
1070  }
1071
1072
1073
1074  /**
1075   * Adds the provided search entry transformer to this client connection.
1076   *
1077   * @param  t  A search entry transformer to be used to intercept and/or alter
1078   *            search result entries before they are returned to the client.
1079   */
1080  public void addSearchEntryTransformer(final SearchEntryTransformer t)
1081  {
1082    searchEntryTransformers.add(t);
1083  }
1084
1085
1086
1087  /**
1088   * Removes the provided search entry transformer from this client connection.
1089   *
1090   * @param  t  The search entry transformer to be removed.
1091   */
1092  public void removeSearchEntryTransformer(final SearchEntryTransformer t)
1093  {
1094    searchEntryTransformers.remove(t);
1095  }
1096
1097
1098
1099  /**
1100   * Adds the provided search reference transformer to this client connection.
1101   *
1102   * @param  t  A search reference transformer to be used to intercept and/or
1103   *            alter search result references before they are returned to the
1104   *            client.
1105   */
1106  public void addSearchReferenceTransformer(final SearchReferenceTransformer t)
1107  {
1108    searchReferenceTransformers.add(t);
1109  }
1110
1111
1112
1113  /**
1114   * Removes the provided search reference transformer from this client
1115   * connection.
1116   *
1117   * @param  t  The search reference transformer to be removed.
1118   */
1119  public void removeSearchReferenceTransformer(
1120                   final SearchReferenceTransformer t)
1121  {
1122    searchReferenceTransformers.remove(t);
1123  }
1124
1125
1126
1127  /**
1128   * Adds the provided intermediate response transformer to this client
1129   * connection.
1130   *
1131   * @param  t  An intermediate response transformer to be used to intercept
1132   *            and/or alter intermediate responses before they are returned to
1133   *            the client.
1134   */
1135  public void addIntermediateResponseTransformer(
1136                   final IntermediateResponseTransformer t)
1137  {
1138    intermediateResponseTransformers.add(t);
1139  }
1140
1141
1142
1143  /**
1144   * Removes the provided intermediate response transformer from this client
1145   * connection.
1146   *
1147   * @param  t  The intermediate response transformer to be removed.
1148   */
1149  public void removeIntermediateResponseTransformer(
1150                   final IntermediateResponseTransformer t)
1151  {
1152    intermediateResponseTransformers.remove(t);
1153  }
1154}