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