Compare commits

..

5 Commits

Author SHA1 Message Date
SeekDaSky 317c4164b3 Documentation du code 2017-12-08 06:06:46 +01:00
SeekDaSky b944b060c1 allow connection from outside of the interop channels 2017-12-08 03:26:46 +01:00
SeekDaSky ecaed684fc refactoring des chanels : classe parente abstraite 2017-12-08 02:38:07 +01:00
SeekDaSky c0d3d1d1a5 add: Interop pour le channel Event 2017-12-08 01:54:01 +01:00
SeekDaSky e739fb25d6 fix: Interop operation pour le canal emergency 2017-12-08 00:39:17 +01:00
12 changed files with 714 additions and 276 deletions

View File

@ -7,11 +7,18 @@ import seekdasky.kWebSocket.Listeners.AsynchronousListener
import seekdasky.kWebSocket.Message.*; import seekdasky.kWebSocket.Message.*;
import seekdasky.kWebSocket.Server import seekdasky.kWebSocket.Server
/**
* Classe s'occupant de la partie chat simple du site, ancienne version que l'on a pas eu le temps de refactorer, pour un meilleur exemple regardez les classe contenues dans le package Channels
*/
class Channel : AsynchronousListener { class Channel : AsynchronousListener {
//nom du channel, utilisé pour définir l'URL d'écoute
val channelName : String; val channelName : String;
//list des clients
var clients : MutableList<Client>; var clients : MutableList<Client>;
//liste des messages du chat
val messages : Stack<Pair<Client, String>>; val messages : Stack<Pair<Client, String>>;
//instance du serveur
val serv : Server; val serv : Server;
constructor(serv : Server , chanName : String){ constructor(serv : Server , chanName : String){
@ -21,43 +28,56 @@ class Channel : AsynchronousListener {
this.serv = serv; this.serv = serv;
} }
/**
* comme toute les autres méthodes filter
*/
override fun filter(c: Client): Boolean { override fun filter(c: Client): Boolean {
return this.channelName == c.URL; return this.channelName == c.URL;
} }
/**
* méthode executée quand le client ferme son socket
*/
override fun processClosed(e: Event) { override fun processClosed(e: Event) {
this.clients.remove(e.client); this.clients.remove(e.client);
ConnectChannel.notifyDisconnect(e.client); ConnectChannel.notifyDisconnect(e.client);
System.out.println("A client disconnected ("+this.clients.count()+" clients connected)"); System.out.println("A client disconnected ("+this.clients.count()+" clients connected)");
} }
/**
* les clients sont ajouté a la liste lors de leur première connection mais ne sont pas loggués
*/
override fun processConnection(c: Client) { override fun processConnection(c: Client) {
this.clients.add(c); this.clients.add(c);
c.data["IsLogged"] = false; c.data["IsLogged"] = false;
} }
/**
* méthode gérant la connexion et l'envoi de message
*/
override fun processEvent(e: Event) { override fun processEvent(e: Event) {
try{ try{
val json = JSONObject(e.message.getString()) val json = JSONObject(e.message.getString())
if(json.has("close")){
return
}
if(e.client.data["IsLogged"] != true){ if(e.client.data["IsLogged"] != true){
//json format : {username:xxx} //json format : {username:xxx}
if(json.has("name") && json.getString("name") != null){ if(json.has("name") && json.getString("name") != null){
e.client.data["Username"] = json.getString("name"); e.client.data["Username"] = json.getString("name");
//on demande si le client est bien connecté
if(ConnectChannel.isConnected(e.client)){ if(ConnectChannel.isConnected(e.client)){
System.out.println("A client connected ("+this.clients.count()+" clients connected)"); System.out.println("A client connected ("+this.clients.count()+" clients connected)");
//on loggue le client et on lui notifie le succès
e.client.data["IsLogged"] = true; e.client.data["IsLogged"] = true;
val jsonLogin = JSONObject(); val jsonLogin = JSONObject();
jsonLogin.put("error",false); jsonLogin.put("error",false);
e.client.send(buildTextMessage(jsonLogin.toString())); e.client.send(buildTextMessage(jsonLogin.toString()));
//on lui envoie la liste des messages
val array = JSONArray(); val array = JSONArray();
//gare a la concurrence
synchronized(this.messages,{ synchronized(this.messages,{
this.messages.toList().forEach { this.messages.toList().forEach {
array.put(JSONArray(listOf(it.first.data["Username"],it.second))) array.put(JSONArray(listOf(it.first.data["Username"],it.second)))
@ -87,6 +107,7 @@ class Channel : AsynchronousListener {
return; return;
}else if(json.has("message")){ }else if(json.has("message")){
//gare a la concurrence
synchronized(this.messages,{ synchronized(this.messages,{
this.messages.push(Pair(e.client,json.getString("message"))) this.messages.push(Pair(e.client,json.getString("message")))
}); });
@ -98,7 +119,7 @@ class Channel : AsynchronousListener {
jsonMessage.put("error",false); jsonMessage.put("error",false);
jsonMessage.put("msg",array); jsonMessage.put("msg",array);
for(c in this.clients){ for(c in this.clients){
if(c != e.client && c.data["IsLogged"] == true){ if(c.data["IsLogged"] == true){
c.send(buildTextMessage(jsonMessage.toString())); c.send(buildTextMessage(jsonMessage.toString()));
} }
} }

View File

@ -1,43 +0,0 @@
import seekdasky.kWebSocket.Client
import seekdasky.kWebSocket.Event
import seekdasky.kWebSocket.Listeners.AsynchronousListener
import seekdasky.kWebSocket.Server
class ChannelDispatcher : AsynchronousListener{
val serv : Server;
private var channels : MutableList<Channel>;
constructor(serv : Server){
this.serv = serv;
this.channels = mutableListOf();
}
override fun filter(c: Client): Boolean {
for(chan in this.channels){
if(chan.channelName == c.URL){
return false;
}
}
return true;
}
override fun processClosed(e: Event) {
//no client should arrive here
}
override fun processConnection(c: Client) {
System.out.println("new channel created: "+c.URL);
//if a client is handled here it means we have no channel at this address, let's create it
var newChan = Channel(this.serv,c.URL);
this.serv.addListener(newChan);
//we transfer the client to the new channel
newChan.processConnection(c);
this.channels.add(newChan);
}
override fun processEvent(e: Event) {
//no client should arrive here
}
}

View File

@ -0,0 +1,359 @@
package Channels
import Collections.Message
import Collections.Stack
import org.json.JSONArray
import org.json.JSONObject
import seekdasky.kWebSocket.Client
import seekdasky.kWebSocket.Event
import seekdasky.kWebSocket.Listeners.AsynchronousListener
import seekdasky.kWebSocket.Message.buildTextMessage
import seekdasky.kWebSocket.Server
/**
* La classe abstraite AbstractChannel fournis toutes les méthodes nécéssaire a la gestion du channel (authentification, méthode de gestion des messages
* utilisés par les classes Interop, dispatch des messages a tout les clients, la seule partie abstraite est le nom du channel (propriété ChanName)
* qui sert a définir l'URL d'écoute.
*/
abstract class AbstractChannel : AsynchronousListener {
//liste de tout les clients du channel
var clients : MutableList<Client>;
//map ayant en clé le nom du channel et en valeur la stack de message, la stack est composé de paire nom d'utilisateur/message
val messages : MutableMap<String, Stack<Pair<String, Message>>>;
//instance du serveur pour dispatcher les évenements
val serv : Server;
//propriété abstraite servant a définir l'addresse d'écoute
abstract val chanName : String;
/**
* Constructeur de la classe
*
* @param serv Server instance du serveur
*/
constructor(serv : Server){
this.clients = mutableListOf();
this.messages = mutableMapOf();
this.serv = serv;
}
/**
* Methode permettant de filtrer les évenements passés a la méthode ProcessEvent()
*
* @param c Client client a filtrer
*/
override fun filter(c: Client): Boolean{
//on compare l'addresse liée au client a l'adresse d'écoute du channel
val regex = "\\/$chanName\\/(.*)".toRegex()
val result = regex.findAll(c.URL);
if(result.count() == 1){
return true;
}
return false;
}
/**
* Lorsqu'un client quitte le channel, on notifie la classe d'authentification et on retire le client de notre liste
*
* @param e Event évenement lancé par le serveur
*/
override fun processClosed(e: Event) {
this.clients.remove(e.client);
ConnectChannel.notifyDisconnect(e.client);
System.out.println("A client disconnected ("+this.clients.count()+" clients connected)");
}
/**
* Méthode executée lors de la première connexion du client
*/
override fun processConnection(c: Client) {
//on rajoute le client a notre liste
this.clients.add(c);
val regex = "\\/$chanName\\/(.*)".toRegex();
val result = regex.findAll(c.URL).elementAt(0).groups[1]?.value ?: "";
//si le sous-channel auquel le client esaie d'accéder n'existe pas, on le créé
if(this.messages[result] == null){
this.messages[result] = Stack( 50);
}
//on rajoute au client une donnée permettant de savoir a quel sous-channel il est lié
if(c.data["channel"] == null){
c.data["channel"] = result;
}
//le client est conecté mais pas logué, il devra envoyer un message spécial pour l'être
c.data["IsLogged"] = false;
}
/**
* Méthode appelée par les classes Interop afin de dispatcher les mesages envoyé par l'API REST du site
*
* @param channel String nom du sous-channel auquel il faut dispatch, un nom vide signifie un broadcast a tout les sous channels
* @param msg Messag message a dispatcher
*/
fun manuallyDispatchMessage(channel : String, msg : Message){
//on récupère la stach du sous-channel
val channelStack = this.messages[channel];
//si il existe, on dispatch
if(channelStack != null){
//gare aux modifications concurrentes
synchronized(channelStack, {
channelStack.push(Pair(msg.user,msg));
})
//dispatch
this.dispatchMessage(channel,msg,"add");
//sinon si la chaine est vide on broadcast
}else if (channel == ""){
//broadcast
for(c in this.messages.values){
synchronized(c,{
c.push(Pair(msg.user,msg));
})
}
//dispatch
this.dispatchMessage(channel,msg,"add");
}
}
/**
* Méthode principale du listener, elle prend en charge tout les messages de tout les clients
*/
override fun processEvent(e: Event) {
try{
val json = JSONObject(e.message.getString())
//si l'utilisateur n'est pas log, on le check
if(e.client.data["IsLogged"] != true){
//json format : {username:xxx}
if(json.has("name") && json.getString("name") != null){
//on set le nom d'utilisateur
e.client.data["Username"] = json.getString("name");
//On vérifie qu'il est bien connecté a l'aide de la classe ConnectChannel
if(ConnectChannel.isConnected(e.client)){
//log
System.out.println("Emergency connection ("+this.clients.count()+" clients connected)");
//on retourne une réponse de succès au client
e.client.data["IsLogged"] = true;
val jsonLogin = JSONObject();
jsonLogin.put("error",false);
e.client.send(buildTextMessage(jsonLogin.toString()));
//on envoie au client la liste des messages récent
val array = JSONObject();
val chanMessages = this.messages[e.client.data["channel"]];
if(chanMessages != null){
//gare a la concurrence
synchronized(chanMessages,{
chanMessages.toList().forEach {
array.put(it.second.id,it.second.toJSON())
}
})
}
//on construit le JSON
val jsonMessage = JSONObject();
jsonMessage.put("error",false);
jsonMessage.put("add",array);
//on envoie la sauce
e.client.send(buildTextMessage(jsonMessage.toString()));
}else{
val jsonError = JSONObject();
jsonError.put("error","Invalid credentials");
e.client.send(buildTextMessage(jsonError.toString()));
}
}else{
//Tentative d'accès au chat sans loggin
val jsonError = JSONObject();
jsonError.put("error","You must send your credential before sending anything else");
e.client.send(buildTextMessage(jsonError.toString()));
}
return;
//si le client envoie un message sur le canal websocket (pas utilisé sur le site car tout passe par l'API REST et les Interops mais utilisable en serveur standalone
}else if(json.has("message")){
//on récupère le channel
val chanMessages = this.messages[e.client.data["channel"]];
val message = Message();
//toujours vrai mais il est important de faire la vérification pour l'analyseur de code kotlin
if(chanMessages != null){
//on peuple l'objet message
message.user = e.client.data["Username"].toString();
message.message = json.getString("message");
//ligne pourrie, on ne peut pas donner d'id vu que c'est le serveur de l'API qui le fait, par défaut on utilise le timestamp (qui plus est un id est toujours une string
message.id = System.currentTimeMillis().toString();
//on check les coordonnés GPS
var lat : Float;
var lng : Float;
//étant doné que javascript fait un peu ce qu'il veut avec les nombres on peut recevoir des int ou des double, il faut tout cast en float
try {
lat = (json.getJSONArray("location")[0] as Double).toFloat();
}catch(e : Exception){
lat = (json.getJSONArray("location")[0] as Integer).toFloat();
}
try {
lng = (json.getJSONArray("location")[1] as Double).toFloat();
}catch(e : Exception){
lng = (json.getJSONArray("location")[1] as Integer).toFloat();
}
message.location = mutableListOf(lat,lng);
//utilisé que par le channel Event si tout va bien
if(json.has("type")){
message.type = json.getInt("type");
}
//concurrence encore
synchronized(chanMessages,{
chanMessages.push(Pair(e.client.data["Username"].toString(),message))
});
}
var chanName = "";
//obligatoire pour l'analyseur de code kotlin
if(e.client.data["channel"] != null){
chanName = e.client.data["channel"].toString();
}
//dispatch.....SPATCH!!!!
for(c in this.clients){
this.dispatchMessage(chanName,message,"add");
}
}else{
System.out.println("unknown JSON: "+json.toString());
}
}catch (e : Exception){
//System.out.println("Something went wrong (probably JSON parsing error");
//utile pour debug
e.printStackTrace();
}
}
/**
* SPATCH!! methode envoyant aux bon clients les opérations demandées
* @param channel String sous-channel
* @param message Message message a dispatch
* @param operation String operation a effectuer (add, upd, del)
*/
fun dispatchMessage(channel: String, message: Message, operation : String){
//whitelist
val authorizedOperations = listOf<String>("add","upd","del");
//wow much security, very reliable, wow
if(!authorizedOperations.contains(operation)){
throw Exception("Unauthorized operation");
}
//on construit le JSON
val array = JSONObject();
array.put(message.id,message.toJSON())
val jsonMessage = JSONObject();
jsonMessage.put("error",false);
//toutes les opréations doivent être présente mais peuvent être vide
for (op in authorizedOperations){
if(op == operation){
jsonMessage.put(op,array);
}else{
jsonMessage.put(op, JSONArray());
}
}
//SPATCH!!
for(c in this.clients){
//on envoie a tout les clients loggés du bon channel, ou en broadcast si aucun channel n'est précisé
if(c.data["IsLogged"] == true && ((channel != "" && c.data["channel"] == channel) || channel == "")){
c.send(buildTextMessage(jsonMessage.toString()));
}
}
}
/**
* Methode utilisée par les classes Interop pour que l'API puisse agir sur les channels
* @param id String id du message a supprimer
*/
fun deleteMessage(id : String){
//comme tout les ids sont unique, on doit parcourir tout les channels a la recherche du message
this.messages.keys.forEach {
//comme j'utilise la version fonctionnelle des boucle, la variable it (qui contiens l'item) se reécrit dans la deuxieme boucle
val key = it;
val array = this.messages[it];
array?.toList()?.forEach{
if(it.second.id == id){
//on enleve de la stack
array.remove(it);
//on créer un message vide, seul l'id est utile
val msg = Message()
msg.id = id;
//on envoie au client
this.dispatchMessage(key,msg,"del");
//petite optimisation pour éviter de continuer a parcourir les channels
return;
}
}
}
}
/**
* Methode utilisée par les classes Interop pour que l'API puisse agir sur les channels
* @param msg Message message avec le contenu modifié
*/
fun updateMessage(msg : Message){
//comme pour le delete, on doit tout parcourir
this.messages.keys.forEach {
val key = it;
val array = this.messages[it];
var index = 0;
array?.toList()?.forEach{
if(it.second.id == msg.id){
//on peuple le nouveau message des anciennes valeurs
msg.user = it.second.user;
msg.location = it.second.location;
msg.timestamp = it.second.timestamp;
array.set(index,Pair(it.first,msg));
//on envoie
this.dispatchMessage(key,msg,"upd");
}
index++;
}
}
}
/**
* Methode implémentée mais pas utilisée suite a une décision durant la nuit
* aucune idée de si ça marche du coup
*/
fun getMessages(chan : String) : MutableList<Pair<String, Message>>{
val channel = this.messages[chan]?.toList();
val returned = mutableListOf<Pair<String, Message>>()
if(channel != null){
for (m in channel){
returned.add(Pair(m.second.id,m.second));
}
}
return returned;
}
}

View File

@ -1,219 +1,13 @@
package Channels package Channels
import Collections.Message
import Collections.Stack
import org.json.JSONArray
import org.json.JSONObject
import seekdasky.kWebSocket.Client
import seekdasky.kWebSocket.Event
import seekdasky.kWebSocket.Listeners.AsynchronousListener
import seekdasky.kWebSocket.Message.buildTextMessage
import seekdasky.kWebSocket.Server import seekdasky.kWebSocket.Server
class Emergency : AsynchronousListener { /**
* Classe gérant le channel emergency
*/
class Emergency : AbstractChannel {
var clients : MutableList<Client>; override val chanName = "emergency";
val messages : MutableMap<String,Stack<Pair<String,Message>>>;
val serv : Server;
constructor(serv : Server){ constructor(server: Server) : super(server);
this.clients = mutableListOf();
this.messages = mutableMapOf();
this.serv = serv;
}
override fun filter(c: Client): Boolean {
val regex = "\\/emergency\\/(.*)".toRegex()
val result = regex.findAll(c.URL);
if(result.count() == 1){
return true;
}
return false;
}
override fun processClosed(e: Event) {
this.clients.remove(e.client);
ConnectChannel.notifyDisconnect(e.client);
System.out.println("A client disconnected ("+this.clients.count()+" clients connected)");
}
override fun processConnection(c: Client) {
this.clients.add(c);
val regex = "\\/emergency\\/(.*)".toRegex();
val result = regex.findAll(c.URL).elementAt(0).groups[1]?.value ?: "";
//create stack if not present
if(this.messages[result] == null){
this.messages[result] = Stack( 50);
}
//bind client to channel
if(c.data["emergencyChannel"] == null){
c.data["emergencyChannel"] = result;
}
c.data["IsLogged"] = false;
}
fun manuallyDispatchMessage(channel : String, msg : String, username : String, location : List<Float>){
val message = Message();
message.location = location.toMutableList();
message.message = msg;
message.user = username;
message.timestamp = System.currentTimeMillis() / 1000;
val channelStack = this.messages[channel];
if(channelStack != null){
synchronized(channelStack, {
channelStack.push(Pair(username,message));
})
val array = JSONArray();
array.put(message.toJSON())
val jsonMessage = JSONObject();
jsonMessage.put("error",false);
jsonMessage.put("data",array);
for(c in this.clients){
if(c.data["IsLogged"] == true && c.data["emergencyChannel"] == channel){
c.send(buildTextMessage(jsonMessage.toString()));
}
}
}else if (channel == ""){
//broadcast
for(c in this.messages.values){
synchronized(c,{
c.push(Pair(username,message));
})
}
val array = JSONArray();
array.put(message.toJSON())
val jsonMessage = JSONObject();
jsonMessage.put("error",false);
jsonMessage.put("data",array);
for(c in this.clients){
if(c.data["IsLogged"] == true){
c.send(buildTextMessage(jsonMessage.toString()));
}
}
}
}
override fun processEvent(e: Event) {
System.out.println(e.message.toString());
try{
val json = JSONObject(e.message.getString())
//dead code, should remove it later
if(json.has("close")){
return
}
//si l'utilisateur n'est pas log, on le check
if(e.client.data["IsLogged"] != true){
//json format : {username:xxx}
if(json.has("name") && json.getString("name") != null){
e.client.data["Username"] = json.getString("name");
//if the client correctly identified with the Interop server
if(ConnectChannel.isConnected(e.client)){
//log
System.out.println("Emergency connection ("+this.clients.count()+" clients connected)");
//return OK to the client
e.client.data["IsLogged"] = true;
val jsonLogin = JSONObject();
jsonLogin.put("error",false);
e.client.send(buildTextMessage(jsonLogin.toString()));
//give the client the list of the most recent messages
val array = JSONArray();
val chanMessages = this.messages[e.client.data["emergencyChannel"]];
if(chanMessages != null){
synchronized(chanMessages,{
chanMessages.toList().forEach {
array.put(it.second.toJSON())
}
})
}
//build JSON
val jsonMessage = JSONObject();
jsonMessage.put("error",false);
jsonMessage.put("data",array);
e.client.send(buildTextMessage(jsonMessage.toString()));
}else{
val jsonError = JSONObject();
jsonError.put("error","Invalid credentials");
e.client.send(buildTextMessage(jsonError.toString()));
}
}else{
//tried to access chat without logging in
val jsonError = JSONObject();
jsonError.put("error","You must send your credential before sending anything else");
e.client.send(buildTextMessage(jsonError.toString()));
}
return;
}else if(json.has("message")){
val chanMessages = this.messages[e.client.data["emergencyChannel"]];
val message = Message();
if(chanMessages != null){
message.user = e.client.data["Username"].toString();
message.message = json.getString("message");
var lat = 0F;
var lng = 0F;
//javascript may send float as integer so we double check that
try {
lat = (json.getJSONArray("location")[0] as Double).toFloat();
}catch(e : Exception){
lat = (json.getJSONArray("location")[0] as Integer).toFloat();
}
try {
lng = (json.getJSONArray("location")[1] as Double).toFloat();
}catch(e : Exception){
lng = (json.getJSONArray("location")[1] as Integer).toFloat();
}
message.location = mutableListOf(lat,lng);
synchronized(chanMessages,{
chanMessages.push(Pair(e.client.data["Username"].toString(),message))
});
}
val array = JSONArray();
array.put(message.toJSON())
val jsonMessage = JSONObject();
jsonMessage.put("error",false);
jsonMessage.put("data",array);
for(c in this.clients){
if(c.data["IsLogged"] == true && c.data["emergencyChannel"] == e.client.data["emergencyChannel"]){
c.send(buildTextMessage(jsonMessage.toString()));
}
}
}else{
System.out.println("unknown JSON: "+json.toString());
}
}catch (e : Exception){
//System.out.println("Something went wrong (probably JSON parsing error");
e.printStackTrace();
}
}
} }

14
src/Channels/Event.kt Normal file
View File

@ -0,0 +1,14 @@
package Channels
import seekdasky.kWebSocket.Server
/**
* classe gérant le channel Event
*/
class Event : AbstractChannel {
override val chanName = "event";
constructor(server: Server) : super(server);
}

View File

@ -3,18 +3,27 @@ package Collections
import org.json.JSONObject import org.json.JSONObject
import java.lang.System; import java.lang.System;
/**
* Modèle de donnée standard pour tout les messages de channel
*/
class Message { class Message {
var user = ""; var user = "";
var message = ""; var message = "";
var timestamp = 0L; var timestamp = 0L;
var location = mutableListOf<Float>(); var location = mutableListOf<Float>();
var id = "";
var type = -1;
//par défaut la date de message est celle actuelle
constructor(){ constructor(){
//on converti le timestamp de milliseconde en seconde (UNIX timestamp)
this.timestamp = System.currentTimeMillis() / 1000; this.timestamp = System.currentTimeMillis() / 1000;
} }
//permet de facilement serialiser le message
fun toJSON() : JSONObject{ fun toJSON() : JSONObject{
val json = JSONObject(); val json = JSONObject();
json.put("user",user); json.put("user",user);
@ -22,6 +31,11 @@ class Message {
json.put("timestamp",timestamp); json.put("timestamp",timestamp);
json.put("location",location); json.put("location",location);
//le type n'est pas présent sur tout les channels
if(this.type >= 0){
json.put("type",type);
}
return json; return json;
} }
} }

View File

@ -1,25 +1,39 @@
package Collections package Collections
/**
* Stack FIFO codée pour m'adier a avoir un historique de conversation limité et facile a utiliser
* la classe est générique et réutilisable ailleur au besoin
*/
class Stack<T>(size : Int){ class Stack<T>(size : Int){
var itCounter = 0; //taille max de la stack
val size = size; val size = size;
//collection utilisée pour stocker les données
var items:MutableList<T> = mutableListOf() var items:MutableList<T> = mutableListOf()
//vérifie si la stack est vide
fun isEmpty():Boolean = this.items.isEmpty() fun isEmpty():Boolean = this.items.isEmpty()
//retourne le nombre d'élément de la stack
fun count():Int = this.items.count() fun count():Int = this.items.count()
override fun toString() = this.items.toString() override fun toString() = this.items.toString()
/**
* Rajoute un élément en haut de la stack
* @param element T item a ajouter
*/
fun push(element: T){ fun push(element: T){
//BEWARE this stack is programmed to empty itself automatically if you push an item when the stack is full //ATTENTION cette stack est faite pour évacuer les éléments les plus anciens toute seule en cas de saturation
if(this.items.count() == this.size ){ if(this.items.count() == this.size ){
this.pull(); this.pull();
} }
this.items.add(element) this.items.add(element)
} }
/**
* retire un élément en pas de la stack
*/
fun pull():T?{ fun pull():T?{
if (this.isEmpty()){ if (this.isEmpty()){
return null return null
@ -28,12 +42,24 @@ class Stack<T>(size : Int){
} }
} }
/**
* permet de mettre a jour des élements dans la stack
*/
fun set(index : Int, item : T){
this.items.set(index,item);
}
//enlève un élément de la stack
fun remove(item : T){ fun remove(item : T){
this.items.remove(item); this.items.remove(item);
} }
fun toList() : List<T>{ /**
return this.items.toList(); * Plutot que d'implémenter l'interface Iterable moi même (risque de bug en cas d'itération concurrente), lorsque l'on a besoin d'itérer sur la stack
* on peut apppeller cette méthode et itérer sur la collection Kotlin qui est thread safe
*/
fun toList() : MutableList<T>{
return this.items.toMutableList();
} }
} }

View File

@ -2,18 +2,80 @@ import Collections.Stack
import org.json.JSONObject import org.json.JSONObject
import seekdasky.UTF8Encoding.encode import seekdasky.UTF8Encoding.encode
import seekdasky.kWebSocket.Client import seekdasky.kWebSocket.Client
import seekdasky.kWebSocket.Event
import seekdasky.kWebSocket.InteropEvent import seekdasky.kWebSocket.InteropEvent
import seekdasky.kWebSocket.Listeners.AsynchronousListener
import seekdasky.kWebSocket.Listeners.InteropListener import seekdasky.kWebSocket.Listeners.InteropListener
import seekdasky.kWebSocket.Message.InteropMessage import seekdasky.kWebSocket.Message.InteropMessage
import seekdasky.kWebSocket.Message.buildTextMessage
object ConnectChannel : InteropListener { /**
* Singleton permettant de gérer les conecion par Interop et par WebSocket, Singleton utilisé par les classes lsiteners pour vérifier l'identité des clients
*/
object ConnectChannel : InteropListener, AsynchronousListener() {
//utilisé pour que chaque gues ai un id unique (dans la limite de 500)
private var lastGuestIndex = 0; private var lastGuestIndex = 0;
//clints s'étant loggué mais ne s'étant pas encore connecté a un channel (si un client se loggue sans jamais se connecter a un socket il sera éjecté de la satck
private val notBoundYet = Stack<Pair<String, String>>(500); private val notBoundYet = Stack<Pair<String, String>>(500);
//client connecté a un channel WebSocket
private val connected = mutableMapOf<Client, String>(); private val connected = mutableMapOf<Client, String>();
/**
* méthode de filtre pour le WebSOcket
*/
override fun filter(c: Client): Boolean {
return c.URL == "/connect";
}
override fun processClosed(e: Event) {
return;
}
override fun processConnection(c: Client) {
return
}
/**
* méthode de login par WebSocket
*/
override fun processEvent(e: Event) {
try {
val json = JSONObject(e.message.toString());
//si l'utilisateur est un guest
if(json.getString("type") == "guest"){
//on lui donne un id unique (dans la limtite de 500)
this.lastGuestIndex = ++this.lastGuestIndex % 500;
val guestName = "Guest"+this.lastGuestIndex;
//on retourne au client son id de guest
val jsonReturn = JSONObject();
jsonReturn.put("error", false);
jsonReturn.put("name", guestName);
this.notBoundYet.push(Pair("guest",guestName));
e.client.send(buildTextMessage(jsonReturn.toString()));
}else{
//on n'autorise pas la connection en tant qu'admin ou utilisateur (car aucun lien possible avec la base de donnée pour vérifier les tokens
val jsonReturn = JSONObject();
jsonReturn.put("error", "Admin or User login is disabled when using the websocket API, please use the REST API to access you account");
e.client.send(buildTextMessage(jsonReturn.toString()));
}
}catch (ex : Exception){
val jsonReturn = JSONObject();
jsonReturn.put("error",true);
e.client.send(buildTextMessage(jsonReturn.toString()));
}
}
/**
* méthode de filtre pour le socket Interop
*/
override fun filter(e: InteropEvent): Boolean { override fun filter(e: InteropEvent): Boolean {
try{ try{
val json = JSONObject(e.message.string); val json = JSONObject(e.message.string);
@ -26,13 +88,18 @@ object ConnectChannel : InteropListener {
} }
} }
/**
* méthode de loggin par Interop
*/
override fun processEvent(e: InteropEvent) { override fun processEvent(e: InteropEvent) {
try { try {
val json = JSONObject(e.message.string); val json = JSONObject(e.message.string);
if(json.getString("type") == "guest"){ if(json.getString("type") == "guest"){
//Login de guest
this.lastGuestIndex = ++this.lastGuestIndex % 500; this.lastGuestIndex = ++this.lastGuestIndex % 500;
val guestName = "Guest"+this.lastGuestIndex; val guestName = "Guest"+this.lastGuestIndex;
//on retourne l'id de l'utilisateur
val jsonReturn = JSONObject(); val jsonReturn = JSONObject();
jsonReturn.put("error", false); jsonReturn.put("error", false);
jsonReturn.put("name", guestName); jsonReturn.put("name", guestName);
@ -42,6 +109,7 @@ object ConnectChannel : InteropListener {
e.client.send(InteropMessage(encode(jsonReturn.toString()))); e.client.send(InteropMessage(encode(jsonReturn.toString())));
}else{ }else{
//la vérification de token étant faite par l'API REST, on peut logg les utilisateurs et admin
val jsonReturn = JSONObject(); val jsonReturn = JSONObject();
jsonReturn.put("error", false); jsonReturn.put("error", false);
jsonReturn.put("name", json.getString("name")); jsonReturn.put("name", json.getString("name"));
@ -56,15 +124,21 @@ object ConnectChannel : InteropListener {
} }
} }
/**
* méthode appelée par les Listeners Interop et WebSocket pour vérifier l'identitée d'un client
*/
fun isConnected(client : Client) : Boolean{ fun isConnected(client : Client) : Boolean{
//est ce que l'utilisateur est deja log sur un autre channel
for(c in this.connected.keys){ for(c in this.connected.keys){
if(c.data["Username"] == client.data["Username"]){ if(c.data["Username"] == client.data["Username"]){
return true; return true;
} }
} }
//est ce qu'il ne s'est pas encore log sur un channel
for(p in this.notBoundYet.toList()) { for(p in this.notBoundYet.toList()) {
if (p.second == client.data["Username"]) { if (p.second == client.data["Username"]) {
//on rajoute le client au clients utilisant un channel
this.connected.put(client, p.first); this.connected.put(client, p.first);
this.notBoundYet.remove(p); this.notBoundYet.remove(p);
return true; return true;
@ -74,6 +148,9 @@ object ConnectChannel : InteropListener {
return false; return false;
} }
/**
* lorsqu'un client se déconnecte, on remet son ID dans la catégorie des clients pas encore connecté afin de ne pas avoir a su re loguer a chaque changement de channel
*/
fun notifyDisconnect(client : Client){ fun notifyDisconnect(client : Client){
val clientType = this.connected[client]; val clientType = this.connected[client];
if(clientType != null){ if(clientType != null){

55
src/Interop/DelMessage.kt Normal file
View File

@ -0,0 +1,55 @@
package Interop
import Channels.Emergency
import Channels.Event
import org.json.JSONObject
import seekdasky.kWebSocket.InteropEvent
import seekdasky.kWebSocket.Listeners.InteropListener
/**
* Classe Interop permettant de gérer toutes les suppréssions de message
*/
class DelMessage : InteropListener {
//channel emergency
val emergencyController : Emergency;
//channel event
val eventController : Event;
constructor(emergency : Emergency, event : Event){
this.emergencyController = emergency;
this.eventController = event;
}
/**
* Comme pour les Listeners WebSocket, cette méthode filtre les évenements entrant
*/
override fun filter(e: InteropEvent): Boolean {
val json = JSONObject(e.message.string);
return json.has("operation") &&
json.getString("operation") == "DelMessage";
}
/**
* méthode gérant la suppression de message
*/
override fun processEvent(e: InteropEvent) {
try {
val json = JSONObject(e.message.string);
//on dispatch au bon channel
if(json.getString("channelType") == "Emergency"){
this.emergencyController.deleteMessage(json.getString("id"));
}else if(json.getString("channelType") == "Event"){
this.eventController.deleteMessage(json.getString("id"));
}
}catch(e : Exception){
e.printStackTrace()
}
}
}

View File

@ -1,18 +1,32 @@
package Interop package Interop
import Channels.Emergency import Channels.Emergency
import Channels.Event
import Collections.Message
import org.json.JSONObject import org.json.JSONObject
import seekdasky.kWebSocket.InteropEvent import seekdasky.kWebSocket.InteropEvent
import seekdasky.kWebSocket.Listeners.InteropListener import seekdasky.kWebSocket.Listeners.InteropListener
/**
* Classe Interop permettant de gérer tous les ajouts de message
*/
class PostMessage : InteropListener { class PostMessage : InteropListener {
//channel emergency
val emergencyController : Emergency; val emergencyController : Emergency;
//channel event
val eventController : Event;
constructor(emergency : Emergency){
constructor(emergency : Emergency, event : Event){
this.emergencyController = emergency; this.emergencyController = emergency;
this.eventController = event;
} }
/**
* cf: DelMessage
*/
override fun filter(e: InteropEvent): Boolean { override fun filter(e: InteropEvent): Boolean {
val json = JSONObject(e.message.string); val json = JSONObject(e.message.string);
@ -21,19 +35,51 @@ class PostMessage : InteropListener {
json.getString("operation") == "PostMessage"; json.getString("operation") == "PostMessage";
} }
/**
* méthode traitant l'ajout de message
*/
override fun processEvent(e: InteropEvent) { override fun processEvent(e: InteropEvent) {
try { try {
val json = JSONObject(e.message.string); val json = JSONObject(e.message.string);
val location = listOf<Float>((json.getJSONArray("location")[0] as Double).toFloat(), (json.getJSONArray("location")[1] as Double).toFloat()); //javascript fait un peu ce qu'il veut au niveau des nombres et peut envoyer des doubles ou des int, on doit tout cast en float
var lat : Float;
var lng : Float
try {
lat = (json.getJSONArray("location")[0] as Double).toFloat();
}catch(e : Exception){
lat = (json.getJSONArray("location")[0] as Integer).toFloat();
}
this.emergencyController.manuallyDispatchMessage( try {
json.getString("channelName"), lng = (json.getJSONArray("location")[1] as Double).toFloat();
json.getString("message"), }catch(e : Exception){
json.getString("username"), lng = (json.getJSONArray("location")[1] as Integer).toFloat();
location }
)
val location = mutableListOf<Float>(lat, lng);
//on peuple le modèle
val message = Message();
message.location = location;
message.message = json.getString("message");
message.user = json.getString("username");
message.id = json.getString("id");
if(json.getString("channelType") == "Emergency"){
this.emergencyController.manuallyDispatchMessage(
json.getString("channelName"),
message
);
}else if(json.getString("channelType") == "Event"){
message.type = json.getInt("type");
this.eventController.manuallyDispatchMessage(
json.getString("channelName"),
message
);
}
}catch(e : Exception){ }catch(e : Exception){

56
src/Interop/UpdMessage.kt Normal file
View File

@ -0,0 +1,56 @@
package Interop
import Channels.Emergency
import Channels.Event
import Collections.Message
import org.json.JSONObject
import seekdasky.kWebSocket.InteropEvent
import seekdasky.kWebSocket.Listeners.InteropListener
/**
* Classe Interop permettant de gérer toutes les mise a jour de message
*/
class UpdMessage : InteropListener {
val emergencyController : Emergency;
val eventController : Event;
constructor(emergency : Emergency, event : Event){
this.emergencyController = emergency;
this.eventController = event;
}
override fun filter(e: InteropEvent): Boolean {
val json = JSONObject(e.message.string);
return json.has("operation") &&
json.getString("operation") == "UpdMessage";
}
override fun processEvent(e: InteropEvent) {
try {
val json = JSONObject(e.message.string);
if(json.getString("channelType") == "Emergency"){
val message = Message();
message.message = json.getString("message");
message.id = json.getString("id");
this.emergencyController.updateMessage(message);
}else if(json.getString("channelType") == "Event"){
val message = Message();
message.message = json.getString("message");
message.id = json.getString("id");
this.eventController.updateMessage(message);
}
}catch(e : Exception){
e.printStackTrace()
}
}
}

View File

@ -1,19 +1,38 @@
import Channels.Emergency import Channels.Emergency
import Channels.Event
import Interop.DelMessage
import Interop.PostMessage import Interop.PostMessage
import Interop.UpdMessage
import seekdasky.kWebSocket.Server import seekdasky.kWebSocket.Server
fun main(args: Array<String>){ fun main(args: Array<String>){
val server = Server("0.0.0.0",9999,null); //on démarre le serveur WebSOcket
val server = Server("0.0.0.0",9999,null)
//on démarre le serveur Interop
server.startInteropServer("localhost",9998); server.startInteropServer("localhost",9998);
//channels WebSocket pilotés par Interop
val emergencyChannel = Emergency(server); val emergencyChannel = Emergency(server);
val eventChannel = Event(server);
//channel WebSocket
server.addListener(Channel(server,"/chat")); server.addListener(Channel(server,"/chat"));
//on bind les listeners
server.addListener(emergencyChannel); server.addListener(emergencyChannel);
server.addInteropListener(PostMessage(emergencyChannel)); server.addListener(eventChannel);
//service de login par WebSocket
server.addListener(ConnectChannel);
//Listeners Interops
server.addInteropListener(PostMessage(emergencyChannel,eventChannel));
server.addInteropListener(DelMessage(emergencyChannel,eventChannel));
server.addInteropListener(UpdMessage(emergencyChannel,eventChannel));
//login Interop
server.addInteropListener(ConnectChannel); server.addInteropListener(ConnectChannel);
//et c'est partiiiiiiii
server.startServer(); server.startServer();
//méthode bloquant le thread principal tant que le serveur est en vie
server.block(); server.block();
} }