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.TS3ConnectionFailedException;
030import com.github.theholywaffle.teamspeak3.api.exception.TS3QueryShutDownException;
031import com.github.theholywaffle.teamspeak3.api.reconnect.ConnectionHandler;
032import com.github.theholywaffle.teamspeak3.api.reconnect.DisconnectingConnectionHandler;
033import com.github.theholywaffle.teamspeak3.api.reconnect.ReconnectStrategy;
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        /**
046         * Artificial delay between sending commands, measured in milliseconds.
047         * <p>
048         * If the query's hostname / IP has not been added to the server's {@code query_ip_whitelist.txt},
049         * you need to use {@link FloodRate#DEFAULT} to prevent the query from being flood-banned.
050         * </p><p>
051         * Calling {@link FloodRate#custom} allows you to use a custom command delay if neither
052         * {@link FloodRate#UNLIMITED} nor {@link FloodRate#DEFAULT} fit your needs.
053         * </p>
054         */
055        public static class FloodRate {
056
057                /**
058                 * Default delay of 350 milliseconds between commands for queries that are not whitelisted.
059                 */
060                public static final FloodRate DEFAULT = new FloodRate(350);
061
062                /**
063                 * No delay between commands. If a query uses this without being whitelisted, it will likely be flood-banned.
064                 */
065                public static final FloodRate UNLIMITED = new FloodRate(0);
066
067                /**
068                 * Creates a FloodRate object that represents a custom command delay.
069                 *
070                 * @param milliseconds
071                 *              the delay between sending commands in milliseconds
072                 *
073                 * @return a new {@code FloodRate} object representing a custom delay
074                 */
075                public static FloodRate custom(int milliseconds) {
076                        if (milliseconds < 0) throw new IllegalArgumentException("Timeout must be positive");
077                        return new FloodRate(milliseconds);
078                }
079
080                private final int ms;
081
082                private FloodRate(int ms) {
083                        this.ms = ms;
084                }
085
086                public int getMs() {
087                        return ms;
088                }
089        }
090
091        /**
092         * The protocol used to communicate with the TeamSpeak3 server.
093         */
094        public enum Protocol {
095                RAW, SSH
096        }
097
098        private final ConnectionHandler connectionHandler;
099        private final EventManager eventManager;
100        private final ExecutorService userThreadPool;
101        private final FileTransferHelper fileTransferHelper;
102        private final CommandQueue globalQueue;
103        private final TS3Config config;
104
105        private final AtomicBoolean connected = new AtomicBoolean(false);
106
107        private Connection connection;
108
109        /**
110         * Creates a TS3Query that connects to a TS3 server at
111         * {@code localhost:10011} using default settings.
112         */
113        public TS3Query() {
114                this(new TS3Config());
115        }
116
117        /**
118         * Creates a customized TS3Query that connects to a server
119         * specified by {@code config}.
120         *
121         * @param config
122         *              configuration for this TS3Query
123         */
124        public TS3Query(TS3Config config) {
125                this.config = config.freeze();
126                this.eventManager = new EventManager(this);
127                this.userThreadPool = Executors.newCachedThreadPool();
128                this.fileTransferHelper = new FileTransferHelper(config.getHost());
129                this.connectionHandler = config.getReconnectStrategy().create(config.getConnectionHandler());
130                this.globalQueue = CommandQueue.newGlobalQueue(this, connectionHandler instanceof DisconnectingConnectionHandler);
131        }
132
133        // PUBLIC
134
135        /**
136         * Tries to establish a connection to the TeamSpeak3 server.
137         *
138         * @throws IllegalStateException
139         *              if this method was called from {@link ConnectionHandler#onConnect}
140         * @throws TS3ConnectionFailedException
141         *              if the query can't connect to the server or the {@link ConnectionHandler} throws an exception
142         */
143        public void connect() {
144                if (Thread.holdsLock(this)) {
145                        // Check that connect is not called from onConnect
146                        throw new IllegalStateException("Cannot call connect from onConnect handler");
147                }
148
149                doConnect();
150        }
151
152        private synchronized void doConnect() {
153                if (userThreadPool.isShutdown()) {
154                        throw new IllegalStateException("The query has already been shut down");
155                }
156
157                disconnect(); // If we're already connected
158
159                try {
160                        CommandQueue queue = CommandQueue.newConnectQueue(this);
161                        Connection con = new Connection(this, config, queue);
162
163                        try {
164                                TS3Api api = queue.getApi();
165                                if (config.getProtocol() == Protocol.RAW && config.hasLoginCredentials()) {
166                                        api.login(config.getUsername(), config.getPassword());
167                                }
168                                connectionHandler.onConnect(api);
169                        } catch (TS3QueryShutDownException e) {
170                                // Disconnected during onConnect, re-throw as a TS3ConnectionFailedException
171                                queue.failRemainingCommands();
172                                throw new TS3ConnectionFailedException(e);
173                        } catch (Exception e) {
174                                con.disconnect();
175                                queue.failRemainingCommands();
176                                throw new TS3ConnectionFailedException("ConnectionHandler threw exception in connect handler", e);
177                        }
178
179                        // Reject new commands and wait until the onConnect queue is empty
180                        queue.shutDown();
181
182                        connection = con;
183                        con.setCommandQueue(globalQueue);
184                        connected.set(true);
185                } catch (TS3ConnectionFailedException conFailed) {
186                        // If this is the first connection attempt, we won't run the handleDisconnect method,
187                        // so we need to call shutDown from this method instead.
188                        if (connection == null) shutDown();
189                        throw conFailed;
190                }
191        }
192
193        /**
194         * Disconnects the query and closes all open resources.
195         * <p>
196         * If the command queue still contains commands when this method is called,
197         * the query will first process these commands, causing this method to block.
198         * However, the query will reject any new commands as soon as this method is called.
199         * </p>
200         *
201         * @throws IllegalStateException
202         *              if this method was called from {@link ConnectionHandler#onConnect}
203         */
204        public void exit() {
205                if (Thread.holdsLock(this)) {
206                        // Check that exit is not called from onConnect
207                        throw new IllegalStateException("Cannot call exit from onConnect handler");
208                }
209
210                try {
211                        globalQueue.quit();
212                } finally {
213                        shutDown();
214                }
215        }
216
217        private synchronized void shutDown() {
218                if (userThreadPool.isShutdown()) return;
219
220                disconnect();
221                globalQueue.failRemainingCommands();
222                userThreadPool.shutdown();
223        }
224
225        private synchronized void disconnect() {
226                if (connection == null) return;
227
228                connection.disconnect();
229                connected.set(false);
230        }
231
232        /**
233         * Returns {@code true} if the query is likely connected,
234         * {@code false} if the query is disconnected or currently trying to reconnect.
235         * <p>
236         * Note that the only way to really determine whether the query is connected or not
237         * is to send a command and check whether it succeeds.
238         * Thus this method could return {@code true} almost a minute after the connection
239         * has been lost, when the last keep-alive command was sent.
240         * </p><p>
241         * Please do not use this method to write your own connection handler.
242         * Instead, use the built-in classes in the {@code api.reconnect} package.
243         * </p>
244         *
245         * @return whether the query is connected or not
246         *
247         * @see TS3Config#setReconnectStrategy(ReconnectStrategy)
248         * @see TS3Config#setConnectionHandler(ConnectionHandler)
249         */
250        public boolean isConnected() {
251                return connected.get();
252        }
253
254        /**
255         * Gets the API object that can be used to send commands to the TS3 server.
256         *
257         * @return a {@code TS3Api} object
258         */
259        public TS3Api getApi() {
260                return globalQueue.getApi();
261        }
262
263        /**
264         * Gets the asynchronous API object that can be used to send commands to the TS3 server
265         * in a non-blocking manner.
266         * <p>
267         * Please only use the asynchronous API if it is really necessary and if you understand
268         * the implications of having multiple threads interact with your program.
269         * </p>
270         *
271         * @return a {@code TS3ApiAsync} object
272         */
273        public TS3ApiAsync getAsyncApi() {
274                return globalQueue.getAsyncApi();
275        }
276
277        // INTERNAL
278
279        void submitUserTask(final String name, final Runnable task) {
280                userThreadPool.submit(() -> {
281                        try {
282                                task.run();
283                        } catch (Throwable throwable) {
284                                log.error(name + " threw an exception", throwable);
285                        }
286                });
287        }
288
289        EventManager getEventManager() {
290                return eventManager;
291        }
292
293        FileTransferHelper getFileTransferHelper() {
294                return fileTransferHelper;
295        }
296
297        void fireDisconnect() {
298                connected.set(false);
299
300                submitUserTask("ConnectionHandler disconnect task", this::handleDisconnect);
301        }
302
303        private void handleDisconnect() {
304                try {
305                        connectionHandler.onDisconnect(this);
306                } finally {
307                        synchronized (this) {
308                                if (!connected.get()) {
309                                        shutDown();
310                                }
311                        }
312                }
313        }
314}