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