001/** 002 * 003 * Copyright 2018 Florian Schmaus 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.jivesoftware.smack; 018 019import java.util.concurrent.TimeUnit; 020import java.util.concurrent.locks.Condition; 021import java.util.concurrent.locks.Lock; 022 023import org.jivesoftware.smack.SmackException.NoResponseException; 024 025public class SynchronizationPointWithSmackException<E extends Exception, R> { 026 027 private final AbstractXMPPConnection connection; 028 private final Lock connectionLock; 029 private final Condition condition; 030 private final String waitFor; 031 032 // Note that there is no need to make 'state' and 'failureException' volatile. Since 'lock' and 'unlock' have the 033 // same memory synchronization effects as synchronization block enter and leave. 034 private State state; 035 private R result; 036 private E failureException; 037 private SmackException smackException; 038 039 private volatile long waitStart; 040 041 /** 042 * Construct a new synchronization point for the given connection. 043 * 044 * @param connection the connection of this synchronization point. 045 * @param waitFor a description of the event this synchronization point handles. 046 */ 047 public SynchronizationPointWithSmackException(AbstractXMPPConnection connection, String waitFor) { 048 this.connection = connection; 049 this.connectionLock = connection.getConnectionLock(); 050 this.condition = connection.getConnectionLock().newCondition(); 051 this.waitFor = waitFor; 052 init(); 053 } 054 055 /** 056 * Initialize (or reset) this synchronization point. 057 */ 058 public void init() { 059 connectionLock.lock(); 060 state = State.Initial; 061 failureException = null; 062 connectionLock.unlock(); 063 } 064 065 /** 066 * Check if this synchronization point is successful or wait the connections reply timeout. 067 * @throws E if there was a failure 068 * @throws InterruptedException if the connection is interrupted. 069 * @throws SmackException 070 */ 071 public R checkIfSuccessOrWaitOrThrow() throws E, InterruptedException, SmackException { 072 connectionLock.lock(); 073 try { 074 switch (state) { 075 // Return immediately on success or failure 076 case Success: 077 return result; 078 case Failure: 079 if (smackException != null) { 080 throw smackException; 081 } 082 throw failureException; 083 default: 084 // Do nothing 085 break; 086 } 087 waitForConditionOrTimeout(); 088 } finally { 089 connectionLock.unlock(); 090 } 091 092 switch (state) { 093 case Initial: 094 case NoResponse: 095 case RequestSent: 096 throw NoResponseException.newWith(connection, waitFor); 097 case Success: 098 return result; 099 case Failure: 100 if (smackException != null) { 101 throw smackException; 102 } 103 throw failureException; 104 default: 105 throw new AssertionError("Unknown state " + state); 106 } 107 } 108 109 /** 110 * Report this synchronization point as successful. 111 */ 112 public void reportSuccess(R result) { 113 connectionLock.lock(); 114 try { 115 this.result = result; 116 state = State.Success; 117 condition.signalAll(); 118 } 119 finally { 120 connectionLock.unlock(); 121 } 122 } 123 124 /** 125 * Report this synchronization point as failed because of the given exception. The {@code failureException} must be set. 126 * 127 * @param smackException the exception causing this synchronization point to fail. 128 */ 129 public void reportFailure(final SmackException smackException) { 130 reportFailureSetException(() -> { 131 assert failureException != null; 132 this.smackException = smackException; 133 }); 134 } 135 136 /** 137 * Report this synchronization point as failed because of the given exception. The {@code failureException} must be set. 138 * 139 * @param failureException the exception causing this synchronization point to fail. 140 */ 141 public void reportFailure(final E failureException) { 142 reportFailureSetException(() -> { 143 assert failureException != null; 144 this.failureException = failureException; 145 }); 146 } 147 148 private void reportFailureSetException(Runnable setException) { 149 connectionLock.lock(); 150 try { 151 state = State.Failure; 152 setException.run(); 153 condition.signalAll(); 154 } 155 finally { 156 connectionLock.unlock(); 157 } 158 } 159 160 /** 161 * Check if this synchronization point was successful. 162 * 163 * @return true if the synchronization point was successful, false otherwise. 164 */ 165 public boolean wasSuccessful() { 166 connectionLock.lock(); 167 try { 168 return state == State.Success; 169 } 170 finally { 171 connectionLock.unlock(); 172 } 173 } 174 175 /** 176 * Check if this synchronization point has its request already sent. 177 * 178 * @return true if the request was already sent, false otherwise. 179 */ 180 public boolean requestSent() { 181 connectionLock.lock(); 182 try { 183 return state == State.RequestSent; 184 } 185 finally { 186 connectionLock.unlock(); 187 } 188 } 189 190 public Exception getFailureException() { 191 connectionLock.lock(); 192 try { 193 if (smackException != null) { 194 return smackException; 195 } 196 return failureException; 197 } 198 finally { 199 connectionLock.unlock(); 200 } 201 } 202 203 public void resetTimeout() { 204 waitStart = System.currentTimeMillis(); 205 } 206 207 /** 208 * Wait for the condition to become something else as {@link State#RequestSent} or {@link State#Initial}. 209 * {@link #reportSuccess()}, {@link #reportFailure()} and {@link #reportFailure(Exception)} will either set this 210 * synchronization point to {@link State#Success} or {@link State#Failure}. If none of them is set after the 211 * connections reply timeout, this method will set the state of {@link State#NoResponse}. 212 * @throws InterruptedException 213 */ 214 private void waitForConditionOrTimeout() throws InterruptedException { 215 waitStart = System.currentTimeMillis(); 216 while (state == State.RequestSent || state == State.Initial) { 217 long timeout = connection.getReplyTimeout(); 218 long remainingWaitMillis = timeout - (System.currentTimeMillis() - waitStart); 219 long remainingWait = TimeUnit.MILLISECONDS.toNanos(remainingWaitMillis); 220 221 if (remainingWait <= 0) { 222 state = State.NoResponse; 223 break; 224 } 225 226 try { 227 condition.awaitNanos(remainingWait); 228 } catch (InterruptedException e) { 229 state = State.Interrupted; 230 throw e; 231 } 232 } 233 } 234 235 private enum State { 236 Initial, 237 RequestSent, 238 NoResponse, 239 Success, 240 Failure, 241 Interrupted, 242 } 243} 244