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}