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