package com.cardshifter.server.model;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import com.cardshifter.api.*;
import com.cardshifter.core.Log4jAdapter;
import com.cardshifter.core.username.*;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import com.cardshifter.api.messages.Message;
import com.cardshifter.api.outgoing.UserStatusMessage;
import com.cardshifter.api.outgoing.UserStatusMessage.Status;
import com.cardshifter.core.game.ServerGame;
import com.cardshifter.core.messages.IncomingHandler;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
/**
* Handles different parts of the server operations, such as message handling, chat room, game creation, current games
* @author Simon Forsberg
*
*/
public class Server implements ClientServerInterface {
private static final Logger logger = LogManager.getLogger(Server.class);
private final AtomicInteger clientId = new AtomicInteger(0);
private final AtomicInteger roomCounter = new AtomicInteger(0);
private final AtomicInteger gameId = new AtomicInteger(0);
/**
* The IncomingHandler receives messages and passes them to the correct Handler
*/
private final HandlerManager handlerManager = new HandlerManager(this);
private final InviteManager inviteManager = new InviteManager(this);
private final Map<Integer, ClientIO> clients = new ConcurrentHashMap<>();
private final Map<Integer, ChatArea> chats = new ConcurrentHashMap<>();
private final Map<Integer, ServerGame> games = new ConcurrentHashMap<>();
private final Map<String, GameFactory> gameFactories = new ConcurrentHashMap<>();
private final Set<ConnectionHandler> handlers = Collections.synchronizedSet(new HashSet<>());
private final AtomicReference<ClientIO> playAny = new AtomicReference<>();
private final ScheduledExecutorService scheduler;
private final ChatArea mainChat;
public Server() {
this.scheduler = Executors.newScheduledThreadPool(2, new ThreadFactoryBuilder().setNameFormat("ai-thread-%d").build());
mainChat = this.newChatRoom("Main");
}
/**
*
* @return This is the master chat room where all clients connecting are automatically added
*/
ChatArea getMainChat() {
return mainChat;
}
/**
* Creates a ChatArea a given name and assigns an id
*
* @param name The name of the chat room to create
* @return The room that was created
*/
public ChatArea newChatRoom(String name) {
int id = roomCounter.incrementAndGet();
ChatArea room = new ChatArea(id, name);
chats.put(id, room);
return room;
}
/**
*
* @return A collection of the current clients
*/
public Map<Integer, ClientIO> getClients() {
return Collections.unmodifiableMap(clients);
}
/**
* Set the user name of a client or fail
*
* @param client The client
* @param userName The user name to set
* @throws UserNameAlreadyInUseException If name is already used by another client
*/
public void trySetClientName(ClientIO client, UserName userName) throws UserNameAlreadyInUseException {
String name = userName.asString();
synchronized (this) {
for (ClientIO other : clients.values()) {
if (other.getName().equals(name)) {
throw new UserNameAlreadyInUseException();
}
}
client.setName(name);
}
}
/**
*
* @return Returns the IncomingHandler for the Server
*/
public IncomingHandler getIncomingHandler() {
return handlerManager.getIncomingHandler();
}
/**
* Passes the message to the IncomingHandler which will parse and perform it
*
* @param client The client sending the message
* @param json The actual contents of the message
*/
@Override
public void handleMessage(ClientIO client, String json) {
handlerManager.handleMessage(client, json);
}
/**
* Puts the client in the clients collection
*
* @param client The client object that will be connecting
*/
public void newClient(ClientIO client) {
logger.info("New client: " + client);
clients.put(client.getId(), client);
}
/**
* Removes client from the clients collection and broadcasts the event
*
* @param client The client object that was disconnected
*/
@Override
public void onDisconnected(ClientIO client) {
logger.info("Client disconnected: " + client);
games.values().stream().filter(game -> game.hasPlayer(client))
.forEach(game -> game.disconnect(client));
clients.remove(client.getId());
getMainChat().remove(client);
broadcast(new UserStatusMessage(client.getId(), client.getName(), Status.OFFLINE));
}
/**
* Sends the message to each client in the clients collection
*
* @param data The message to broadcast
*/
void broadcast(Message data) {
clients.values().forEach(cl -> cl.sendToClient(data));
}
/**
* Puts the game factory into the gameFactories collection
*
* @param gameType Name of the game type
* @param factory The GameFactory object
*/
public void addGameFactory(String gameType, GameFactory factory) {
this.gameFactories.put(gameType, factory);
}
/**
*
* @return A collection of the current game factories
*/
public Map<String, GameFactory> getGameFactories() {
return Collections.unmodifiableMap(gameFactories);
}
/**
* Puts the created game into the games collection unless its factory is invalid
*
* @param parameter the name of the game factory to use
* @return A reference to the game object
*/
public ServerGame createGame(String parameter) {
GameFactory suppl = gameFactories.get(parameter);
if (suppl == null) {
throw new IllegalArgumentException("No such game factory: " + parameter);
}
ServerGame game = suppl.newGame(this, gameId.incrementAndGet());
this.games.put(game.getId(), game);
return game;
}
/**
*
* @return The available ChatAreas
*/
public Map<Integer, ChatArea> getChats() {
return new HashMap<>(chats);
}
/**
*
* @return a new hash map that contains the contents of games
*/
public Map<Integer, ServerGame> getGames() {
return new HashMap<>(games);
}
/**
*
* @return The server's invite manager
*/
public InviteManager getInviteManager() {
return inviteManager;
}
/**
* Adds the ConnectionHandler to the handlers set
*
* @param handler the ConnectionHandler to add
*/
public void addConnections(ConnectionHandler handler) {
handler.start();
this.handlers.add(handler);
}
/**
* This could be used for randomly pairing up clients
* @return Atomic reference that will reference a client that is looking to play if one exists
*/
public AtomicReference<ClientIO> getPlayAny() {
return playAny;
}
/**
*
* @return The scheduler object
*/
public ScheduledExecutorService getScheduler() {
return scheduler;
}
/**
* Closes all clients, shuts down all handlers, shuts down the scheduler
*/
public void stop() {
// Use a copy to avoid ConcurrentModificationException
new ArrayList<>(clients.values()).forEach(ClientIO::close);
for (ConnectionHandler handler : handlers) {
try {
handler.shutdown();
} catch (Exception e) {
logger.error("Error shutting down " + handler, e);
}
}
this.scheduler.shutdown();
}
/**
*
* @return The CommandHandler object
*/
public CommandHandler getCommandHandler() {
return handlerManager.getCommandHandler();
}
@Override
public void performIncoming(Message message, ClientIO client) {
getIncomingHandler().perform(message, client);
}
@Override
public int newClientId() {
return clientId.incrementAndGet();
}
@Override
public LogInterface getLogger() {
return new Log4jAdapter();
}
}