001/*
002 * Copyright 2007-2016 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2016 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        throw new LDAPException(ResultCode.LOCAL_ERROR,
360             ERR_EXTOP_INTERRUPTED.get(connection.getHostPort()), ie);
361      }
362
363      return handleResponse(connection, response, requestTime);
364    }
365    finally
366    {
367      connection.deregisterResponseAcceptor(messageID);
368    }
369  }
370
371
372
373  /**
374   * Processes this extended operation in synchronous mode, in which the same
375   * thread will send the request and read the response.
376   *
377   * @param  connection  The connection to use to communicate with the directory
378   *                     server.
379   *
380   * @return  An LDAP result object that provides information about the result
381   *          of the extended processing.
382   *
383   * @throws  LDAPException  If a problem occurs while sending the request or
384   *                         reading the response.
385   */
386  private ExtendedResult processSync(final LDAPConnection connection)
387          throws LDAPException
388  {
389    // Create the LDAP message.
390    messageID = connection.nextMessageID();
391    final LDAPMessage message =
392         new LDAPMessage(messageID,  this, getControls());
393
394
395    // Set the appropriate timeout on the socket.
396    try
397    {
398      connection.getConnectionInternals(true).getSocket().setSoTimeout(
399           (int) getResponseTimeoutMillis(connection));
400    }
401    catch (Exception e)
402    {
403      debugException(e);
404    }
405
406
407    // Send the request to the server.
408    final long requestTime = System.nanoTime();
409    debugLDAPRequest(this);
410    connection.getConnectionStatistics().incrementNumExtendedRequests();
411    connection.sendMessage(message);
412
413    while (true)
414    {
415      final LDAPResponse response;
416      try
417      {
418        response = connection.readResponse(messageID);
419      }
420      catch (final LDAPException le)
421      {
422        debugException(le);
423
424        if ((le.getResultCode() == ResultCode.TIMEOUT) &&
425            connection.getConnectionOptions().abandonOnTimeout())
426        {
427          connection.abandon(messageID);
428        }
429
430        throw le;
431      }
432
433      if (response instanceof IntermediateResponse)
434      {
435        final IntermediateResponseListener listener =
436             getIntermediateResponseListener();
437        if (listener != null)
438        {
439          listener.intermediateResponseReturned(
440               (IntermediateResponse) response);
441        }
442      }
443      else
444      {
445        return handleResponse(connection, response, requestTime);
446      }
447    }
448  }
449
450
451
452  /**
453   * Performs the necessary processing for handling a response.
454   *
455   * @param  connection   The connection used to read the response.
456   * @param  response     The response to be processed.
457   * @param  requestTime  The time the request was sent to the server.
458   *
459   * @return  The extended result.
460   *
461   * @throws  LDAPException  If a problem occurs.
462   */
463  private ExtendedResult handleResponse(final LDAPConnection connection,
464                                        final LDAPResponse response,
465                                        final long requestTime)
466          throws LDAPException
467  {
468    if (response == null)
469    {
470      final long waitTime = nanosToMillis(System.nanoTime() - requestTime);
471      if (connection.getConnectionOptions().abandonOnTimeout())
472      {
473        connection.abandon(messageID);
474      }
475
476      throw new LDAPException(ResultCode.TIMEOUT,
477           ERR_EXTENDED_CLIENT_TIMEOUT.get(waitTime, messageID, oid,
478                connection.getHostPort()));
479    }
480
481    if (response instanceof ConnectionClosedResponse)
482    {
483      final ConnectionClosedResponse ccr = (ConnectionClosedResponse) response;
484      final String msg = ccr.getMessage();
485      if (msg == null)
486      {
487        // The connection was closed while waiting for the response.
488        throw new LDAPException(ccr.getResultCode(),
489             ERR_CONN_CLOSED_WAITING_FOR_EXTENDED_RESPONSE.get(
490                  connection.getHostPort(), toString()));
491      }
492      else
493      {
494        // The connection was closed while waiting for the response.
495        throw new LDAPException(ccr.getResultCode(),
496             ERR_CONN_CLOSED_WAITING_FOR_EXTENDED_RESPONSE_WITH_MESSAGE.get(
497                  connection.getHostPort(), toString(), msg));
498      }
499    }
500
501    connection.getConnectionStatistics().incrementNumExtendedResponses(
502         System.nanoTime() - requestTime);
503    return (ExtendedResult) response;
504  }
505
506
507
508  /**
509   * {@inheritDoc}
510   */
511  @InternalUseOnly()
512  public final void responseReceived(final LDAPResponse response)
513         throws LDAPException
514  {
515    try
516    {
517      responseQueue.put(response);
518    }
519    catch (Exception e)
520    {
521      debugException(e);
522      throw new LDAPException(ResultCode.LOCAL_ERROR,
523           ERR_EXCEPTION_HANDLING_RESPONSE.get(getExceptionMessage(e)), e);
524    }
525  }
526
527
528
529  /**
530   * {@inheritDoc}
531   */
532  @Override()
533  public final int getLastMessageID()
534  {
535    return messageID;
536  }
537
538
539
540  /**
541   * {@inheritDoc}
542   */
543  @Override()
544  public final OperationType getOperationType()
545  {
546    return OperationType.EXTENDED;
547  }
548
549
550
551  /**
552   * {@inheritDoc}.  Subclasses should override this method to return a
553   * duplicate of the appropriate type.
554   */
555  public ExtendedRequest duplicate()
556  {
557    return duplicate(getControls());
558  }
559
560
561
562  /**
563   * {@inheritDoc}.  Subclasses should override this method to return a
564   * duplicate of the appropriate type.
565   */
566  public ExtendedRequest duplicate(final Control[] controls)
567  {
568    final ExtendedRequest r = new ExtendedRequest(oid, value, controls);
569    r.setResponseTimeoutMillis(getResponseTimeoutMillis(null));
570    return r;
571  }
572
573
574
575  /**
576   * Retrieves the user-friendly name for the extended request, if available.
577   * If no user-friendly name has been defined, then the OID will be returned.
578   *
579   * @return  The user-friendly name for this extended request, or the OID if no
580   *          user-friendly name is available.
581   */
582  public String getExtendedRequestName()
583  {
584    // By default, we will return the OID.  Subclasses should override this to
585    // provide the user-friendly name.
586    return oid;
587  }
588
589
590
591  /**
592   * {@inheritDoc}
593   */
594  @Override()
595  public void toString(final StringBuilder buffer)
596  {
597    buffer.append("ExtendedRequest(oid='");
598    buffer.append(oid);
599    buffer.append('\'');
600
601    final Control[] controls = getControls();
602    if (controls.length > 0)
603    {
604      buffer.append(", controls={");
605      for (int i=0; i < controls.length; i++)
606      {
607        if (i > 0)
608        {
609          buffer.append(", ");
610        }
611
612        buffer.append(controls[i]);
613      }
614      buffer.append('}');
615    }
616
617    buffer.append(')');
618  }
619
620
621
622  /**
623   * {@inheritDoc}
624   */
625  public void toCode(final List<String> lineList, final String requestID,
626                     final int indentSpaces, final boolean includeProcessing)
627  {
628    // Create the request variable.
629    final ArrayList<ToCodeArgHelper> constructorArgs =
630         new ArrayList<ToCodeArgHelper>(3);
631    constructorArgs.add(ToCodeArgHelper.createString(oid, "Request OID"));
632    constructorArgs.add(ToCodeArgHelper.createASN1OctetString(value,
633         "Request Value"));
634
635    final Control[] controls = getControls();
636    if (controls.length > 0)
637    {
638      constructorArgs.add(ToCodeArgHelper.createControlArray(controls,
639           "Request Controls"));
640    }
641
642    ToCodeHelper.generateMethodCall(lineList, indentSpaces, "ExtendedRequest",
643         requestID + "Request", "new ExtendedRequest", constructorArgs);
644
645
646    // Add lines for processing the request and obtaining the result.
647    if (includeProcessing)
648    {
649      // Generate a string with the appropriate indent.
650      final StringBuilder buffer = new StringBuilder();
651      for (int i=0; i < indentSpaces; i++)
652      {
653        buffer.append(' ');
654      }
655      final String indent = buffer.toString();
656
657      lineList.add("");
658      lineList.add(indent + "try");
659      lineList.add(indent + '{');
660      lineList.add(indent + "  ExtendedResult " + requestID +
661           "Result = connection.processExtendedOperation(" + requestID +
662           "Request);");
663      lineList.add(indent + "  // The extended operation was processed and " +
664           "we have a result.");
665      lineList.add(indent + "  // This does not necessarily mean that the " +
666           "operation was successful.");
667      lineList.add(indent + "  // Examine the result details for more " +
668           "information.");
669      lineList.add(indent + "  ResultCode resultCode = " + requestID +
670           "Result.getResultCode();");
671      lineList.add(indent + "  String message = " + requestID +
672           "Result.getMessage();");
673      lineList.add(indent + "  String matchedDN = " + requestID +
674           "Result.getMatchedDN();");
675      lineList.add(indent + "  String[] referralURLs = " + requestID +
676           "Result.getReferralURLs();");
677      lineList.add(indent + "  String responseOID = " + requestID +
678           "Result.getOID();");
679      lineList.add(indent + "  ASN1OctetString responseValue = " + requestID +
680           "Result.getValue();");
681      lineList.add(indent + "  Control[] responseControls = " + requestID +
682           "Result.getResponseControls();");
683      lineList.add(indent + '}');
684      lineList.add(indent + "catch (LDAPException e)");
685      lineList.add(indent + '{');
686      lineList.add(indent + "  // A problem was encountered while attempting " +
687           "to process the extended operation.");
688      lineList.add(indent + "  // Maybe the following will help explain why.");
689      lineList.add(indent + "  ResultCode resultCode = e.getResultCode();");
690      lineList.add(indent + "  String message = e.getMessage();");
691      lineList.add(indent + "  String matchedDN = e.getMatchedDN();");
692      lineList.add(indent + "  String[] referralURLs = e.getReferralURLs();");
693      lineList.add(indent + "  Control[] responseControls = " +
694           "e.getResponseControls();");
695      lineList.add(indent + '}');
696    }
697  }
698}