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}