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