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