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}