001/*
002 * Copyright 2007-2018 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2018 Ping Identity Corporation
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.sdk;
022
023
024
025import java.util.ArrayList;
026import java.util.List;
027import java.util.concurrent.LinkedBlockingQueue;
028import java.util.concurrent.TimeUnit;
029import java.util.logging.Level;
030
031import com.unboundid.asn1.ASN1Buffer;
032import com.unboundid.asn1.ASN1BufferSequence;
033import com.unboundid.asn1.ASN1Element;
034import com.unboundid.asn1.ASN1OctetString;
035import com.unboundid.asn1.ASN1Sequence;
036import com.unboundid.ldap.protocol.LDAPMessage;
037import com.unboundid.ldap.protocol.LDAPResponse;
038import com.unboundid.ldap.protocol.ProtocolOp;
039import com.unboundid.ldap.sdk.extensions.StartTLSExtendedRequest;
040import com.unboundid.util.Extensible;
041import com.unboundid.util.InternalUseOnly;
042import com.unboundid.util.NotMutable;
043import com.unboundid.util.ThreadSafety;
044import com.unboundid.util.ThreadSafetyLevel;
045
046import static com.unboundid.ldap.sdk.LDAPMessages.*;
047import static com.unboundid.util.Debug.*;
048import static com.unboundid.util.StaticUtils.*;
049import static com.unboundid.util.Validator.*;
050
051
052
053/**
054 * This class implements the processing necessary to perform an LDAPv3 extended
055 * operation, which provides a way to request actions not included in the core
056 * LDAP protocol.  Subclasses can provide logic to help implement more specific
057 * types of extended operations, but it is important to note that if such
058 * subclasses include an extended request value, then the request value must be
059 * kept up-to-date if any changes are made to custom elements in that class that
060 * would impact the request value encoding.
061 */
062@Extensible()
063@NotMutable()
064@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
065public class ExtendedRequest
066       extends LDAPRequest
067       implements ResponseAcceptor, ProtocolOp
068{
069  /**
070   * The BER type for the extended request OID element.
071   */
072  protected static final byte TYPE_EXTENDED_REQUEST_OID = (byte) 0x80;
073
074
075
076  /**
077   * The BER type for the extended request value element.
078   */
079  protected static final byte TYPE_EXTENDED_REQUEST_VALUE = (byte) 0x81;
080
081
082
083  /**
084   * The serial version UID for this serializable class.
085   */
086  private static final long serialVersionUID = 5572410770060685796L;
087
088
089
090  // The encoded value for this extended request, if available.
091  private final ASN1OctetString value;
092
093  // The message ID from the last LDAP message sent from this request.
094  private int messageID = -1;
095
096  // The queue that will be used to receive response messages from the server.
097  private final LinkedBlockingQueue<LDAPResponse> responseQueue =
098       new LinkedBlockingQueue<LDAPResponse>();
099
100  // The OID for this extended request.
101  private final String oid;
102
103
104
105  /**
106   * Creates a new extended request with the provided OID and no value.
107   *
108   * @param  oid  The OID for this extended request.  It must not be
109   *              {@code null}.
110   */
111  public ExtendedRequest(final String oid)
112  {
113    super(null);
114
115    ensureNotNull(oid);
116
117    this.oid = oid;
118
119    value = null;
120  }
121
122
123
124  /**
125   * Creates a new extended request with the provided OID and no value.
126   *
127   * @param  oid       The OID for this extended request.  It must not be
128   *                   {@code null}.
129   * @param  controls  The set of controls for this extended request.
130   */
131  public ExtendedRequest(final String oid, final Control[] controls)
132  {
133    super(controls);
134
135    ensureNotNull(oid);
136
137    this.oid = oid;
138
139    value = null;
140  }
141
142
143
144  /**
145   * Creates a new extended request with the provided OID and value.
146   *
147   * @param  oid    The OID for this extended request.  It must not be
148   *                {@code null}.
149   * @param  value  The encoded value for this extended request.  It may be
150   *                {@code null} if this request should not have a value.
151   */
152  public ExtendedRequest(final String oid, final ASN1OctetString value)
153  {
154    super(null);
155
156    ensureNotNull(oid);
157
158    this.oid   = oid;
159    this.value = value;
160  }
161
162
163
164  /**
165   * Creates a new extended request with the provided OID and value.
166   *
167   * @param  oid       The OID for this extended request.  It must not be
168   *                   {@code null}.
169   * @param  value     The encoded value for this extended request.  It may be
170   *                   {@code null} if this request should not have a value.
171   * @param  controls  The set of controls for this extended request.
172   */
173  public ExtendedRequest(final String oid, final ASN1OctetString value,
174                         final Control[] controls)
175  {
176    super(controls);
177
178    ensureNotNull(oid);
179
180    this.oid   = oid;
181    this.value = value;
182  }
183
184
185
186  /**
187   * Creates a new extended request with the information from the provided
188   * extended request.
189   *
190   * @param  extendedRequest  The extended request that should be used to create
191   *                          this new extended request.
192   */
193  protected ExtendedRequest(final ExtendedRequest extendedRequest)
194  {
195    super(extendedRequest.getControls());
196
197    oid   = extendedRequest.oid;
198    value = extendedRequest.value;
199  }
200
201
202
203  /**
204   * Retrieves the OID for this extended request.
205   *
206   * @return  The OID for this extended request.
207   */
208  public final String getOID()
209  {
210    return oid;
211  }
212
213
214
215  /**
216   * Indicates whether this extended request has a value.
217   *
218   * @return  {@code true} if this extended request has a value, or
219   *          {@code false} if not.
220   */
221  public final boolean hasValue()
222  {
223    return (value != null);
224  }
225
226
227
228  /**
229   * Retrieves the encoded value for this extended request, if available.
230   *
231   * @return  The encoded value for this extended request, or {@code null} if
232   *          this request does not have a value.
233   */
234  public final ASN1OctetString getValue()
235  {
236    return value;
237  }
238
239
240
241  /**
242   * {@inheritDoc}
243   */
244  @Override()
245  public final byte getProtocolOpType()
246  {
247    return LDAPMessage.PROTOCOL_OP_TYPE_EXTENDED_REQUEST;
248  }
249
250
251
252  /**
253   * {@inheritDoc}
254   */
255  @Override()
256  public final void writeTo(final ASN1Buffer writer)
257  {
258    final ASN1BufferSequence requestSequence =
259         writer.beginSequence(LDAPMessage.PROTOCOL_OP_TYPE_EXTENDED_REQUEST);
260    writer.addOctetString(TYPE_EXTENDED_REQUEST_OID, oid);
261
262    if (value != null)
263    {
264      writer.addOctetString(TYPE_EXTENDED_REQUEST_VALUE, value.getValue());
265    }
266    requestSequence.end();
267  }
268
269
270
271  /**
272   * Encodes the extended request protocol op to an ASN.1 element.
273   *
274   * @return  The ASN.1 element with the encoded extended request protocol op.
275   */
276  @Override()
277  public ASN1Element encodeProtocolOp()
278  {
279    // Create the extended request protocol op.
280    final ASN1Element[] protocolOpElements;
281    if (value == null)
282    {
283      protocolOpElements = new ASN1Element[]
284      {
285        new ASN1OctetString(TYPE_EXTENDED_REQUEST_OID, oid)
286      };
287    }
288    else
289    {
290      protocolOpElements = new ASN1Element[]
291      {
292        new ASN1OctetString(TYPE_EXTENDED_REQUEST_OID, oid),
293        new ASN1OctetString(TYPE_EXTENDED_REQUEST_VALUE, value.getValue())
294      };
295    }
296
297    return new ASN1Sequence(LDAPMessage.PROTOCOL_OP_TYPE_EXTENDED_REQUEST,
298                            protocolOpElements);
299  }
300
301
302
303  /**
304   * Sends this extended request to the directory server over the provided
305   * connection and returns the associated response.
306   *
307   * @param  connection  The connection to use to communicate with the directory
308   *                     server.
309   * @param  depth       The current referral depth for this request.  It should
310   *                     always be one for the initial request, and should only
311   *                     be incremented when following referrals.
312   *
313   * @return  An LDAP result object that provides information about the result
314   *          of the extended operation processing.
315   *
316   * @throws  LDAPException  If a problem occurs while sending the request or
317   *                         reading the response.
318   */
319  @Override()
320  protected ExtendedResult process(final LDAPConnection connection,
321                                   final int depth)
322            throws LDAPException
323  {
324    if (connection.synchronousMode())
325    {
326      return processSync(connection);
327    }
328
329    // Create the LDAP message.
330    messageID = connection.nextMessageID();
331    final LDAPMessage message = new LDAPMessage(messageID, this, getControls());
332
333
334    // Register with the connection reader to be notified of responses for the
335    // request that we've created.
336    connection.registerResponseAcceptor(messageID, this);
337
338
339    try
340    {
341      // Send the request to the server.
342      final long responseTimeout = getResponseTimeoutMillis(connection);
343      debugLDAPRequest(Level.INFO, this, messageID, connection);
344      final long requestTime = System.nanoTime();
345      connection.getConnectionStatistics().incrementNumExtendedRequests();
346      if (this instanceof StartTLSExtendedRequest)
347      {
348        connection.sendMessage(message, 50L);
349      }
350      else
351      {
352        connection.sendMessage(message, responseTimeout);
353      }
354
355      // Wait for and process the response.
356      final LDAPResponse response;
357      try
358      {
359        if (responseTimeout > 0)
360        {
361          response = responseQueue.poll(responseTimeout, TimeUnit.MILLISECONDS);
362        }
363        else
364        {
365          response = responseQueue.take();
366        }
367      }
368      catch (final InterruptedException ie)
369      {
370        debugException(ie);
371        Thread.currentThread().interrupt();
372        throw new LDAPException(ResultCode.LOCAL_ERROR,
373             ERR_EXTOP_INTERRUPTED.get(connection.getHostPort()), ie);
374      }
375
376      return handleResponse(connection, response, requestTime);
377    }
378    finally
379    {
380      connection.deregisterResponseAcceptor(messageID);
381    }
382  }
383
384
385
386  /**
387   * Processes this extended operation in synchronous mode, in which the same
388   * thread will send the request and read the response.
389   *
390   * @param  connection  The connection to use to communicate with the directory
391   *                     server.
392   *
393   * @return  An LDAP result object that provides information about the result
394   *          of the extended processing.
395   *
396   * @throws  LDAPException  If a problem occurs while sending the request or
397   *                         reading the response.
398   */
399  private ExtendedResult processSync(final LDAPConnection connection)
400          throws LDAPException
401  {
402    // Create the LDAP message.
403    messageID = connection.nextMessageID();
404    final LDAPMessage message =
405         new LDAPMessage(messageID,  this, getControls());
406
407
408    // Send the request to the server.
409    final long requestTime = System.nanoTime();
410    debugLDAPRequest(Level.INFO, this, messageID, connection);
411    connection.getConnectionStatistics().incrementNumExtendedRequests();
412    connection.sendMessage(message, getResponseTimeoutMillis(connection));
413
414    while (true)
415    {
416      final LDAPResponse response;
417      try
418      {
419        response = connection.readResponse(messageID);
420      }
421      catch (final LDAPException le)
422      {
423        debugException(le);
424
425        if ((le.getResultCode() == ResultCode.TIMEOUT) &&
426            connection.getConnectionOptions().abandonOnTimeout())
427        {
428          connection.abandon(messageID);
429        }
430
431        throw le;
432      }
433
434      if (response instanceof IntermediateResponse)
435      {
436        final IntermediateResponseListener listener =
437             getIntermediateResponseListener();
438        if (listener != null)
439        {
440          listener.intermediateResponseReturned(
441               (IntermediateResponse) response);
442        }
443      }
444      else
445      {
446        return handleResponse(connection, response, requestTime);
447      }
448    }
449  }
450
451
452
453  /**
454   * Performs the necessary processing for handling a response.
455   *
456   * @param  connection   The connection used to read the response.
457   * @param  response     The response to be processed.
458   * @param  requestTime  The time the request was sent to the server.
459   *
460   * @return  The extended result.
461   *
462   * @throws  LDAPException  If a problem occurs.
463   */
464  private ExtendedResult handleResponse(final LDAPConnection connection,
465                                        final LDAPResponse response,
466                                        final long requestTime)
467          throws LDAPException
468  {
469    if (response == null)
470    {
471      final long waitTime = nanosToMillis(System.nanoTime() - requestTime);
472      if (connection.getConnectionOptions().abandonOnTimeout())
473      {
474        connection.abandon(messageID);
475      }
476
477      throw new LDAPException(ResultCode.TIMEOUT,
478           ERR_EXTENDED_CLIENT_TIMEOUT.get(waitTime, messageID, oid,
479                connection.getHostPort()));
480    }
481
482    if (response instanceof ConnectionClosedResponse)
483    {
484      final ConnectionClosedResponse ccr = (ConnectionClosedResponse) response;
485      final String msg = ccr.getMessage();
486      if (msg == null)
487      {
488        // The connection was closed while waiting for the response.
489        throw new LDAPException(ccr.getResultCode(),
490             ERR_CONN_CLOSED_WAITING_FOR_EXTENDED_RESPONSE.get(
491                  connection.getHostPort(), toString()));
492      }
493      else
494      {
495        // The connection was closed while waiting for the response.
496        throw new LDAPException(ccr.getResultCode(),
497             ERR_CONN_CLOSED_WAITING_FOR_EXTENDED_RESPONSE_WITH_MESSAGE.get(
498                  connection.getHostPort(), toString(), msg));
499      }
500    }
501
502    connection.getConnectionStatistics().incrementNumExtendedResponses(
503         System.nanoTime() - requestTime);
504    return (ExtendedResult) response;
505  }
506
507
508
509  /**
510   * {@inheritDoc}
511   */
512  @InternalUseOnly()
513  @Override()
514  public final void responseReceived(final LDAPResponse response)
515         throws LDAPException
516  {
517    try
518    {
519      responseQueue.put(response);
520    }
521    catch (final Exception e)
522    {
523      debugException(e);
524
525      if (e instanceof InterruptedException)
526      {
527        Thread.currentThread().interrupt();
528      }
529
530      throw new LDAPException(ResultCode.LOCAL_ERROR,
531           ERR_EXCEPTION_HANDLING_RESPONSE.get(getExceptionMessage(e)), e);
532    }
533  }
534
535
536
537  /**
538   * {@inheritDoc}
539   */
540  @Override()
541  public final int getLastMessageID()
542  {
543    return messageID;
544  }
545
546
547
548  /**
549   * {@inheritDoc}
550   */
551  @Override()
552  public final OperationType getOperationType()
553  {
554    return OperationType.EXTENDED;
555  }
556
557
558
559  /**
560   * {@inheritDoc}.  Subclasses should override this method to return a
561   * duplicate of the appropriate type.
562   */
563  @Override()
564  public ExtendedRequest duplicate()
565  {
566    return duplicate(getControls());
567  }
568
569
570
571  /**
572   * {@inheritDoc}.  Subclasses should override this method to return a
573   * duplicate of the appropriate type.
574   */
575  @Override()
576  public ExtendedRequest duplicate(final Control[] controls)
577  {
578    final ExtendedRequest r = new ExtendedRequest(oid, value, controls);
579    r.setResponseTimeoutMillis(getResponseTimeoutMillis(null));
580    return r;
581  }
582
583
584
585  /**
586   * Retrieves the user-friendly name for the extended request, if available.
587   * If no user-friendly name has been defined, then the OID will be returned.
588   *
589   * @return  The user-friendly name for this extended request, or the OID if no
590   *          user-friendly name is available.
591   */
592  public String getExtendedRequestName()
593  {
594    // By default, we will return the OID.  Subclasses should override this to
595    // provide the user-friendly name.
596    return oid;
597  }
598
599
600
601  /**
602   * {@inheritDoc}
603   */
604  @Override()
605  public void toString(final StringBuilder buffer)
606  {
607    buffer.append("ExtendedRequest(oid='");
608    buffer.append(oid);
609    buffer.append('\'');
610
611    final Control[] controls = getControls();
612    if (controls.length > 0)
613    {
614      buffer.append(", controls={");
615      for (int i=0; i < controls.length; i++)
616      {
617        if (i > 0)
618        {
619          buffer.append(", ");
620        }
621
622        buffer.append(controls[i]);
623      }
624      buffer.append('}');
625    }
626
627    buffer.append(')');
628  }
629
630
631
632  /**
633   * {@inheritDoc}
634   */
635  @Override()
636  public void toCode(final List<String> lineList, final String requestID,
637                     final int indentSpaces, final boolean includeProcessing)
638  {
639    // Create the request variable.
640    final ArrayList<ToCodeArgHelper> constructorArgs =
641         new ArrayList<ToCodeArgHelper>(3);
642    constructorArgs.add(ToCodeArgHelper.createString(oid, "Request OID"));
643    constructorArgs.add(ToCodeArgHelper.createASN1OctetString(value,
644         "Request Value"));
645
646    final Control[] controls = getControls();
647    if (controls.length > 0)
648    {
649      constructorArgs.add(ToCodeArgHelper.createControlArray(controls,
650           "Request Controls"));
651    }
652
653    ToCodeHelper.generateMethodCall(lineList, indentSpaces, "ExtendedRequest",
654         requestID + "Request", "new ExtendedRequest", constructorArgs);
655
656
657    // Add lines for processing the request and obtaining the result.
658    if (includeProcessing)
659    {
660      // Generate a string with the appropriate indent.
661      final StringBuilder buffer = new StringBuilder();
662      for (int i=0; i < indentSpaces; i++)
663      {
664        buffer.append(' ');
665      }
666      final String indent = buffer.toString();
667
668      lineList.add("");
669      lineList.add(indent + "try");
670      lineList.add(indent + '{');
671      lineList.add(indent + "  ExtendedResult " + requestID +
672           "Result = connection.processExtendedOperation(" + requestID +
673           "Request);");
674      lineList.add(indent + "  // The extended operation was processed and " +
675           "we have a result.");
676      lineList.add(indent + "  // This does not necessarily mean that the " +
677           "operation was successful.");
678      lineList.add(indent + "  // Examine the result details for more " +
679           "information.");
680      lineList.add(indent + "  ResultCode resultCode = " + requestID +
681           "Result.getResultCode();");
682      lineList.add(indent + "  String message = " + requestID +
683           "Result.getMessage();");
684      lineList.add(indent + "  String matchedDN = " + requestID +
685           "Result.getMatchedDN();");
686      lineList.add(indent + "  String[] referralURLs = " + requestID +
687           "Result.getReferralURLs();");
688      lineList.add(indent + "  String responseOID = " + requestID +
689           "Result.getOID();");
690      lineList.add(indent + "  ASN1OctetString responseValue = " + requestID +
691           "Result.getValue();");
692      lineList.add(indent + "  Control[] responseControls = " + requestID +
693           "Result.getResponseControls();");
694      lineList.add(indent + '}');
695      lineList.add(indent + "catch (LDAPException e)");
696      lineList.add(indent + '{');
697      lineList.add(indent + "  // A problem was encountered while attempting " +
698           "to process the extended operation.");
699      lineList.add(indent + "  // Maybe the following will help explain why.");
700      lineList.add(indent + "  ResultCode resultCode = e.getResultCode();");
701      lineList.add(indent + "  String message = e.getMessage();");
702      lineList.add(indent + "  String matchedDN = e.getMatchedDN();");
703      lineList.add(indent + "  String[] referralURLs = e.getReferralURLs();");
704      lineList.add(indent + "  Control[] responseControls = " +
705           "e.getResponseControls();");
706      lineList.add(indent + '}');
707    }
708  }
709}