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