001/*
002 * Copyright 2008-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.io.Serializable;
026import java.util.concurrent.ArrayBlockingQueue;
027import java.util.concurrent.Future;
028import java.util.concurrent.TimeoutException;
029import java.util.concurrent.TimeUnit;
030import java.util.concurrent.atomic.AtomicBoolean;
031import java.util.concurrent.atomic.AtomicReference;
032
033import com.unboundid.util.Debug;
034import com.unboundid.util.NotMutable;
035import com.unboundid.util.StaticUtils;
036import com.unboundid.util.ThreadSafety;
037import com.unboundid.util.ThreadSafetyLevel;
038
039import static com.unboundid.ldap.sdk.LDAPMessages.*;
040
041
042
043/**
044 * This class defines an object that provides information about a request that
045 * was initiated asynchronously.  It may be used to abandon or cancel the
046 * associated request.  This class also implements the
047 * {@code java.util.concurrent.Future} interface, so it may be used in that
048 * manner.
049 * <BR><BR>
050 * <H2>Example</H2>
051 * The following example initiates an asynchronous modify operation and then
052 * attempts to abandon it:
053 * <PRE>
054 * Modification mod = new Modification(ModificationType.REPLACE,
055 *      "description", "This is the new description.");
056 * ModifyRequest modifyRequest =
057 *      new ModifyRequest("dc=example,dc=com", mod);
058 *
059 * AsyncRequestID asyncRequestID =
060 *      connection.asyncModify(modifyRequest, myAsyncResultListener);
061 *
062 * // Assume that we've waited a reasonable amount of time but the modify
063 * // hasn't completed yet so we'll try to abandon it.
064 *
065 * connection.abandon(asyncRequestID);
066 * </PRE>
067 */
068@NotMutable()
069@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
070public final class AsyncRequestID
071       implements Serializable, Future<LDAPResult>
072{
073  /**
074   * The serial version UID for this serializable class.
075   */
076  private static final long serialVersionUID = 8244005138437962030L;
077
078
079
080  // The queue used to receive the result for the associated operation.
081  private final ArrayBlockingQueue<LDAPResult> resultQueue;
082
083  // A flag indicating whether a request has been made to cancel the operation.
084  private final AtomicBoolean cancelRequested;
085
086  // The result for the associated operation.
087  private final AtomicReference<LDAPResult> result;
088
089  // The message ID for the request message.
090  private final int messageID;
091
092  // The connection used to process the asynchronous operation.
093  private final LDAPConnection connection;
094
095  // The timer task that will allow the associated request to be cancelled.
096  private volatile AsyncTimeoutTimerTask timerTask;
097
098
099
100  /**
101   * Creates a new async request ID with the provided message ID.
102   *
103   * @param  messageID   The message ID for the associated request.
104   * @param  connection  The connection used to process the asynchronous
105   *                     operation.
106   */
107  AsyncRequestID(final int messageID, final LDAPConnection connection)
108  {
109    this.messageID  = messageID;
110    this.connection = connection;
111
112    resultQueue     = new ArrayBlockingQueue<LDAPResult>(1);
113    cancelRequested = new AtomicBoolean(false);
114    result          = new AtomicReference<LDAPResult>();
115    timerTask       = null;
116  }
117
118
119
120  /**
121   * Retrieves the message ID for the associated request.
122   *
123   * @return  The message ID for the associated request.
124   */
125  public int getMessageID()
126  {
127    return messageID;
128  }
129
130
131
132  /**
133   * Attempts to cancel the associated asynchronous operation operation.  This
134   * will cause an abandon request to be sent to the server for the associated
135   * request, but because there is no response to an abandon operation then
136   * there is no way that we can determine whether the operation was actually
137   * abandoned.
138   *
139   * @param  mayInterruptIfRunning  Indicates whether to interrupt the thread
140   *                                running the associated task.  This will be
141   *                                ignored.
142   *
143   * @return  {@code true} if an abandon request was sent to cancel the
144   *          associated operation, or {@code false} if it was not possible to
145   *          send an abandon request because the operation has already
146   *          completed, because an abandon request has already been sent, or
147   *          because an error occurred while trying to send the cancel request.
148   */
149  public boolean cancel(final boolean mayInterruptIfRunning)
150  {
151    // If the operation has already completed, then we can't cancel it.
152    if (isDone())
153    {
154      return false;
155    }
156
157    // Try to send a request to cancel the operation.
158    try
159    {
160      cancelRequested.set(true);
161      result.compareAndSet(null,
162           new LDAPResult(messageID, ResultCode.USER_CANCELED,
163                INFO_ASYNC_REQUEST_USER_CANCELED.get(), null,
164                StaticUtils.NO_STRINGS, StaticUtils.NO_CONTROLS));
165
166      connection.abandon(this);
167    }
168    catch (final Exception e)
169    {
170      Debug.debugException(e);
171    }
172
173    return true;
174  }
175
176
177
178  /**
179   * Indicates whether an attempt has been made to cancel the associated
180   * operation before it completed.
181   *
182   * @return  {@code true} if an attempt was made to cancel the operation, or
183   *          {@code false} if no cancel attempt was made, or if the operation
184   *          completed before it could be canceled.
185   */
186  public boolean isCancelled()
187  {
188    return cancelRequested.get();
189  }
190
191
192
193  /**
194   * Indicates whether the associated operation has completed, regardless of
195   * whether it completed normally, completed with an error, or was canceled
196   * before starting.
197   *
198   * @return  {@code true} if the associated operation has completed, or if an
199   *          attempt has been made to cancel it, or {@code false} if the
200   *          operation has not yet completed and no cancel attempt has been
201   *          made.
202   */
203  public boolean isDone()
204  {
205    if (cancelRequested.get())
206    {
207      return true;
208    }
209
210    if (result.get() != null)
211    {
212      return true;
213    }
214
215    final LDAPResult newResult = resultQueue.poll();
216    if (newResult != null)
217    {
218      result.set(newResult);
219      return true;
220    }
221
222    return false;
223  }
224
225
226
227  /**
228   * Attempts to get the result for the associated operation, waiting if
229   * necessary for it to complete.  Note that this method will differ from the
230   * behavior defined in the {@code java.util.concurrent.Future} API in that it
231   * will not wait forever.  Rather, it will wait for no more than the length of
232   * time specified as the maximum response time defined in the connection
233   * options for the connection used to send the asynchronous request.  This is
234   * necessary because the operation may have been abandoned or otherwise
235   * interrupted, or the associated connection may have become invalidated, in
236   * a way that the LDAP SDK cannot detect.
237   *
238   * @return  The result for the associated operation.  If the operation has
239   *          been canceled, or if no result has been received within the
240   *          response timeout period, then a generated response will be
241   *          returned.
242   *
243   * @throws  InterruptedException  If the thread calling this method was
244   *                                interrupted before a result was received.
245   */
246  public LDAPResult get()
247         throws InterruptedException
248  {
249    final long maxWaitTime =
250         connection.getConnectionOptions().getResponseTimeoutMillis();
251
252    try
253    {
254      return get(maxWaitTime, TimeUnit.MILLISECONDS);
255    }
256    catch (final TimeoutException te)
257    {
258      Debug.debugException(te);
259      return new LDAPResult(messageID, ResultCode.TIMEOUT, te.getMessage(),
260           null, StaticUtils.NO_STRINGS, StaticUtils.NO_CONTROLS);
261    }
262  }
263
264
265
266  /**
267   * Attempts to get the result for the associated operation, waiting if
268   * necessary for up to the specified length of time for the operation to
269   * complete.
270   *
271   * @param  timeout   The maximum length of time to wait for the response.
272   * @param  timeUnit  The time unit for the provided {@code timeout} value.
273   *
274   * @return  The result for the associated operation.  If the operation has
275   *          been canceled, then a generated response will be returned.
276   *
277   * @throws  InterruptedException  If the thread calling this method was
278   *                                interrupted before a result was received.
279   *
280   * @throws  TimeoutException  If a timeout was encountered before the result
281   *                            could be obtained.
282   */
283  public LDAPResult get(final long timeout, final TimeUnit timeUnit)
284         throws InterruptedException, TimeoutException
285  {
286    final LDAPResult newResult = resultQueue.poll();
287    if (newResult != null)
288    {
289      result.set(newResult);
290      return newResult;
291    }
292
293    final LDAPResult previousResult = result.get();
294    if (previousResult != null)
295    {
296      return previousResult;
297    }
298
299    final LDAPResult resultAfterWaiting = resultQueue.poll(timeout, timeUnit);
300    if (resultAfterWaiting == null)
301    {
302      final long timeoutMillis = timeUnit.toMillis(timeout);
303      throw new TimeoutException(
304           WARN_ASYNC_REQUEST_GET_TIMEOUT.get(timeoutMillis));
305    }
306    else
307    {
308      result.set(resultAfterWaiting);
309      return resultAfterWaiting;
310    }
311  }
312
313
314
315  /**
316   * Sets the timer task that may be used to cancel this result after a period
317   * of time.
318   *
319   * @param  timerTask  The timer task that may be used to cancel this result
320   *                    after a period of time.  It may be {@code null} if no
321   *                    timer task should be used.
322   */
323  void setTimerTask(final AsyncTimeoutTimerTask timerTask)
324  {
325    this.timerTask = timerTask;
326  }
327
328
329
330  /**
331   * Sets the result for the associated operation.
332   *
333   * @param  result  The result for the associated operation.  It must not be
334   *                 {@code null}.
335   */
336  void setResult(final LDAPResult result)
337  {
338    resultQueue.offer(result);
339
340    final AsyncTimeoutTimerTask t = timerTask;
341    if (t != null)
342    {
343      t.cancel();
344      connection.getTimer().purge();
345      timerTask = null;
346    }
347  }
348
349
350
351  /**
352   * Retrieves a hash code for this async request ID.
353   *
354   * @return  A hash code for this async request ID.
355   */
356  @Override()
357  public int hashCode()
358  {
359    return messageID;
360  }
361
362
363
364  /**
365   * Indicates whether the provided object is equal to this async request ID.
366   *
367   * @param  o  The object for which to make the determination.
368   *
369   * @return  {@code true} if the provided object is equal to this async request
370   *          ID, or {@code false} if not.
371   */
372  @Override()
373  public boolean equals(final Object o)
374  {
375    if (o == null)
376    {
377      return false;
378    }
379
380    if (o == this)
381    {
382      return true;
383    }
384
385    if (o instanceof AsyncRequestID)
386    {
387      return (((AsyncRequestID) o).messageID == messageID);
388    }
389    else
390    {
391      return false;
392    }
393  }
394
395
396
397  /**
398   * Retrieves a string representation of this async request ID.
399   *
400   * @return  A string representation of this async request ID.
401   */
402  @Override()
403  public String toString()
404  {
405    return "AsyncRequestID(messageID=" + messageID + ')';
406  }
407}