001package com.github.theholywaffle.teamspeak3;
002
003/*
004 * #%L
005 * TeamSpeak 3 Java API
006 * %%
007 * Copyright (C) 2014 Bert De Geyter
008 * %%
009 * Permission is hereby granted, free of charge, to any person obtaining a copy
010 * of this software and associated documentation files (the "Software"), to deal
011 * in the Software without restriction, including without limitation the rights
012 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
013 * copies of the Software, and to permit persons to whom the Software is
014 * furnished to do so, subject to the following conditions:
015 * 
016 * The above copyright notice and this permission notice shall be included in
017 * all copies or substantial portions of the Software.
018 * 
019 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
020 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
021 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
022 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
023 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
024 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
025 * THE SOFTWARE.
026 * #L%
027 */
028
029import com.github.theholywaffle.teamspeak3.api.exception.TS3Exception;
030import com.github.theholywaffle.teamspeak3.api.exception.TS3QueryShutDownException;
031import com.github.theholywaffle.teamspeak3.api.reconnect.ConnectionHandler;
032import com.github.theholywaffle.teamspeak3.api.reconnect.ReconnectStrategy;
033import com.github.theholywaffle.teamspeak3.commands.Command;
034import org.slf4j.Logger;
035import org.slf4j.LoggerFactory;
036
037import java.util.concurrent.ExecutorService;
038import java.util.concurrent.Executors;
039import java.util.concurrent.atomic.AtomicBoolean;
040
041public class TS3Query {
042
043        private static final Logger log = LoggerFactory.getLogger(TS3Query.class);
044
045        public static class FloodRate {
046
047                public static final FloodRate DEFAULT = new FloodRate(350);
048                public static final FloodRate UNLIMITED = new FloodRate(0);
049
050                public static FloodRate custom(int milliseconds) {
051                        if (milliseconds < 0) throw new IllegalArgumentException("Timeout must be positive");
052                        return new FloodRate(milliseconds);
053                }
054
055                private final int ms;
056
057                private FloodRate(int ms) {
058                        this.ms = ms;
059                }
060
061                public int getMs() {
062                        return ms;
063                }
064        }
065
066        private final ConnectionHandler connectionHandler;
067        private final EventManager eventManager;
068        private final ExecutorService userThreadPool;
069        private final FileTransferHelper fileTransferHelper;
070        private final TS3Api api;
071        private final TS3ApiAsync asyncApi;
072        private final TS3Config config;
073
074        private final AtomicBoolean connected = new AtomicBoolean(false);
075        private final AtomicBoolean shuttingDown = new AtomicBoolean(false);
076
077        private QueryIO io;
078
079        /**
080         * Creates a TS3Query that connects to a TS3 server at
081         * {@code localhost:10011} using default settings.
082         */
083        public TS3Query() {
084                this(new TS3Config());
085        }
086
087        /**
088         * Creates a customized TS3Query that connects to a server
089         * specified by {@code config}.
090         *
091         * @param config
092         *              configuration for this TS3Query
093         */
094        public TS3Query(TS3Config config) {
095                this.config = config;
096                this.eventManager = new EventManager(this);
097                this.userThreadPool = Executors.newCachedThreadPool();
098                this.fileTransferHelper = new FileTransferHelper(config.getHost());
099                this.connectionHandler = config.getReconnectStrategy().create(config.getConnectionHandler());
100
101                this.asyncApi = new TS3ApiAsync(this);
102                this.api = new TS3Api(asyncApi);
103        }
104
105        /*
106         * Copy constructor only used for ReconnectQuery
107         */
108        private TS3Query(TS3Query query) {
109                this.config = query.config;
110                this.eventManager = query.eventManager;
111                this.userThreadPool = query.userThreadPool;
112                this.fileTransferHelper = query.fileTransferHelper;
113                this.connectionHandler = null;
114
115                this.asyncApi = new TS3ApiAsync(this);
116                this.api = new TS3Api(asyncApi);
117        }
118
119        // PUBLIC
120
121        public void connect() {
122                synchronized (this) {
123                        if (userThreadPool.isShutdown()) {
124                                throw new IllegalStateException("The query has already been shut down");
125                        }
126                }
127
128                QueryIO newIO = new QueryIO(this, config);
129
130                try {
131                        connectionHandler.onConnect(new ReconnectQuery(this, newIO));
132                } catch (Exception e) {
133                        log.error("ConnectionHandler threw exception in connect handler", e);
134                }
135
136                synchronized (this) {
137                        QueryIO oldIO = io;
138                        io = newIO;
139                        if (oldIO != null) {
140                                oldIO.disconnect();
141                                newIO.continueFrom(io);
142                        }
143                        connected.set(true);
144                }
145        }
146
147        /**
148         * Removes and closes all used resources to the TeamSpeak server.
149         */
150        public void exit() {
151                if (shuttingDown.compareAndSet(false, true)) {
152                        try {
153                                // Sending this command will guarantee that all previously sent commands have been processed
154                                api.quit();
155                        } catch (TS3Exception e) {
156                                log.warn("Could not send a quit command to terminate the connection", e);
157                        } finally {
158                                shutDown();
159                        }
160                } else {
161                        // Only 1 thread shall ever send quit, the rest of the threads wait for the first thread to finish
162                        try {
163                                synchronized (this) {
164                                        while (connected.get()) {
165                                                wait();
166                                        }
167                                }
168                        } catch (InterruptedException e) {
169                                // Restore interrupt, then bail out
170                                Thread.currentThread().interrupt();
171                        }
172                }
173        }
174
175        private synchronized void shutDown() {
176                if (userThreadPool.isShutdown()) return;
177
178                if (io != null) {
179                        io.disconnect();
180                        io.failRemainingCommands();
181                }
182
183                userThreadPool.shutdown();
184                connected.set(false);
185                notifyAll();
186        }
187
188        /**
189         * Returns {@code true} if the query is likely connected,
190         * {@code false} if the query is disconnected or currently trying to reconnect.
191         * <p>
192         * Note that the only way to really determine whether the query is connected or not
193         * is to send a command and check whether it succeeds.
194         * Thus this method could return {@code true} almost a minute after the connection
195         * has been lost, when the last keep-alive command was sent.
196         * </p><p>
197         * Please do not use this method to write your own connection handler.
198         * Instead, use the built-in classes in the {@code api.reconnect} package.
199         * </p>
200         *
201         * @return whether the query is connected or not
202         *
203         * @see TS3Config#setReconnectStrategy(ReconnectStrategy)
204         * @see TS3Config#setConnectionHandler(ConnectionHandler)
205         */
206        public boolean isConnected() {
207                return connected.get();
208        }
209
210        public TS3Api getApi() {
211                return api;
212        }
213
214        public TS3ApiAsync getAsyncApi() {
215                return asyncApi;
216        }
217
218        // INTERNAL
219
220        synchronized void doCommandAsync(Command c) {
221                if (userThreadPool.isShutdown()) {
222                        c.getFuture().fail(new TS3QueryShutDownException());
223                        return;
224                }
225
226                io.enqueueCommand(c);
227        }
228
229        void submitUserTask(final String name, final Runnable task) {
230                userThreadPool.submit(new Runnable() {
231                        @Override
232                        public void run() {
233                                try {
234                                        task.run();
235                                } catch (Throwable throwable) {
236                                        log.error(name + " threw an exception", throwable);
237                                }
238                        }
239                });
240        }
241
242        EventManager getEventManager() {
243                return eventManager;
244        }
245
246        FileTransferHelper getFileTransferHelper() {
247                return fileTransferHelper;
248        }
249
250        void fireDisconnect() {
251                connected.set(false);
252
253                submitUserTask("ConnectionHandler disconnect task", new Runnable() {
254                        @Override
255                        public void run() {
256                                handleDisconnect();
257                        }
258                });
259        }
260
261        private synchronized void handleDisconnect() {
262                try {
263                        connectionHandler.onDisconnect(this);
264                } finally {
265                        if (!connected.get()) {
266                                shuttingDown.set(true); // Try to prevent extraneous exit commands
267                                shutDown();
268                        }
269                }
270        }
271
272        private static class ReconnectQuery extends TS3Query {
273
274                private final TS3Query parent;
275
276                private ReconnectQuery(TS3Query query, QueryIO io) {
277                        super(query);
278                        super.io = io;
279                        this.parent = query;
280                }
281
282                @Override
283                public void connect() {
284                        throw new UnsupportedOperationException("Can't call connect from onConnect");
285                }
286
287                @Override
288                public void exit() {
289                        parent.exit();
290                }
291        }
292}