public class ChatterbotServiceException extends Exception {
    private static final long serialVersionUID = 1L;

    public ChatterbotServiceException(String message, Throwable cause) {
        super(message, cause);
    }
} public interface Action {

    /**
     * Ejecuta la acción y deja los resultados en el mapa suministrado.
     * 
     * @param keys
     * @param values
     */
    public void execute(List<String> keys, List<String> values);
} @Entity
public class Chat extends VersionablePersistentEntity {

    private static final long serialVersionUID = 1L;

    @Column(name = "chat_client")
    private String client;
    private String finalNode;

    @Column(name = "is_successful")
    private boolean successful;

    @Column(name = "chat_length")
    private Integer length;
    
    @OneToMany(mappedBy = "chat", cascade = CascadeType.ALL, fetch = FetchType.EAGER )
    private List<ChatEntry> chatEntries;

    @ManyToOne(cascade = CascadeType.ALL)
    private Server server;

    Chat() {
    }

    /**
     * Crea un nuevo chat, sin mensajes y en estado no satisfactorio.
     * 
     * @param client Nombre del usuario Jabber del cliente.
     * @param server Servidor que atiende la consulta.
     */
    public Chat(String client, Server server) {
        this.client = client;
        this.server = server;
        this.chatEntries = new ArrayList<ChatEntry>();
        this.finalNode = "";
        this.successful = false;
        this.length = 0;
//        this.server.addChat(this);
    }

    /**
     * Crea un nuevo chat, sin mensajes.
     * 
     * @param client Nombre del usuario Jabber del cliente.
     * @param server Servidor que atiende la consulta.
     * @param successful Estado satisfactorio de la conversacion.
     */
    public Chat(String client, Server server, boolean successful) {
        this.client = client;
        this.server = server;
        this.chatEntries = new ArrayList<ChatEntry>();
        this.finalNode = "";
        this.successful = successful;
        this.length = 0;
    }

    /**
     * @return Nombre de usuario Jabber del cliente.
     */
    public String getClient() {
        return this.client;
    }

    /**
     * @param client Nombre de usuario Jabber del cliente.
     * @return this
     */
    public Chat setClient(String client) {
        this.client = client;
        return this;
    }

    /**
     * @return Fecha y hora de comienzo del chat.
     */
    public Date getStartDate() {
        if (chatEntries.isEmpty()) {
            return null;
        } else {
            return chatEntries.iterator().next().getDate();
        }
    }

    /**
     * @return Fecha y hora de finalización del chat.
     */
    public Date getEndDate() {
        if (chatEntries.isEmpty()) {
            return null;
        } else {
            return chatEntries.get(chatEntries.size() - 1).getDate();
        }
    }

    /**
     * @return Lista de mensajes de la conversación.
     */
    public List<ChatEntry> getChatEntries() {
        return this.chatEntries;
    }

    /**
     * Agrega un nuevo mensaje a la conversación.
     * 
     * @param chatEntry Mensaje a agregar.
     * @return this
     */
    public Chat addChatEntry(ChatEntry chatEntry) {
        this.chatEntries.add(chatEntry);

        Date minDate = chatEntry.getDate(), maxDate = chatEntry.getDate();
        for (ChatEntry e : this.chatEntries) {
            if (e.getDate().before(minDate)) {
                minDate = e.getDate();
            }
            if (e.getDate().after(maxDate)) {
                maxDate = e.getDate();
            }
        }
        this.length = (int) ((maxDate.getTime() - minDate.getTime()) / (1000 * 60));

        return this;
    }

    /**
     * @return Si la conversación fue satisfactoria o no.
     */
    public boolean isSuccessful() {
        return this.successful;
    }

    /**
     * @param successful Si la conversación fue satisfactoria o no.
     * @return this
     */
    public Chat setSuccessful(boolean successful) {
        this.successful = successful;
        return this;
    }

    /**
     * @return Servidor que atendió al cliente.
     */
    public Server getServer() {
        return this.server;
    }

    public void setServer(Server server) {
        this.server = server;
    }
    
    /**
     * @return Nombre del nodo del estado final de la conversación.
     */
    public String getFinalNode() {
        return this.finalNode;
    }

    /**
     * @param finalNode Nombre del nodo del estado final de la conversación.
     */
    public void setFinalNode(String finalNode) {
        this.finalNode = finalNode;
    }

    /**
     * @return Duración del chat en minutos.
     */
    public Integer getLength() {
        return length;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = super.hashCode();
        result = prime * result + ((chatEntries == null) ? 0 : chatEntries.hashCode());
        result = prime * result + ((client == null) ? 0 : client.hashCode());
        result = prime * result + ((finalNode == null) ? 0 : finalNode.hashCode());
        result = prime * result + ((server == null) ? 0 : server.hashCode());
        result = prime * result + (successful ? 1231 : 1237);
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (!super.equals(obj))
            return false;
        if (getClass() != obj.getClass())
            return false;
        final Chat other = (Chat) obj;
        if (chatEntries == null) {
            if (other.chatEntries != null)
                return false;
        } else if (!chatEntries.equals(other.chatEntries))
            return false;
        if (client == null) {
            if (other.client != null)
                return false;
        } else if (!client.equals(other.client))
            return false;
        if (finalNode == null) {
            if (other.finalNode != null)
                return false;
        } else if (!finalNode.equals(other.finalNode))
            return false;
        if (server == null) {
            if (other.server != null)
                return false;
        } else if (!server.equals(other.server))
            return false;
        if (successful != other.successful)
            return false;
        return true;
    }

    @Override
    public String toString() {
        return client + " " + server + " " + chatEntries + " " + finalNode + " " + successful;
    }

} @Entity
public class ChatEntry extends VersionablePersistentEntity implements Serializable {

    static final long serialVersionUID = 1L;

    @Column(name="cdate")
    private Date date;
    private String message;
    private boolean userQuery;

    @ManyToOne
    private Chat chat;

    ChatEntry() {
    }

    /**
     * Crea un nuevo mensaje y lo agrega al chat que recibe por parámetro.
     * 
     * @param date Fecha y hora del mensaje.
     * @param message Contenido del mensaje.
     * @param userQuery True si es del usuario, false si es del chatterbot.
     */
    public ChatEntry(Chat chat, Date date, String message, boolean userQuery) {
        super();
        this.chat = chat;
        this.date = date;
        this.message = message;
        this.userQuery = userQuery;
        // this.chat.addChatEntry(this);
    }

    /**
     * @return Fecha y hora del mensaje.
     */
    public Date getDate() {
        return date;
    }

    /**
     * @return Texto del mensaje.
     */
    public String getMessage() {
        return message;
    }

    /**
     * @return Si es del usuario o del chatterbot.
     */
    public boolean isUserQuery() {
        return userQuery;
    }

    /**
     * @return Chat al que pertenece el mensaje.
     */
    public Chat getChat() {
        return this.chat;
    }

    @Override
    public String toString() {
        StringBuffer sb = new StringBuffer();
        sb.append("[LOG date: " + date);
        sb.append(" message: " + message + "]");
        return sb.toString();
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = super.hashCode();
        result = prime * result + ((date == null) ? 0 : date.hashCode());
        result = prime * result + ((message == null) ? 0 : message.hashCode());
        result = prime * result + (userQuery ? 1231 : 1237);
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (!super.equals(obj))
            return false;
        if (getClass() != obj.getClass())
            return false;
        ChatEntry other = (ChatEntry) obj;
        if (date == null) {
            if (other.date != null)
                return false;
        } else if (!date.equals(other.date))
            return false;
        if (message == null) {
            if (other.message != null)
                return false;
        } else if (!message.equals(other.message))
            return false;
        if (userQuery != other.userQuery)
            return false;
        return true;
    }
} @Entity
public class ErrorLog extends VersionablePersistentEntity {
    private static final long serialVersionUID = 0L;
    
    private static final int MAX_LENGTH_MESSAGE = 500; 
    private static final int MAX_LENGTH_STACK_TRACE = 1000; 

    @Column(length=MAX_LENGTH_MESSAGE)
    private String message;

    @Column(length=MAX_LENGTH_STACK_TRACE)
    private String stackTrace;
    private Date timestamp;

    ErrorLog() {
    }

    /**
     * Crea un nuevo error log dado un mensaje, un stack trace y un timestamp.
     * 
     * @param message Mensaje del error.
     * @param stackTrace Stack trace completo.
     * @param timestamp Fecha y hora del error.
     */
    public ErrorLog(String message, String stackTrace, Date timestamp) {
        super();

        if (message.length() > MAX_LENGTH_MESSAGE) {
            this.message = message.substring(0, MAX_LENGTH_MESSAGE - 1);
        } else {
        	this.message = message;
        }
        
        if (stackTrace.length() > MAX_LENGTH_STACK_TRACE) {
            this.stackTrace = stackTrace.substring(0, MAX_LENGTH_STACK_TRACE- 1);
        } else {
        	this.stackTrace = stackTrace;
        }

        this.timestamp = timestamp;
    }

    /**
     * Crea un nuevo error log dado un mensaje y un stack trace utilizando la fecha de creación del objeto.
     * 
     * @param message Mensaje del error.
     * @param stackTrace Stack trace completo.
     */
    public ErrorLog(String message, String stackTrace) {
        super();
        
        if (message.length() > MAX_LENGTH_MESSAGE) {
            this.message = message.substring(0, MAX_LENGTH_MESSAGE - 1);
        } else {
        	this.message = message;
        }
        
        if (stackTrace.length() > MAX_LENGTH_STACK_TRACE) {
            this.stackTrace = stackTrace.substring(0, MAX_LENGTH_STACK_TRACE- 1);
        } else {
        	this.stackTrace = stackTrace;
        }

        this.timestamp = new Date();
    }

    /**
     * @return Mensaje del error.
     */
    public String getMessage() {
        return message;
    }

    /**
     * @return Stack trace del error.
     */
    public String getStackTrace() {
        return stackTrace;
    }

    /**
     * @return Fecha y hora del error.
     */
    public Date getTimestamp() {
        return timestamp;
    }

    /**
     * @param message Mensaje de error.
     */
    public void setMessage(String message) {
        this.message = message;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = super.hashCode();
        result = prime * result + ((message == null) ? 0 : message.hashCode());
        result = prime * result + ((stackTrace == null) ? 0 : stackTrace.hashCode());
        result = prime * result + ((timestamp == null) ? 0 : timestamp.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (!super.equals(obj))
            return false;
        if (getClass() != obj.getClass())
            return false;
        ErrorLog other = (ErrorLog) obj;
        if (message == null) {
            if (other.message != null)
                return false;
        } else if (!message.equals(other.message))
            return false;
        if (stackTrace == null) {
            if (other.stackTrace != null)
                return false;
        } else if (!stackTrace.equals(other.stackTrace))
            return false;
        if (timestamp == null) {
            if (other.timestamp != null)
                return false;
        } else if (!timestamp.equals(other.timestamp))
            return false;
        return true;
    }
    
    
} @Entity
public class GlobalNode extends AbstractNode {

    private static final long serialVersionUID = 1L;

    GlobalNode() {
    }

    /**
     * Crea un <code>GlobalNode</code> completando toda su información.
     * 
     * @param action Acción a realizar cuando el chatterbot está en el nodo.
     * @param keywords Palabras clave que hacen que el chatterbot llegue a este nodo.
     * @param description Descripción del nodo.
     * @param answer Respuesta que el chatterbot emite cuando estando el el nodo.
     */
    public GlobalNode(Action action, String keywords, String description, String answer) {
        super(action, keywords, description, answer);
    }

    
}
 @Entity
@Table(uniqueConstraints = @UniqueConstraint(columnNames = { "hostname", "port" }))
public class Server extends VersionablePersistentEntity implements Serializable {

    static final long serialVersionUID = 1L;

    private String name;
    private String hostname;
    private int port;
    private int maxChatterbots;
    private float maxLoad;
    private boolean enabled;

    @OneToMany(mappedBy = "server", cascade = CascadeType.ALL)
    private Collection<Chat> chats;

    Server() {
    }

    /**
     * Crea un <code>Server</code> completando toda su información menos el log de chats ocurridos en el servidor.
     * 
     * @param name Nombre del servidor.
     * @param hostname Hostname del servidor.
     * @param port Puerto en que escucha el servidor.
     * @param maxChatterbots Cantidad máxima de chatterbox del servidor.
     * @param maxLoad Carga máxima del servidor.
     * @param enabled Estado del servidor (habilitado o deshabilitado).
     */
    public Server(String name, String hostname, int port, int maxChatterbots, float maxLoad, boolean enabled) {
        super();
        this.name = name;
        this.hostname = hostname;
        this.maxChatterbots = maxChatterbots;
        this.maxLoad = maxLoad;
        this.port = port;
        this.enabled = enabled;
        this.chats = new ArrayList<Chat>();
    }

    /**
     * @return Nombre del servidor.
     */
    public String getName() {
        return this.name;
    }

    /**
     * @param name Nombre del servidor.
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * @return Hosname del servidor.
     */
    public String getHostname() {
        return hostname;
    }

    /**
     * @param hostname del servidor.
     */
    public void setHostname(String hostname) {
        this.hostname = hostname;
    }

    /**
     * @return Puerto en que escucha el servidor.
     */
    public int getPort() {
        return port;
    }

    /**
     * @param port Puerto en que escucha el servidor.
     */
    public void setPort(int port) {
        this.port = port;
    }

    /**
     * @return Cantidad máxima de chatterbot que puede albergar el servidor.
     */
    public int getMaxChatterbots() {
        return maxChatterbots;
    }

    /**
     * 
     * @param maxChatterbots Cantidad máxima de chatterbot que puede albergar el servidor.
     */
    public void setMaxChatterbots(int maxChatterbots) {
        this.maxChatterbots = maxChatterbots;
    }

    /**
     * @return Máxima carga que se desea que tenga el servidor.
     */
    public float getMaxLoad() {
        return maxLoad;
    }

    /**
     * @param maxLoad Máxima carga que se desea que tenga el servidor.
     */
    public void setMaxLoad(float maxLoad) {
        this.maxLoad = maxLoad;
    }

    /**
     * @return Estado del servidor (habilitado o deshabilitado).
     */
    public boolean isEnabled() {
        return this.enabled;
    }

    /**
     * @param enabled Estado del servidor (habilitado o deshabilitado).
     */
    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    /**
     * @return <code>Collection<Chat></code> con el log de chats que ocurrieron en este servidor.
     */
    public Collection<Chat> getChats() {
        return chats;
    }

    /**
     * Agrega un <code>Chat</code> al log de chats de este servidor.
     * 
     * @param chat <code>Chat</code> que se desea agregar al log.
     * @return this.
     */
    public Server addChat(Chat chat) {
        this.chats.add(chat);
        return this;
    }

    @Override
    public String toString() {
        return "Hostname: " + hostname + "\n" + "Port: " + port + "\n" + "MaxChatterbots: " + maxChatterbots + "\n"
                + "MaxLoad: " + maxLoad;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = super.hashCode();
        result = prime * result + ((hostname == null) ? 0 : hostname.hashCode());
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        result = prime * result + port;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (!super.equals(obj))
            return false;
        if (getClass() != obj.getClass())
            return false;
        final Server other = (Server) obj;
        if (hostname == null) {
            if (other.hostname != null)
                return false;
        } else if (!hostname.equals(other.hostname))
            return false;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        if (port != other.port)
            return false;
        return true;
    }

} @Entity
public abstract class TreeNode extends AbstractNode {

    private static final long serialVersionUID = 1L;

    @ManyToOne()
    protected InternalNode parent;

    @ManyToOne()
    private TreeNode errorTransition;

    /* Quienes me tienen en sus transiciones de error */
    @OneToMany(mappedBy = "errorTransition", cascade = CascadeType.MERGE, fetch = FetchType.EAGER)
    private Collection<TreeNode> references = new ArrayList<TreeNode>();

    TreeNode() {
    }

    /**
     * Construye un <code>TreeNode</code> completando únicamente la información del <code>Node</code> del que hereda.
     * 
     * @param action Acción a realizar cuando el chatterbot está en el nodo.
     * @param keywords Palabras clave que hacen que el chatterbot llegue a este nodo.
     * @param description Descripción del nodo.
     * @param answer Respuesta que el chatterbot emite cuando estando el el nodo.
     * @param errorTransition Transición de error a la que se pasa si ocurre un error durante la ejecución de una
     *            acción.
     */
    public TreeNode(Action action, String keywords, String description, String answer, TreeNode errorTransition) {
        super(action, keywords, description, answer);
        setErrorTransition(errorTransition);
    }

    /**
     * @see ar.edu.itba.tpf.chatterbot.model.GlobalNode#hasTransitions()
     */
    public abstract boolean hasTransitions();

    /**
     * @return Nodo padre del nodo.
     */
    public InternalNode getParent() {
        return parent;
    }

    /**
     * @return Transición de error.
     */
    public TreeNode getErrorTransition() {
        return errorTransition;
    }

    /**
     * @param errorTransition Transición de error.
     */
    public void setErrorTransition(TreeNode errorTransition) {
        if ((this.errorTransition == null && errorTransition == null)
                || (this.errorTransition != null && errorTransition != null && this.errorTransition
                        .equals(errorTransition))) {
            return;
        }
        if (this.errorTransition != null) {
            // System.out.println("setErrorTransition: sacandome de mi padre anterior");
            this.errorTransition.getReferences().remove(this);
        }

        this.errorTransition = errorTransition;
        if (this.errorTransition != null) {
            // System.out.println("setErrorTransition: poniendome en mi nuevo padre");
            this.errorTransition.getReferences().add(this);
        }
    }

    public Collection<TreeNode> getReferences() {
        return references;
    }

} public interface ChatterbotService {

    /**
     * Almacena un nodo global.
     * 
     * @param node Nodo que se desea almacenar.
     */
    public void persistGlobalNode(GlobalNode globalNode);

    /**
     * Almacena un nodo del árbol de consultas.
     * 
     * @param node Nodo que se desea almacenar.
     */
    public void persistTreeNode(TreeNode treeNode);

    /**
     * Almacena una acción.
     * 
     * @param baseAction Acción que se desea almacenar.
     */
    public void persistBaseAction(BaseAction baseAction);

    /**
     * Elimina un nodo global.
     * 
     * @param node Nodo que se desea almacenar.
     */
    public void removeGlobalNode(GlobalNode node);

    /**
     * Elimina una acción
     * 
     * @param action Acción que se desea almacenar.
     */
    public void removeAction(BaseAction action);

    /**
     * Obtiene una colección con todos los nodos globales.
     * 
     * @return Colección con todos los nodos globales.
     */
    public Collection<GlobalNode> getGlobalNodes();

    /**
     * Obtiene el nodo raíz del árbol de decisión del chatterbot. Dicho nodo es el único que no tiene padre.
     * 
     * @return Nodo raíz del árbol de decisión del chatterbot.
     */
    public TreeNode getRootNode();

    /**
     * Obtiene el proximo nodo (del árbol de decisión o bien global), al que debe pasar el chatterbot.
     * 
     * @param currentNode Nodo en el que se encuentra el chatterbot.
     * @param message Mensaje del cliente.
     * @return Nodo al que debe pasar el chatterbot.
     */
    public TreeNode getTransition(InternalNode currentNode, String message) throws ChatterbotServiceException;

    /**
     * Busca si el mensaje matchea con un nodo global, y de ser así lo retorna.
     * 
     * @param message Mensaje del cliente.
     * @return Nodo global que matchea, o null si no hay ninguno.
     */
    public GlobalNode getGlobalTransition(String message) throws ChatterbotServiceException;

    /**
     * Obtiene el nodo que tiene la descripción indicada.
     * 
     * @param description Descripción que tiene el nodo buscado.
     * @return Nodo con la descripción indicada.
     */
    public TreeNode getTreeNodeByDescription(String description);

    /**
     * Obtiene la acción que tiene la descripción indicada.
     * 
     * @param description Descripción que tiene la acción buscada.
     * @return Nodo Acción con la descripción indicada.
     */
    public BaseAction getActionByDescription(String description);

    /**
     * @return Conjunto con todas las acciones.
     */
    public Collection<BaseAction> getAllActions();

    public void persistWebserviceAction(WebServiceAction action);

    public void removeWebServiceAction(WebServiceAction action);

    public int getErrorTransitionReferencesCount(TreeNode t);

    /**
     * Le agrega al nodo padre el nodo hoja hijo recibido
     * 
     * @param father
     * @param child
     * @throws ConstraintViolationException En caso de que ya exista un nodo con el mismo nombre que el que se desea
     *                 agregar.
     */
    public void addLeafNode(TreeNode father, LeafNode child) throws ConstraintViolationException;
    
    public void removeTreeNode(TreeNode node) throws ChatterbotServiceException;
} public interface LoggingService {

    /**
     * Almacena una conversacion.
     * 
     * @param chat Conversación que se desea almacenar.
     */
    public void persistChat(Chat chat);

    /**
     * Obtiene un histograma con la cantidad de chats por dia.
     * 
     * @param intervalCriteria Intervalo en el que se realiza la búsqueda.
     * @return Mapa con la cantidad de chats realizados en cada fecha.
     */
    public Map<Date, Long> getChatsPerDay(IntervalCriteria intervalCriteria);

    /**
     * Obtiene el porcentaje de chats que fueron atendidos en forma satisfactoria.
     * 
     * @param intervalCriteria Intervalo de fechas en la que se realiza la búsqueda.
     * @return Un arreglo de long indicando, el número de chats successful y el número total de chats.
     */
    public Long[] getSuccessfulChatsRatio(IntervalCriteria intervalCriteria);

    /**
     * Obtiene un histograma con la cantidad de chats de diferentes duraciones.
     * 
     * @param intervalCriteria Intervalo de fechas en la que se realiza la búsqueda.
     * @return Mapa con la cantidad de chats que duraron diferente cantidad de minutos.
     */
    public Map<String, Long> getAverageChatTime(IntervalCriteria intervalCriteria);

    /**
     * Obtiene todos los chats que satisfacen un criterio de búsqueda.
     * 
     * @param loggingCriteria Criterio de búsqueda que contiene palabras clave.
     * @return Colección con todos los chats cuyo log tiene uno o más de las palabras clave que se indican en el
     *         criterio de búsqueda.
     */
    public Collection<Chat> getChats(LoggingCriteria loggingCriteria);

    /**
     * Almacena un error ocurrido en la aplicación.
     * 
     * @param errorLog Información del error ocurrido.
     */
    public void persistErrorLog(ErrorLog errorLog);

    /**
     * Elimina un ErrorLog.
     * 
     * @param server ErrorLog que se desea eliminar.
     */
    public void removeErrorLog(ErrorLog errorLog);

    /**
     * Obtiene todos los errores ocurridos en un determinado intervalo de tiempo.
     * 
     * @param intervalCriteria Intervalo de tiempo en el cual buscar errores.
     * @return Colección de errores ocurridos en dicho intervalo.
     */
    public Collection<ErrorLog> getErrorLogs(IntervalCriteria intervalCriteria);

    /**
     * Obtiene los nodos hoja del árbol de consultas y la cantidad de veces que fueron visitados en el intervalo dado.
     * 
     * @param intervalCriteria Intervalo en el cual hacer la consulta.
     * @return Listado de nodos con la cantidad de veces que fueron visitados.
     */
    public Map<String, Long> getMostVisitedNodes(IntervalCriteria intervalCriteria);
} public interface ServerService {

    /**
     * Almacena los datos de un servidor.
     * 
     * @param server Servidor que se desea almacenar.
     */
    public void persistServer(Server server);

    /**
     * Elimina un servidor.
     * 
     * @param server Servidor que se desea eliminar.
     */
    public void removeServer(Server server);

    /**
     * Obtiene la lista de servidores.
     * 
     * @return Colección de servidores.
     */
    public Collection<Server> getServers();

    /**
     * @return Cantidad de chatterbots activos en cada servidor.
     */
    public Map<Server, Integer> getActiveChatterbots() throws ServerServiceException;

    /**
     * @param serverCriteria Crierio de búsqueda.
     * @return Cantidad de chats simultáneos en cada servidor.
     */
    public Map<Date, Long> getServerLoad(ServerCriteria serverCriteria);

    /**
     * @param serverName Nombre del servidor buscado.
     * @return Servidor buscado, o NULL si no existe.
     */
    public Server getServerByName(String serverName);
    
    /**
     * Envía un request al dispatcher para que vuelva a cargar los servidores de la base de datos.
     */
    public void synchronizeServers() throws ServerServiceException;
}
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.Session;
import javax.jms.TextMessage;
import javax.xml.ws.WebServiceException;
import org.apache.log4j.Logger;
import org.springframework.jms.JmsException;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessageCreator;
import ar.edu.itba.tpf.chatterbot.exception.ChatterbotServiceException;
import ar.edu.itba.tpf.chatterbot.model.Action;
import ar.edu.itba.tpf.chatterbot.model.Chat;
import ar.edu.itba.tpf.chatterbot.model.ChatEntry;
import ar.edu.itba.tpf.chatterbot.model.ErrorLog;
import ar.edu.itba.tpf.chatterbot.model.GlobalNode;
import ar.edu.itba.tpf.chatterbot.model.InternalNode;
import ar.edu.itba.tpf.chatterbot.model.Server;
import ar.edu.itba.tpf.chatterbot.model.TreeNode;
import ar.edu.itba.tpf.chatterbot.service.ChatterbotService;
import ar.edu.itba.tpf.chatterbot.service.LoggingService;
import ar.edu.itba.tpf.chatterbot.service.ServerService;
            logger.error("Error al enviar respuesta al dispatcher. Usuario: " + user);
        }

    }


    /**
     * Destruye el contexto actual del chatterbot y lo deja listo para atender un nuevo cliente.
     */
    public void destroyChatterbot() {
        logger.debug("Finalizando conversación con el usuario " + currentUser);

        if (chat.getChatEntries().size() > 0) {
            server = serverService.getServerByName(server.getName());
            chat.setServer(server);
            chat.getServer().addChat(chat);
            serverService.persistServer(server);
        }
        this.currentUser = "";
        this.chat = null;
        this.active = false;
    }

    /**
     * Procesa un mensaje del cliente y genera la respuesta del chatterbot. El chatterbot puede modificar su estado
     * (nodo y variables de sesión).
     * 
     * @param message Mensaje enviado por el cliente.
     * @return Respuesta del chatterbot.
     */
    private String processMessage(String message) throws ChatterbotServiceException {

        lastClientMessage = message;
        String answert = null;
        TreeNode oldNode = currentNode;

        if (currentNode == null) {
            throw new ChatterbotServiceException("Error interno del sistema. Por favor contacte al administrador.",
                    null);
        }
        /*
         * Obtener el próximo nodo del árbol.
         */
        logger.debug("Procesando nodos hijos del nodo actual");
        currentNode = chatterbotService.getTransition((InternalNode) currentNode, message);

        
        if (currentNode == null) {
            currentNode = oldNode;

            /*
             * Determinar si existe una transición hacia un nodo global .
             */
            logger.debug("Buscando en la lista de nodos globales.");
            GlobalNode globalNode = chatterbotService.getGlobalTransition(message);
            if (globalNode == null) {
                /*
                 * Mostrar el menu de opciones disponibles.
                 */
                logger.debug("No hay coincidencia con nodos globales.");
                answert = "No entiendo su consulta. Posibles opciones:";
                for (TreeNode t : ((InternalNode) oldNode).getChildren()) {
                    answert += "\n" + t.getDescription();
                }
            } else {
                logger.debug("Coincidencia con el nodo global " + globalNode.getDescription());
                if (!executeAction(globalNode.getAction())) {
                    answert = getSessionVariableValue(sessionVariablesKeys, sessionVariablesValues, ERROR_SESSION_VARNAME);
                    if (answert == null) {
                        answert = "Error interno del sistema al ejecutar la acción asociada.";
                    }
                } else {
                    answert = generateAnswert(globalNode.getAnswer());
                }
            }

        } else {
            if (!executeAction(currentNode.getAction())) {
                answert = getSessionVariableValue(sessionVariablesKeys, sessionVariablesValues, ERROR_SESSION_VARNAME);
                if (answert == null) {
                    answert = "Error interno del sistema al ejecutar la acción asociada.";
                }
            } else {
                answert = generateAnswert(currentNode.getAnswer());
            }
        }
        cleanErrorSessionVariable();
        
        /*
         * Guardar la pregunta y respuesta en la conversación.
         */
        chat.addChatEntry(new ChatEntry(chat, new Date(), message, true));
        chat.addChatEntry(new ChatEntry(chat, new Date(), answert, false));

        /*
         * Si se llega a un nodo hoja, termina la conversación y se debe guardar el log de la consulta.
         */
        if (!currentNode.hasTransitions()) {
            chat.setSuccessful(true);

            server = serverService.getServerByName(server.getName());
            chat.setServer(server);
            chat.getServer().addChat(chat);
            chat.setFinalNode(currentNode.getDescription());
            serverService.persistServer(server);

            chat = new Chat(currentUser, server);
            currentNode = chatterbotService.getRootNode();
            answert += "\n\n" + currentNode.getAnswer();
        }

        return answert;
    }

    /**
     * Ejecuta una determinada acción, y modifica el estado del chatterbot (las variables de sesión) en función de la
     * acción ejecutada.
     * 
     * @param action Acción a ser ejecutada.
     */
    private boolean executeAction(Action action) {