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.Debug;
041import com.unboundid.util.Extensible;
042import com.unboundid.util.InternalUseOnly;
043import com.unboundid.util.NotMutable;
044import com.unboundid.util.StaticUtils;
045import com.unboundid.util.ThreadSafety;
046import com.unboundid.util.ThreadSafetyLevel;
047import com.unboundid.util.Validator;
048
049import static com.unboundid.ldap.sdk.LDAPMessages.*;
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<>();
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    Validator.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    Validator.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    Validator.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    Validator.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    messageID = extendedRequest.messageID;
198    oid = extendedRequest.oid;
199    value = extendedRequest.value;
200  }
201
202
203
204  /**
205   * Retrieves the OID for this extended request.
206   *
207   * @return  The OID for this extended request.
208   */
209  public final String getOID()
210  {
211    return oid;
212  }
213
214
215
216  /**
217   * Indicates whether this extended request has a value.
218   *
219   * @return  {@code true} if this extended request has a value, or
220   *          {@code false} if not.
221   */
222  public final boolean hasValue()
223  {
224    return (value != null);
225  }
226
227
228
229  /**
230   * Retrieves the encoded value for this extended request, if available.
231   *
232   * @return  The encoded value for this extended request, or {@code null} if
233   *          this request does not have a value.
234   */
235  public final ASN1OctetString getValue()
236  {
237    return value;
238  }
239
240
241
242  /**
243   * {@inheritDoc}
244   */
245  @Override()
246  public final byte getProtocolOpType()
247  {
248    return LDAPMessage.PROTOCOL_OP_TYPE_EXTENDED_REQUEST;
249  }
250
251
252
253  /**
254   * {@inheritDoc}
255   */
256  @Override()
257  public final void writeTo(final ASN1Buffer writer)
258  {
259    final ASN1BufferSequence requestSequence =
260         writer.beginSequence(LDAPMessage.PROTOCOL_OP_TYPE_EXTENDED_REQUEST);
261    writer.addOctetString(TYPE_EXTENDED_REQUEST_OID, oid);
262
263    if (value != null)
264    {
265      writer.addOctetString(TYPE_EXTENDED_REQUEST_VALUE, value.getValue());
266    }
267    requestSequence.end();
268  }
269
270
271
272  /**
273   * Encodes the extended request protocol op to an ASN.1 element.
274   *
275   * @return  The ASN.1 element with the encoded extended request protocol op.
276   */
277  @Override()
278  public ASN1Element encodeProtocolOp()
279  {
280    // Create the extended request protocol op.
281    final ASN1Element[] protocolOpElements;
282    if (value == null)
283    {
284      protocolOpElements = new ASN1Element[]
285      {
286        new ASN1OctetString(TYPE_EXTENDED_REQUEST_OID, oid)
287      };
288    }
289    else
290    {
291      protocolOpElements = new ASN1Element[]
292      {
293        new ASN1OctetString(TYPE_EXTENDED_REQUEST_OID, oid),
294        new ASN1OctetString(TYPE_EXTENDED_REQUEST_VALUE, value.getValue())
295      };
296    }
297
298    return new ASN1Sequence(LDAPMessage.PROTOCOL_OP_TYPE_EXTENDED_REQUEST,
299                            protocolOpElements);
300  }
301
302
303
304  /**
305   * Sends this extended request to the directory server over the provided
306   * connection and returns the associated response.
307   *
308   * @param  connection  The connection to use to communicate with the directory
309   *                     server.
310   * @param  depth       The current referral depth for this request.  It should
311   *                     always be one for the initial request, and should only
312   *                     be incremented when following referrals.
313   *
314   * @return  An LDAP result object that provides information about the result
315   *          of the extended operation processing.
316   *
317   * @throws  LDAPException  If a problem occurs while sending the request or
318   *                         reading the response.
319   */
320  @Override()
321  protected ExtendedResult process(final LDAPConnection connection,
322                                   final int depth)
323            throws LDAPException
324  {
325    if (connection.synchronousMode())
326    {
327      return processSync(connection);
328    }
329
330    // Create the LDAP message.
331    messageID = connection.nextMessageID();
332    final LDAPMessage message = new LDAPMessage(messageID, this, getControls());
333
334
335    // Register with the connection reader to be notified of responses for the
336    // request that we've created.
337    connection.registerResponseAcceptor(messageID, this);
338
339
340    try
341    {
342      // Send the request to the server.
343      final long responseTimeout = getResponseTimeoutMillis(connection);
344      Debug.debugLDAPRequest(Level.INFO, this, messageID, connection);
345      final long requestTime = System.nanoTime();
346      connection.getConnectionStatistics().incrementNumExtendedRequests();
347      if (this instanceof StartTLSExtendedRequest)
348      {
349        connection.sendMessage(message, 50L);
350      }
351      else
352      {
353        connection.sendMessage(message, responseTimeout);
354      }
355
356      // Wait for and process the response.
357      final LDAPResponse response;
358      try
359      {
360        if (responseTimeout > 0)
361        {
362          response = responseQueue.poll(responseTimeout, TimeUnit.MILLISECONDS);
363        }
364        else
365        {
366          response = responseQueue.take();
367        }
368      }
369      catch (final InterruptedException ie)
370      {
371        Debug.debugException(ie);
372        Thread.currentThread().interrupt();
373        throw new LDAPException(ResultCode.LOCAL_ERROR,
374             ERR_EXTOP_INTERRUPTED.get(connection.getHostPort()), ie);
375      }
376
377      return handleResponse(connection, response, requestTime);
378    }
379    finally
380    {
381      connection.deregisterResponseAcceptor(messageID);
382    }
383  }
384
385
386
387  /**
388   * Processes this extended operation in synchronous mode, in which the same
389   * thread will send the request and read the response.
390   *
391   * @param  connection  The connection to use to communicate with the directory
392   *                     server.
393   *
394   * @return  An LDAP result object that provides information about the result
395   *          of the extended processing.
396   *
397   * @throws  LDAPException  If a problem occurs while sending the request or
398   *                         reading the response.
399   */
400  private ExtendedResult processSync(final LDAPConnection connection)
401          throws LDAPException
402  {
403    // Create the LDAP message.
404    messageID = connection.nextMessageID();
405    final LDAPMessage message =
406         new LDAPMessage(messageID,  this, getControls());
407
408
409    // Send the request to the server.
410    final long requestTime = System.nanoTime();
411    Debug.debugLDAPRequest(Level.INFO, this, messageID, connection);
412    connection.getConnectionStatistics().incrementNumExtendedRequests();
413    connection.sendMessage(message, getResponseTimeoutMillis(connection));
414
415    while (true)
416    {
417      final LDAPResponse response;
418      try
419      {
420        response = connection.readResponse(messageID);
421      }
422      catch (final LDAPException le)
423      {
424        Debug.debugException(le);
425
426        if ((le.getResultCode() == ResultCode.TIMEOUT) &&
427            connection.getConnectionOptions().abandonOnTimeout())
428        {
429          connection.abandon(messageID);
430        }
431
432        throw le;
433      }
434
435      if (response instanceof IntermediateResponse)
436      {
437        final IntermediateResponseListener listener =
438             getIntermediateResponseListener();
439        if (listener != null)
440        {
441          listener.intermediateResponseReturned(
442               (IntermediateResponse) response);
443        }
444      }
445      else
446      {
447        return handleResponse(connection, response, requestTime);
448      }
449    }
450  }
451
452
453
454  /**
455   * Performs the necessary processing for handling a response.
456   *
457   * @param  connection   The connection used to read the response.
458   * @param  response     The response to be processed.
459   * @param  requestTime  The time the request was sent to the server.
460   *
461   * @return  The extended result.
462   *
463   * @throws  LDAPException  If a problem occurs.
464   */
465  private ExtendedResult handleResponse(final LDAPConnection connection,
466                                        final LDAPResponse response,
467                                        final long requestTime)
468          throws LDAPException
469  {
470    if (response == null)
471    {
472      final long waitTime =
473           StaticUtils.nanosToMillis(System.nanoTime() - requestTime);
474      if (connection.getConnectionOptions().abandonOnTimeout())
475      {
476        connection.abandon(messageID);
477      }
478
479      throw new LDAPException(ResultCode.TIMEOUT,
480           ERR_EXTENDED_CLIENT_TIMEOUT.get(waitTime, messageID, oid,
481                connection.getHostPort()));
482    }
483
484    if (response instanceof ConnectionClosedResponse)
485    {
486      final ConnectionClosedResponse ccr = (ConnectionClosedResponse) response;
487      final String msg = ccr.getMessage();
488      if (msg == null)
489      {
490        // The connection was closed while waiting for the response.
491        throw new LDAPException(ccr.getResultCode(),
492             ERR_CONN_CLOSED_WAITING_FOR_EXTENDED_RESPONSE.get(
493                  connection.getHostPort(), toString()));
494      }
495      else
496      {
497        // The connection was closed while waiting for the response.
498        throw new LDAPException(ccr.getResultCode(),
499             ERR_CONN_CLOSED_WAITING_FOR_EXTENDED_RESPONSE_WITH_MESSAGE.get(
500                  connection.getHostPort(), toString(), msg));
501      }
502    }
503
504    connection.getConnectionStatistics().incrementNumExtendedResponses(
505         System.nanoTime() - requestTime);
506    return (ExtendedResult) response;
507  }
508
509
510
511  /**
512   * {@inheritDoc}
513   */
514  @InternalUseOnly()
515  @Override()
516  public final void responseReceived(final LDAPResponse response)
517         throws LDAPException
518  {
519    try
520    {
521      responseQueue.put(response);
522    }
523    catch (final Exception e)
524    {
525      Debug.debugException(e);
526
527      if (e instanceof InterruptedException)
528      {
529        Thread.currentThread().interrupt();
530      }
531
532      throw new LDAPException(ResultCode.LOCAL_ERROR,
533           ERR_EXCEPTION_HANDLING_RESPONSE.get(
534                StaticUtils.getExceptionMessage(e)),
535           e);
536    }
537  }
538
539
540
541  /**
542   * {@inheritDoc}
543   */
544  @Override()
545  public final int getLastMessageID()
546  {
547    return messageID;
548  }
549
550
551
552  /**
553   * {@inheritDoc}
554   */
555  @Override()
556  public final OperationType getOperationType()
557  {
558    return OperationType.EXTENDED;
559  }
560
561
562
563  /**
564   * {@inheritDoc}.  Subclasses should override this method to return a
565   * duplicate of the appropriate type.
566   */
567  @Override()
568  public ExtendedRequest duplicate()
569  {
570    return duplicate(getControls());
571  }
572
573
574
575  /**
576   * {@inheritDoc}.  Subclasses should override this method to return a
577   * duplicate of the appropriate type.
578   */
579  @Override()
580  public ExtendedRequest duplicate(final Control[] controls)
581  {
582    final ExtendedRequest r = new ExtendedRequest(oid, value, controls);
583    r.setResponseTimeoutMillis(getResponseTimeoutMillis(null));
584    return r;
585  }
586
587
588
589  /**
590   * Retrieves the user-friendly name for the extended request, if available.
591   * If no user-friendly name has been defined, then the OID will be returned.
592   *
593   * @return  The user-friendly name for this extended request, or the OID if no
594   *          user-friendly name is available.
595   */
596  public String getExtendedRequestName()
597  {
598    // By default, we will return the OID.  Subclasses should override this to
599    // provide the user-friendly name.
600    return oid;
601  }
602
603
604
605  /**
606   * {@inheritDoc}
607   */
608  @Override()
609  public void toString(final StringBuilder buffer)
610  {
611    buffer.append("ExtendedRequest(oid='");
612    buffer.append(oid);
613    buffer.append('\'');
614
615    final Control[] controls = getControls();
616    if (controls.length > 0)
617    {
618      buffer.append(", controls={");
619      for (int i=0; i < controls.length; i++)
620      {
621        if (i > 0)
622        {
623          buffer.append(", ");
624        }
625
626        buffer.append(controls[i]);
627      }
628      buffer.append('}');
629    }
630
631    buffer.append(')');
632  }
633
634
635
636  /**
637   * {@inheritDoc}
638   */
639  @Override()
640  public void toCode(final List<String> lineList, final String requestID,
641                     final int indentSpaces, final boolean includeProcessing)
642  {
643    // Create the request variable.
644    final ArrayList<ToCodeArgHelper> constructorArgs = new ArrayList<>(3);
645    constructorArgs.add(ToCodeArgHelper.createString(oid, "Request OID"));
646    constructorArgs.add(ToCodeArgHelper.createASN1OctetString(value,
647         "Request Value"));
648
649    final Control[] controls = getControls();
650    if (controls.length > 0)
651    {
652      constructorArgs.add(ToCodeArgHelper.createControlArray(controls,
653           "Request Controls"));
654    }
655
656    ToCodeHelper.generateMethodCall(lineList, indentSpaces, "ExtendedRequest",
657         requestID + "Request", "new ExtendedRequest", constructorArgs);
658
659
660    // Add lines for processing the request and obtaining the result.
661    if (includeProcessing)
662    {
663      // Generate a string with the appropriate indent.
664      final StringBuilder buffer = new StringBuilder();
665      for (int i=0; i < indentSpaces; i++)
666      {
667        buffer.append(' ');
668      }
669      final String indent = buffer.toString();
670
671      lineList.add("");
672      lineList.add(indent + "try");
673      lineList.add(indent + '{');
674      lineList.add(indent + "  ExtendedResult " + requestID +
675           "Result = connection.processExtendedOperation(" + requestID +
676           "Request);");
677      lineList.add(indent + "  // The extended operation was processed and " +
678           "we have a result.");
679      lineList.add(indent + "  // This does not necessarily mean that the " +
680           "operation was successful.");
681      lineList.add(indent + "  // Examine the result details for more " +
682           "information.");
683      lineList.add(indent + "  ResultCode resultCode = " + requestID +
684           "Result.getResultCode();");
685      lineList.add(indent + "  String message = " + requestID +
686           "Result.getMessage();");
687      lineList.add(indent + "  String matchedDN = " + requestID +
688           "Result.getMatchedDN();");
689      lineList.add(indent + "  String[] referralURLs = " + requestID +
690           "Result.getReferralURLs();");
691      lineList.add(indent + "  String responseOID = " + requestID +
692           "Result.getOID();");
693      lineList.add(indent + "  ASN1OctetString responseValue = " + requestID +
694           "Result.getValue();");
695      lineList.add(indent + "  Control[] responseControls = " + requestID +
696           "Result.getResponseControls();");
697      lineList.add(indent + '}');
698      lineList.add(indent + "catch (LDAPException e)");
699      lineList.add(indent + '{');
700      lineList.add(indent + "  // A problem was encountered while attempting " +
701           "to process the extended operation.");
702      lineList.add(indent + "  // Maybe the following will help explain why.");
703      lineList.add(indent + "  ResultCode resultCode = e.getResultCode();");
704      lineList.add(indent + "  String message = e.getMessage();");
705      lineList.add(indent + "  String matchedDN = e.getMatchedDN();");
706      lineList.add(indent + "  String[] referralURLs = e.getReferralURLs();");
707      lineList.add(indent + "  Control[] responseControls = " +
708           "e.getResponseControls();");
709      lineList.add(indent + '}');
710    }
711  }
712}