001/*
002 * Copyright 2008-2018 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2018 Ping Identity Corporation
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<>(1);
113    cancelRequested = new AtomicBoolean(false);
114    result          = new AtomicReference<>();
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  @Override()
150  public boolean cancel(final boolean mayInterruptIfRunning)
151  {
152    // If the operation has already completed, then we can't cancel it.
153    if (isDone())
154    {
155      return false;
156    }
157
158    // Try to send a request to cancel the operation.
159    try
160    {
161      cancelRequested.set(true);
162      result.compareAndSet(null,
163           new LDAPResult(messageID, ResultCode.USER_CANCELED,
164                INFO_ASYNC_REQUEST_USER_CANCELED.get(), null,
165                StaticUtils.NO_STRINGS, StaticUtils.NO_CONTROLS));
166
167      connection.abandon(this);
168    }
169    catch (final Exception e)
170    {
171      Debug.debugException(e);
172    }
173
174    return true;
175  }
176
177
178
179  /**
180   * Indicates whether an attempt has been made to cancel the associated
181   * operation before it completed.
182   *
183   * @return  {@code true} if an attempt was made to cancel the operation, or
184   *          {@code false} if no cancel attempt was made, or if the operation
185   *          completed before it could be canceled.
186   */
187  @Override()
188  public boolean isCancelled()
189  {
190    return cancelRequested.get();
191  }
192
193
194
195  /**
196   * Indicates whether the associated operation has completed, regardless of
197   * whether it completed normally, completed with an error, or was canceled
198   * before starting.
199   *
200   * @return  {@code true} if the associated operation has completed, or if an
201   *          attempt has been made to cancel it, or {@code false} if the
202   *          operation has not yet completed and no cancel attempt has been
203   *          made.
204   */
205  @Override()
206  public boolean isDone()
207  {
208    if (cancelRequested.get())
209    {
210      return true;
211    }
212
213    if (result.get() != null)
214    {
215      return true;
216    }
217
218    final LDAPResult newResult = resultQueue.poll();
219    if (newResult != null)
220    {
221      result.set(newResult);
222      return true;
223    }
224
225    return false;
226  }
227
228
229
230  /**
231   * Attempts to get the result for the associated operation, waiting if
232   * necessary for it to complete.  Note that this method will differ from the
233   * behavior defined in the {@code java.util.concurrent.Future} API in that it
234   * will not wait forever.  Rather, it will wait for no more than the length of
235   * time specified as the maximum response time defined in the connection
236   * options for the connection used to send the asynchronous request.  This is
237   * necessary because the operation may have been abandoned or otherwise
238   * interrupted, or the associated connection may have become invalidated, in
239   * a way that the LDAP SDK cannot detect.
240   *
241   * @return  The result for the associated operation.  If the operation has
242   *          been canceled, or if no result has been received within the
243   *          response timeout period, then a generated response will be
244   *          returned.
245   *
246   * @throws  InterruptedException  If the thread calling this method was
247   *                                interrupted before a result was received.
248   */
249  @Override()
250  public LDAPResult get()
251         throws InterruptedException
252  {
253    final long maxWaitTime =
254         connection.getConnectionOptions().getResponseTimeoutMillis();
255
256    try
257    {
258      return get(maxWaitTime, TimeUnit.MILLISECONDS);
259    }
260    catch (final TimeoutException te)
261    {
262      Debug.debugException(te);
263      return new LDAPResult(messageID, ResultCode.TIMEOUT, te.getMessage(),
264           null, StaticUtils.NO_STRINGS, StaticUtils.NO_CONTROLS);
265    }
266  }
267
268
269
270  /**
271   * Attempts to get the result for the associated operation, waiting if
272   * necessary for up to the specified length of time for the operation to
273   * complete.
274   *
275   * @param  timeout   The maximum length of time to wait for the response.
276   * @param  timeUnit  The time unit for the provided {@code timeout} value.
277   *
278   * @return  The result for the associated operation.  If the operation has
279   *          been canceled, then a generated response will be returned.
280   *
281   * @throws  InterruptedException  If the thread calling this method was
282   *                                interrupted before a result was received.
283   *
284   * @throws  TimeoutException  If a timeout was encountered before the result
285   *                            could be obtained.
286   */
287  @Override()
288  public LDAPResult get(final long timeout, final TimeUnit timeUnit)
289         throws InterruptedException, TimeoutException
290  {
291    final LDAPResult newResult = resultQueue.poll();
292    if (newResult != null)
293    {
294      result.set(newResult);
295      return newResult;
296    }
297
298    final LDAPResult previousResult = result.get();
299    if (previousResult != null)
300    {
301      return previousResult;
302    }
303
304    final LDAPResult resultAfterWaiting = resultQueue.poll(timeout, timeUnit);
305    if (resultAfterWaiting == null)
306    {
307      final long timeoutMillis = timeUnit.toMillis(timeout);
308      throw new TimeoutException(
309           WARN_ASYNC_REQUEST_GET_TIMEOUT.get(timeoutMillis));
310    }
311    else
312    {
313      result.set(resultAfterWaiting);
314      return resultAfterWaiting;
315    }
316  }
317
318
319
320  /**
321   * Sets the timer task that may be used to cancel this result after a period
322   * of time.
323   *
324   * @param  timerTask  The timer task that may be used to cancel this result
325   *                    after a period of time.  It may be {@code null} if no
326   *                    timer task should be used.
327   */
328  void setTimerTask(final AsyncTimeoutTimerTask timerTask)
329  {
330    this.timerTask = timerTask;
331  }
332
333
334
335  /**
336   * Sets the result for the associated operation.
337   *
338   * @param  result  The result for the associated operation.  It must not be
339   *                 {@code null}.
340   */
341  void setResult(final LDAPResult result)
342  {
343    resultQueue.offer(result);
344
345    final AsyncTimeoutTimerTask t = timerTask;
346    if (t != null)
347    {
348      t.cancel();
349      connection.getTimer().purge();
350      timerTask = null;
351    }
352  }
353
354
355
356  /**
357   * Retrieves a hash code for this async request ID.
358   *
359   * @return  A hash code for this async request ID.
360   */
361  @Override()
362  public int hashCode()
363  {
364    return messageID;
365  }
366
367
368
369  /**
370   * Indicates whether the provided object is equal to this async request ID.
371   *
372   * @param  o  The object for which to make the determination.
373   *
374   * @return  {@code true} if the provided object is equal to this async request
375   *          ID, or {@code false} if not.
376   */
377  @Override()
378  public boolean equals(final Object o)
379  {
380    if (o == null)
381    {
382      return false;
383    }
384
385    if (o == this)
386    {
387      return true;
388    }
389
390    if (o instanceof AsyncRequestID)
391    {
392      return (((AsyncRequestID) o).messageID == messageID);
393    }
394    else
395    {
396      return false;
397    }
398  }
399
400
401
402  /**
403   * Retrieves a string representation of this async request ID.
404   *
405   * @return  A string representation of this async request ID.
406   */
407  @Override()
408  public String toString()
409  {
410    return "AsyncRequestID(messageID=" + messageID + ')';
411  }
412}