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