+ * @see IMessageHandler
+ * @version 1.0
+ * @author Guillaume "Cess" Prost
+ */
+public class SimpleMessageHandler implements IMessageHandler {
+
+ private final String header_ref_query = "Detail_refus";
+ private final String header_err_query = "Detail_erreur";
+
+ /*
+ * Liste des services du serveur
+ */
+ private AccountService accountService;
+ private SaleService saleService;
+ private StatisticsService statisticsService;
+ private StockService stockService;
+ //TODO faire qqch de clientService
+ //private ClientService clientService;
+ private ServerStateService serverStateService;
+ private IMessageToolFactory messageToolFactory;
+
+ private MessageManager manager;
+
+ public SimpleMessageHandler() {
+ serverStateService = new ServerStateService();
+ accountService = new AccountService();
+ //clientService = new ClientService();
+ stockService = new StockService();
+ saleService = new SaleService(stockService);
+ statisticsService = new StatisticsService(stockService, saleService);
+ messageToolFactory = new GSonMessageToolFactory();
+
+ manager = new MessageManager();
+
+ /*
+ WAITER-restricted queries
+ */
+ manager.supportForConnectedAccounts(MessageType.QUERY_STOCK, (message)->{
+ Map fields = new HashMap<>();
+ fields.put("stock", messageToolFactory.getListFormatter(Product.class).format(stockService.getProductList()));
+ return new Message(MessageType.MSG_STOCK, serverStateService.getServer(), message.getEmitter(), message.getUser(), message, fields);
+ }, AccountLevel.WAITER);
+ manager.supportForConnectedAccounts(MessageType.QUERY_SALE, (message)->{
+ Map fields = new HashMap<>();
+ try {
+ long saleId = saleService.submitSale(messageToolFactory.getObjectParser(Sale.class).parse(message.getField("sale")));
+ fields.put("saleId", Long.toString(saleId));
+ return new Message(MessageType.ACK_SALE, serverStateService.getServer(), message.getEmitter(), message.getUser(), message, fields);
+ }catch(NullPointerException e){
+ fields.put(header_ref_query, e.toString());
+ return new Message(MessageType.REFUSED_QUERY, serverStateService.getServer(), message.getEmitter(), message.getUser(), message, fields);
+ }
+ }, AccountLevel.WAITER);
+ manager.supportForConnectedAccounts(MessageType.QUERY_STAT, (message)->{
+ Map fields = new HashMap<>();
+ fields.put("total_sale_worth", Double.toString(statisticsService.getTotalSaleWorth()));
+ fields.put("total_sale_amount", Integer.toString(statisticsService.getTotalAmountSale()));
+ fields.put("total_money_made", Double.toString(statisticsService.getTotalMoneyMade()));
+ fields.put("top_popular_products", messageToolFactory.getListFormatter(LightweightProduct.class).format(statisticsService.getTopPopularProducts(5)));
+ fields.put("staff_sale_worth",Double.toString(statisticsService.getStaffSaleWorth()));
+ fields.put("staff_sale_amount",Integer.toString(statisticsService.getStaffSaleAmount()));
+ fields.put("guest_sale_worth",Double.toString(statisticsService.getGuestSaleWorth()));
+ fields.put("guest_sale_amount",Integer.toString(statisticsService.getGuestSaleAmount()));
+
+ return new Message(MessageType.MSG_STAT, serverStateService.getServer(), message.getEmitter(), message.getUser(), message, fields);
+ }, AccountLevel.WAITER);
+
+ /*
+ MASTER-restricted queries
+ */
+ manager.supportForConnectedAccounts(MessageType.QUERY_UPDATE, (message)->{
+ try{
+ List updates = messageToolFactory.getListParser(ProductUpdate.class).parse(message.getField("updates"));
+ stockService.applyUpdateList(updates);
+ return new Message(MessageType.ACK_UPDATE, serverStateService.getServer(), message.getEmitter(), message.getUser(), message, null);
+ }catch (ServerQueryException | NullPointerException e){
+ Map fields = new HashMap<>();
+ fields.put(header_err_query, e.toString());
+ return new Message(MessageType.ERROR_QUERY, serverStateService.getServer(), message.getEmitter(), message.getUser(), message, fields);
+ }
+ }, AccountLevel.MASTER);
+
+ /*
+ Queries without account connection requirements
+ */
+ manager.support(MessageType.QUERY_ACCOUNT_LIST, (message)->{
+ Map fields = new HashMap<>();
+ fields.put("accounts", messageToolFactory.getListFormatter(Account.class).format(accountService.getAccountList()));
+ return new Message(MessageType.MSG_ACCOUNT_LIST, serverStateService.getServer(), message.getEmitter(), message.getUser(), message, fields);
+ }, AccountLevel.getLowest(), false);
+ manager.support(MessageType.QUERY_CONNECT_ACCOUNT, (message)->{
+ final String desiredStateFieldHeader = "desired_state",
+ accountCredentialsFieldHeader = "account";
+
+ if(message.getFields().containsKey(desiredStateFieldHeader)){
+ if(message.getFields().containsKey(accountCredentialsFieldHeader)){
+ boolean desiredState = messageToolFactory.getObjectParser(Boolean.class).parse(message.getField(desiredStateFieldHeader));
+ Account accountCredentials = messageToolFactory.getObjectParser(Account.class).parse(message.getField(accountCredentialsFieldHeader));
+
+ if(accountService.submitAccountCredentials(accountCredentials, desiredState)){
+ return new Message(MessageType.ACK_CONNECT_ACCOUNT, serverStateService.getServer(), message.getEmitter(), message.getUser(), message, null);
+ }else{
+ Map fields = new HashMap<>();
+ fields.put(header_ref_query, "Impossible d'effectuer l'action : identifiants invalides ou état désiré déjà atteint");
+
+ return new Message(MessageType.REFUSED_QUERY, serverStateService.getServer(), message.getEmitter(), message.getUser(), message, fields);
+ }
+ }else{
+ return getMissingArgumentQueryReplyMessage(message, accountCredentialsFieldHeader);
+ }
+ }else{
+ return getMissingArgumentQueryReplyMessage(message, desiredStateFieldHeader);
+ }
+ }, AccountLevel.getLowest(), false);
+ manager.support(MessageType.QUERY_PING, (message)->new Message(MessageType.ACK_PING, serverStateService.getServer(), message.getEmitter(), message.getUser(), message, null), AccountLevel.getLowest(), false);
+ manager.support(MessageType.QUERY_CONFIG_LIST, (message)->{
+ Map fields = new HashMap<>();
+ fields.put("config", messageToolFactory.getObjectFormatter(ServerConfig.class).format(serverStateService.getConfig()));
+ return new Message(MessageType.MSG_CONFIG_LIST, serverStateService.getServer(), message.getEmitter(), message.getUser(), message, fields);
+ }, AccountLevel.getLowest(), false);
+ }
+
+ private Message getUnsupportedQueryReplyMessage(Message message){
+ final String msg_ref_unsupported_query = "Ce type de requêtes n'est actuellement pas supportée par ce serveur.";
+ Map fields = new HashMap<>();
+ fields.put(header_ref_query, msg_ref_unsupported_query);
+
+ return new Message(MessageType.REFUSED_QUERY,
+ serverStateService.getServer(),
+ message.getEmitter(),
+ message.getUser(),
+ message,
+ fields);
+ }
+
+ private Message getMissingArgumentQueryReplyMessage(Message message, String missingArgumentHeader){
+ Map fields = new HashMap<>();
+ fields.put(header_err_query, "The following required header is missing : "+missingArgumentHeader);
+
+ return new Message(MessageType.ERROR_QUERY,
+ serverStateService.getServer(),
+ message.getEmitter(),
+ message.getUser(),
+ message,
+ fields);
+ }
+
+ private Message getExceptionOccuredQueryReplyMessage(Message message, Exception exception){
+ Map fields = new HashMap<>();
+ fields.put(header_err_query, exception.getMessage());
+
+ return new Message(MessageType.ERROR_QUERY,
+ serverStateService.getServer(),
+ message.getEmitter(),
+ message.getUser(),
+ message,
+ fields);
+ }
+
+ @Override
+ public Message handleMessage(Message message) {
+
+ Map fields = new HashMap<>();
+
+ if(manager.contains(message.getType())){
+ if(manager.isQueryRestrictedToConnectedAccount(message.getType())) {
+ if (!isAccountRegisteredAndConnected(message)) {
+ fields.put(header_ref_query, "Compte utilisateur inconnu ou déconnecté.");
+ return new Message(MessageType.REFUSED_QUERY, serverStateService.getServer(), message.getEmitter(), message.getUser(), message, fields);
+ }
+ if (!checkAccountPermission(message)) {
+ fields.put(header_ref_query, "Compte utilisateur avec permission trop faible");
+ return new Message(MessageType.REFUSED_QUERY, serverStateService.getServer(), message.getEmitter(), message.getUser(), message, fields);
+ }
+ }
+ try{
+ return manager.getProcess(message.getType()).execute(message);
+ }catch(Exception e){
+ return getExceptionOccuredQueryReplyMessage(message, e);
+ }
+ }
+
+ return getUnsupportedQueryReplyMessage(message);
+ }
+
+ /**
+ * Interface interne utilisé pour encapsuler le traitement des messages dans un objet
+ */
+ private interface IMessageProcess{
+ Message execute(Message request);
+ }
+
+ private class MessageTypeEntry{
+ private MessageType type;
+ private IMessageProcess process;
+ private AccountLevel level;
+ private boolean connectedAccountRestriction;
+
+ MessageTypeEntry(MessageType type, IMessageProcess process, AccountLevel level, boolean connectedAccountRestriction) {
+ this.type = type;
+ this.process = process;
+ this.level = level;
+ this.connectedAccountRestriction = connectedAccountRestriction;
+ }
+
+ IMessageProcess getProcess() {
+ return process;
+ }
+
+ AccountLevel getLevel() {
+ return level;
+ }
+
+ boolean isConnectedAccountRestriction() {
+ return connectedAccountRestriction;
+ }
+
+ boolean matches(MessageType type){
+ return type.equals(this.type);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ MessageTypeEntry that = (MessageTypeEntry) o;
+
+ return connectedAccountRestriction == that.connectedAccountRestriction && type == that.type && process.equals(that.process) && level == that.level;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = type.hashCode();
+ result = 31 * result + process.hashCode();
+ result = 31 * result + level.hashCode();
+ result = 31 * result + (connectedAccountRestriction ? 1 : 0);
+ return result;
+ }
+ }
+
+ private class MessageManager{
+ private Set entries;
+
+ MessageManager(){
+ entries = new HashSet<>();
+ }
+
+ void supportForConnectedAccounts(MessageType type, IMessageProcess process, AccountLevel permissionLevel){
+ support(type, process, permissionLevel, true);
+ }
+
+ void support(MessageType type, IMessageProcess process, AccountLevel permissionLevel, boolean accountConnectionRequired){
+ entries.add(new MessageTypeEntry(type, process, permissionLevel, accountConnectionRequired));
+ }
+
+ private MessageTypeEntry getFirstMatch(MessageType type){
+ return entries.stream().filter(entry->entry.matches(type)).findFirst().orElse(null);
+ }
+
+ IMessageProcess getProcess(MessageType messageType){
+ MessageTypeEntry entry = getFirstMatch(messageType);
+ if(entry!=null)
+ return entry.getProcess();
+
+ return null;
+ }
+
+ AccountLevel getLevel(MessageType messageType){
+ MessageTypeEntry entry = getFirstMatch(messageType);
+ if(entry!=null)
+ return entry.getLevel();
+
+ return null;
+ }
+
+ boolean contains(MessageType type) {
+ return getFirstMatch(type)!=null;
+ }
+
+ boolean isQueryRestrictedToConnectedAccount(MessageType type) {
+ MessageTypeEntry entry = getFirstMatch(type);
+ return entry != null && entry.isConnectedAccountRestriction();
+ }
+ }
+
+ /**
+ * Vérifie si le compte utilisé pour émettre la query contenu dans le message est enregistré et connecté.
+ * @param message message dont l'expéditeur doit être vérifié
+ * @return true si le compte est existant et connecté, false si au moins une des conditions est fausse.
+ */
+ private boolean isAccountRegisteredAndConnected(Message message){
+ return accountService.isAccountRegistered(message.getUser()) && accountService.isAccountConnected(message.getUser());
+ }
+
+ /**
+ * Vérifie si le compte émetteur de la query a les autorisations suffisantes pour effectuer la query.
+ *
+ * @param message message dont le niveau de permission de l'expéditeur doit être vérifié
+ * @return true si l'expéditeur correspond à un compte et qu'il a les autorisations suffisantes, false sinon.
+ */
+ private boolean checkAccountPermission(Message message){
+ return accountService.isAccountRegistered(message.getUser()) && accountService.getAccountPermissionLevel(message.getUser()).compareTo(manager.getLevel(message.getType()))>=0;
+ }
+}
diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/exception/ServerQueryException.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/exception/ServerQueryException.java
new file mode 100644
index 00000000..314e5f46
--- /dev/null
+++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/exception/ServerQueryException.java
@@ -0,0 +1,19 @@
+package com.pqt.server.exception;
+
+public class ServerQueryException extends Exception {
+
+ public ServerQueryException() {
+ }
+
+ public ServerQueryException(String message) {
+ super(message);
+ }
+
+ public ServerQueryException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public ServerQueryException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/account/AccountEntry.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/account/AccountEntry.java
new file mode 100644
index 00000000..9e7fcd05
--- /dev/null
+++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/account/AccountEntry.java
@@ -0,0 +1,58 @@
+package com.pqt.server.module.account;
+
+import com.pqt.core.entities.user_account.AccountLevel;
+
+import java.io.Serializable;
+
+public class AccountEntry implements Serializable{
+ private String username, passwordHash, salt;
+ private AccountLevel level;
+
+ public AccountEntry() {
+ }
+
+ public AccountEntry(String username, String passwordHash, String salt, AccountLevel level) {
+ this.username = username;
+ this.passwordHash = passwordHash;
+ this.salt = salt;
+ this.level = level;
+ }
+
+ String getUsername() {
+ return username;
+ }
+
+ String getPasswordHash() {
+ return passwordHash;
+ }
+
+ String getSalt() {
+ return salt;
+ }
+
+ AccountLevel getLevel() {
+ return level;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ AccountEntry that = (AccountEntry) o;
+
+ if (!username.equals(that.username)) return false;
+ if (!passwordHash.equals(that.passwordHash)) return false;
+ if (!salt.equals(that.salt)) return false;
+ return level == that.level;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = username.hashCode();
+ result = 31 * result + passwordHash.hashCode();
+ result = 31 * result + salt.hashCode();
+ result = 31 * result + level.hashCode();
+ return result;
+ }
+}
diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/account/AccountService.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/account/AccountService.java
new file mode 100644
index 00000000..5a7c7ce3
--- /dev/null
+++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/account/AccountService.java
@@ -0,0 +1,120 @@
+package com.pqt.server.module.account;
+
+import com.pqt.core.entities.user_account.Account;
+import com.pqt.core.entities.user_account.AccountLevel;
+
+import java.util.List;
+
+//TODO ajouter logs
+/**
+ * Cette classe correspond au service de gestion des comptes utilisateurs. Il permet la vérification de l'existance
+ * d'un compte, de son état (connecté/déconnecté), de changer son état ainsi que de récupérer son niveau d'accréditation.
+ *
+ * @author Guillaume "Cess" Prost
+ * @see AccountLevel
+ * @see Account
+ */
+public class AccountService {
+
+ private IAccountDao dao;
+
+ public AccountService() {
+ dao = new FileAccountDao();
+ }
+
+ /**
+ * Vérifie si un compte utilisateur donné est actuellement connecté. Le compte utilisateur doit être existant
+ * pour être connecté.
+ *
+ * Les informations contenues dans l'objet {@code account} passé en paramètre ne sont pas directements utilisées,
+ * elles servent juste à déterminer le compte utilisateur réel correspondant. Il est nécessaire de s'assurer
+ * que les données passées en paramètres soient justes avant de faire appel à cette méthode.
+ *
+ * Pour cette méthode, seul le nom d'utilisateur est pris en compte pour établir une correspondance.
+ *
+ * Dans le cas où aucune correspondance ne peut être faite entre les informations fournies et les données
+ * enregistrées, la valeur {@code false} sera renvoyée.
+ *
+ * @param account Objet {@link Account} dont les informations seront utilisées pour déterminer le compte concerné.
+ *
+ * @return {@code true} si une correspondance a pû être établie et que le compte correspondant est connecté,
+ * {@code false} sinon.
+ */
+ public boolean isAccountConnected(Account account) {
+ return dao.isAccountConnected(account);
+ }
+
+ /**
+ * Soumet une demande de changement d'état pour un compte utilisateur. Les états possibles sont "connecté" ({@code true})
+ * et "déconnecté" ({@code false}).
+ *
+ * Les informations contenues dans l'objet {@code account} passé en paramètre ne sont pas directements utilisées,
+ * elles servent juste à déterminer le compte utilisateur réel correspondant. Il est nécessaire de s'assurer
+ * que les données passées en paramètres soient justes avant de faire appel à cette méthode.
+ *
+ * Pour cette méthode, seul le nom d'utilisateur est pris en compte pour établir une correspondance. Le mot de passe
+ * est uniquement requis pour une connexion, et pas pour une déconnexion.
+ *
+ * Une fois la correspondance effectuée, une tentative de changement d'état sera faite pour le compte correspondant.
+ * Cette tentative peut échouer si :
+ *
+ *
Le compte est déjà dans l'état désiré
+ *
Le mot de passe ne correspond pas (uniquement pour une connexion)
+ *
+ * Dans le cas d'un échec, l'état du compte reste inchangé et la valeur booléenne {@code false} est renvoyée,
+ * sans plus de détails. Si le changement d'état a eu lieu, la valeur booléenne {@code true} est renvoyée.
+ *
+ * @param account Objet {@link Account} dont les informations seront utilisées pour déterminer le compte concerné.
+ * @param desiredState L'état dans lequel le compte doit se trouver une fois le changement fait.
+ * @return {@code true} si le changement d'état a eu lieu, {@code false} sinon.
+ */
+ public boolean submitAccountCredentials(Account account, boolean desiredState) {
+ return dao.submitAccountCredentials(account, desiredState);
+ }
+
+ /**
+ * Vérifie si un compte utilisateur donné existe dans la base de donnée du serveur.
+ *
+ * Les informations contenues dans l'objet {@code account} passé en paramètre ne sont pas directements utilisées,
+ * elles servent juste à déterminer le compte utilisateur réel correspondant. Il est nécessaire de s'assurer
+ * que les données passées en paramètres soient justes avant de faire appel à cette méthode.
+ *
+ * Pour cette méthode, seul le nom d'utilisateur est pris en compte pour établir une correspondance.
+ *
+ * @param account Objet {@link Account} dont les informations seront utilisées pour déterminer le compte concerné.
+ * @return {@code true} si une correspondance a pû être établie entre les données fournies et un compte dans la base
+ * de données, {@code false} sinon.
+ */
+ public boolean isAccountRegistered(Account account){
+ return dao.isAccountRegistered(account);
+ }
+
+ /**
+ * Récupère le niveau de permission du compte utilisateur correspondant aux informations fournies en paramètre.
+ *
+ * Les informations contenues dans l'objet {@code account} passé en paramètre ne sont pas directements utilisées,
+ * elles servent juste à déterminer le compte utilisateur réel correspondant. Il est nécessaire de s'assurer
+ * que les données passées en paramètres soient justes avant de faire appel à cette méthode.
+ *
+ * Pour cette méthode, seul le nom d'utilisateur est pris en compte pour établir une correspondance.
+ *
+ * @param account Objet {@link Account} dont les informations seront utilisées pour déterminer le compte concerné.
+ * @return Le niveau de permission {@link AccountLevel} du compte correspondant aux informations si une correspondance
+ * a pû être établie entre {@code account} et un compte utilisateur de la base de donnée, et {@code null} si aucune
+ * correspondance n'a pû être faite.
+ */
+ public AccountLevel getAccountPermissionLevel(Account account){
+ return dao.getAccountPermissionLevel(account);
+ }
+
+ /**
+ * Renvoie la liste des comptes utilisateurs contenus dans la base de données sous forme d'une liste d'objets
+ * {@link Account}. Seuls les noms d'utilisateurs ainsi que les niveaux de permissions sont récupérés, les
+ * autres champs étant volontairement initialisés avec une valeur {@code null}.
+ * @return Une liste d'objet {@link Account} représentant les différents comptes utilisateurs existant dans la base
+ * de données.
+ */
+ public List getAccountList(){
+ return dao.getAccountList();
+ }
+}
diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/account/FileAccountDao.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/account/FileAccountDao.java
new file mode 100644
index 00000000..3695b015
--- /dev/null
+++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/account/FileAccountDao.java
@@ -0,0 +1,148 @@
+package com.pqt.server.module.account;
+
+import com.pqt.core.entities.user_account.Account;
+import com.pqt.core.entities.user_account.AccountLevel;
+import com.pqt.server.tools.io.ISerialFileManager;
+import com.pqt.server.tools.io.SimpleSerialFileManagerFactory;
+import com.pqt.server.tools.security.IHashTool;
+import com.pqt.server.tools.security.MD5HashTool;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+//TODO ajouter logs
+
+/**
+ * Implémentation de l'interface {@link IAccountDao} utilisant un fichier contenant des objets sérialisés comme
+ * source de persistance de données.
+ *
+ * Cette classe n'est pas faite pour gérer les accès concurentiels au fichier assurant la persistance, et n'est donc pas
+ * thread-safe. Elle est conçue pour que tous les accès soient effectués depuis un même thread et depuis un unique objet.
+ *
+ * Cette classe manipule les mot de passe sous forme chiffrée via un système de hash (md5) + salt, et ne fait pas
+ * persister les mots de passes non-chiffrées. Les noms d'utilisateurs sont stockés sans chiffrage.
+ */
+class FileAccountDao implements IAccountDao {
+
+ private static final String ACCOUNT_FILE_NAME = "acc.pqt";
+
+ private Set accountEntries;
+ private Set connectedAccount;
+ private IHashTool hashTool;
+ private ISerialFileManager fileManager;
+
+ FileAccountDao() {
+ accountEntries = new HashSet<>();
+ connectedAccount = new HashSet<>();
+ hashTool = new MD5HashTool();
+ fileManager = SimpleSerialFileManagerFactory.getFileManager(AccountEntry.class, ACCOUNT_FILE_NAME);
+ loadFromFile();
+ }
+
+ /**
+ * Recherche une correspondance entre un objet {@link Account} et les objets {@link AccountEntry} contenu dans
+ * la collection {@code entries}. La correspondance se base sur la valeur renvoyée par {@link Account#getUsername()}
+ * et sur {@link AccountEntry#getUsername()}.
+ * @param account données à utiliser pour rechercher la correspondance.
+ * @param entries collection de données à utiliser pour rechercher la correspondance
+ * @return La première correspondance trouvée, ou {@code null} si aucune correspondance n'a pu être faite.
+ */
+ private AccountEntry lookupMatchingEntry(Account account, Collection entries){
+ return entries.stream().filter(accountEntry -> accountEntry.getUsername().equals(account.getUsername())).findFirst().orElse(null);
+ }
+
+ @Override
+ public synchronized boolean isAccountConnected(Account account) {
+ return lookupMatchingEntry(account, connectedAccount)!=null;
+ }
+
+ @Override
+ public synchronized boolean submitAccountCredentials(Account account, boolean desiredState) {
+ if(isAccountRegistered(account)){
+ if(desiredState!=isAccountConnected(account)){
+ if(desiredState)
+ return connect(account);
+ else
+ return disconnect(account);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Passe un compte déconnecté dans l'état connecté. N'effecctue le changement que si un compte déconnecté correspond
+ * aux données fournies et que le mot de passe fournit par {@code account.getPassword()} corresspond à celui du
+ * compte correspondant.
+ *
+ * @param account données à utiliser pour effectuer la correspondance et l'identification
+ * @return {@code true} si le changement d'état a eu lieu, {@code false sinon}
+ */
+ private boolean connect(Account account){
+ AccountEntry entry = lookupMatchingEntry(account, accountEntries);
+ if(entry==null)
+ return false;
+ else{
+ String expectedUsername = entry.getUsername();
+ String expectedPasswordHash = entry.getPasswordHash();
+ String salt = entry.getSalt();
+
+ if(expectedUsername.equals(account.getUsername()) && hashTool.hashAndSalt(account.getPassword(), salt).equals(expectedPasswordHash)){
+ connectedAccount.add(entry);
+ return true;
+ }else
+ return false;
+ }
+ }
+
+ /**
+ * Passe un comtpe connecté dans l'état déconnecté. N'effectue le changement que si un compte connecté correspond
+ * aux données fournies.
+ * @param account données à utiliser pour efffectuer la correspondance avec un compte
+ * @return {@code true} si le changement d'état a eu lieu, {@code false sinon}
+ */
+ private boolean disconnect(Account account){
+ AccountEntry entry = lookupMatchingEntry(account, accountEntries);
+ if(entry!=null && connectedAccount.contains(entry)){
+ connectedAccount.remove(entry);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public synchronized boolean isAccountRegistered(Account account) {
+ return lookupMatchingEntry(account, accountEntries)!=null;
+ }
+
+ @Override
+ public synchronized AccountLevel getAccountPermissionLevel(Account account) {
+ if(isAccountRegistered(account))
+ return lookupMatchingEntry(account, accountEntries).getLevel();
+ return null;
+ }
+
+ @Override
+ public synchronized List getAccountList() {
+ return accountEntries.stream().map(accountEntry -> new Account(accountEntry.getUsername(), null, accountEntry.getLevel())).collect(Collectors.toList());
+ }
+
+ /**
+ * Sauvegarde les données des comptes dans le fichier de sauvegarde.
+ */
+ private void saveToFile(){
+ fileManager.saveSetToFile(accountEntries);
+ }
+
+ /**
+ * Charge les données des comptes depuis le fichier de sauvegarde.
+ *
+ * Attention : pour des raisons de cohérence des données, tous les comptes connectés sont repassés dans l'état
+ * déconnectés une fois le chargement fait.
+ */
+ private void loadFromFile(){
+ this.accountEntries = new HashSet<>(fileManager.loadSetFromFile());
+ //TODO faire check des comptes au lieu de tout déconnecter?
+ this.connectedAccount.clear();
+ }
+}
diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/account/IAccountDao.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/account/IAccountDao.java
new file mode 100644
index 00000000..04c70f81
--- /dev/null
+++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/account/IAccountDao.java
@@ -0,0 +1,62 @@
+package com.pqt.server.module.account;
+
+import com.pqt.core.entities.user_account.Account;
+import com.pqt.core.entities.user_account.AccountLevel;
+
+import java.util.List;
+
+/**
+ * Cette interface définit les méthodes de base que doivent avoir les classes de DAO du service {@link AccountService}.
+ *
+ * Les implémentations de cette interface sont censé assurer la persistance des donnéess de connexions des comptes
+ * utilisateurs et doit également garder une trace niveau runtime de l'état des comptes (état connecté ou état
+ * déconnecté). Enfin, les implémentations doivent pouvoir déterminer une correspondance entre un nom d'utilisateur et
+ * un compte, et doit pouvoir effectuer les changements d'état sur la base d'un mot de passe non-chiffré fournit grâce
+ * à une instance {@link Account} (voir {@link #submitAccountCredentials(Account, boolean)}).
+ *
+ * @author Guillaume "Cess" Prost
+ */
+interface IAccountDao {
+
+ /**
+ * @see AccountService#isAccountConnected(Account)
+ *
+ * @param account données à utiliser
+ * @return {@code true} si les données correspondent à un compte et que ce dernier est connecté, {@code false}
+ * sinon.
+ */
+ boolean isAccountConnected(Account account);
+
+ /**
+ * @see AccountService#submitAccountCredentials(Account, boolean)
+ *
+ * @param account données à utiliser
+ * @param desiredState état désiré pour le compte
+ * @return {@code true} si les données correspondent à un compte et que le changement d'état a eu lieu,
+ * {@code false} sinon.
+ */
+ boolean submitAccountCredentials(Account account, boolean desiredState);
+
+ /**
+ * @see AccountService#isAccountRegistered(Account)
+ *
+ * @param account données à utiliser
+ * @return {@code true} si les données correspondent à un compte, {@code false} sinon.
+ */
+ boolean isAccountRegistered(Account account);
+
+ /**
+ * @see AccountService#getAccountPermissionLevel(Account)
+ * @param account données à utiliser
+ * @return Le niveau d'accréditation du compte utilisateur correspondant aux données, ou {@code null} si aucun
+ * compte ne correspond.
+ */
+ AccountLevel getAccountPermissionLevel(Account account);
+
+ /**
+ * @see AccountService#getAccountList()
+ * @return Une liste d'objet {@link Account} représentant les différents comptes utilisateurs existant dans la base
+ * de données.
+ */
+ List getAccountList();
+}
diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/client/ClientEntry.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/client/ClientEntry.java
new file mode 100644
index 00000000..2f577782
--- /dev/null
+++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/client/ClientEntry.java
@@ -0,0 +1,58 @@
+package com.pqt.server.module.client;
+
+import com.pqt.core.entities.members.Client;
+
+import java.util.Date;
+
+public class ClientEntry {
+ private Client client;
+ private Date timestamp;
+
+ public ClientEntry(Client client) {
+ this.client = client;
+ timestamp = new Date();
+ }
+
+ public ClientEntry(Client client, Date timestamp) {
+ this.client = client;
+ this.timestamp = timestamp;
+ }
+
+ public Client getClient() {
+ return client;
+ }
+
+ public void setClient(Client client) {
+ this.client = client;
+ }
+
+ public Date getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(Date timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ public boolean check(Client client){
+ return this.client.equals(client);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ ClientEntry that = (ClientEntry) o;
+
+ if (client != null ? !client.equals(that.client) : that.client != null) return false;
+ return timestamp != null ? timestamp.equals(that.timestamp) : that.timestamp == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = client != null ? client.hashCode() : 0;
+ result = 31 * result + (timestamp != null ? timestamp.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/client/ClientService.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/client/ClientService.java
new file mode 100644
index 00000000..26a03396
--- /dev/null
+++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/client/ClientService.java
@@ -0,0 +1,91 @@
+package com.pqt.server.module.client;
+
+import com.pqt.core.entities.members.Client;
+
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+//TODO ajouter logs
+
+/**
+ * Cette classe correspond au service de gestion des clients.
+ *
+ * Un client est une instance du logiciel de composition des commandes, ce dernier étant la principale entitée capable
+ * d'envoyer des requêtes aux serveurs de données.
+ *
+ * Ce service est censé permettre la tracabilité des clients se connectant au serveur, en gardant en cache tous les
+ * clients avec des horodateurs représentant la date et l'heure de la dernière requête reçue de chaque client.
+ */
+public class ClientService {
+
+ private Set clientCache;
+
+ public ClientService(){
+ clientCache = new HashSet<>();
+ }
+
+ private ClientEntry lookupClientEntry(Client client){
+ return clientCache.stream().filter(clientEntry -> clientEntry.check(client)).findFirst().orElse(null);
+ }
+ /**
+ * Vérifie si le client donné est connu.
+ * @param client client à vérifier
+ * @return {@code true} si le client donné correspond à une entrée du cache, {@code false} sinon.
+ */
+ public boolean isClientRegistered(Client client) {
+ return clientCache.contains(client);
+ }
+
+ /**
+ * Enregistre un client dans le cache du service. Si le client existe déjà dans la base, rafraichit l'horodateur
+ * associé.
+ * @param client client à enregistrer
+ */
+ public void registerClient(Client client) {
+ if(lookupClientEntry(client)==null){
+ clientCache.add(new ClientEntry(client));
+ }else{
+ refreshClientTimestamp(client);
+ }
+ }
+
+ /**
+ * Etabit une correspondance entre {@code client} et une entrée du cache du service, et renvoie l'horodateur associé
+ * à la correspondance. Renvoie {@code null} si aucune correspondance n'a pû être faite.
+ * @param client données à utiliser pour établir la correspondance
+ * @return l'horodateur associé à la correspondance, ou {@code null} si aucune correspondance ne peut être faite.
+ */
+ public Date getClientTimestamp(Client client) {
+ ClientEntry entry = lookupClientEntry(client);
+ return entry!=null? entry.getTimestamp() : null;
+ }
+
+ /**
+ * Récupère la liste des clients actuellement dans le cache du service
+ * @return Liste des clients dans le cache
+ */
+ public List getClientList(){
+ return clientCache.stream().map(ClientEntry::getClient).collect(Collectors.toList());
+ }
+
+ /**
+ * Vide le cache du service.
+ */
+ public void clear(){
+ clientCache.clear();
+ }
+
+ /**
+ * Met à jour l'horodateur associé au client donné.
+ * @param client données à utiliser pour établir la correspondance
+ */
+ private void refreshClientTimestamp(Client client) {
+ ClientEntry entry = lookupClientEntry(client);
+ if(entry!=null){
+ entry.setTimestamp(new Date());
+ }
+ }
+}
diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/sale/ISaleDao.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/sale/ISaleDao.java
new file mode 100644
index 00000000..c26d7c86
--- /dev/null
+++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/sale/ISaleDao.java
@@ -0,0 +1,23 @@
+package com.pqt.server.module.sale;
+
+import com.pqt.core.entities.sale.Sale;
+
+/**
+ * Interface définissant les méthodes requises pour tout DAO du service de gestion des commandes {@link SaleService}.
+ *
+ * Les implémentations de cette interface doivent pouvoir valider des commandes, agir sur le stock et générer les
+ * identifiants de commandes validées.
+ *
+ * Les implémentations peuvent (optionnel) assurer une persistance des données relatives aux commandes validées, et
+ * peuvent donc assurer le revert des commandes {@link #submitSaleRevert(long)}. Le support de cette fonctionnalité
+ * est optionnel.
+ *
+ * @see SaleService pour de plus amples détails sur le fonctionnement attendu des méthodes
+ */
+public interface ISaleDao {
+
+ long submitSale(Sale sale);
+
+ boolean isSaleRevertSupported();
+ boolean submitSaleRevert(long id);
+}
diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/sale/NoRevertFileSaleDao.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/sale/NoRevertFileSaleDao.java
new file mode 100644
index 00000000..96e34d11
--- /dev/null
+++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/sale/NoRevertFileSaleDao.java
@@ -0,0 +1,150 @@
+package com.pqt.server.module.sale;
+
+import com.pqt.core.entities.product.Product;
+import com.pqt.core.entities.sale.Sale;
+import com.pqt.server.module.stock.StockService;
+import com.pqt.server.tools.FileUtil;
+import com.pqt.server.tools.entities.SaleContent;
+import org.apache.commons.io.input.ReversedLinesFileReader;
+
+import java.io.*;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Iterator;
+
+/**
+ * Implémentation de l'interface {@link ISaleDao} utilisant un fichier comme moyen pour assurer la persistance des
+ * données relatives aux commandes validées. Cette implémentation ne supporte pas le rollback de commandes.
+ * .
+ * La persistance des données est faite de sorte que ces données soient lisibles par un humain lorsque le fichier est
+ * ouvert avecc un éditeur de texte quelconque.
+ *
+ * Les identifiants attribués aux commandes validées sont incrémentés à chaque fois, en se basant soit sur la dernière
+ * valeur attribuée (lue depuis le fichier de sauvegarde lors de l'instantiation), soit sur une valeur par défaut. Ils
+ * sont tous positifs et non-nuls.
+ */
+public class NoRevertFileSaleDao implements ISaleDao {
+
+ private static final String SALE_LOG_FILE_NAME = "sale_log.txt";
+ private static final long DEFAULT_SALE_ID = 0; //équivaut à la valeur du premier id - 1
+ private StockService stockService;
+ private long nextSaleId;
+ private ISaleRenderer renderer;
+
+ NoRevertFileSaleDao(StockService stockService) {
+ this.stockService = stockService;
+ this.renderer = getRenderer();
+ nextSaleId = readLastSaleIdFromFile()+1;
+ }
+
+ @Override
+ public long submitSale(Sale sale) {
+ boolean valid = true;
+ Iterator it = sale.getProducts().keySet().iterator();
+ while(valid && it.hasNext()){
+ Product p = it.next();
+ Product product = stockService.getProduct(p.getId());
+ valid = product!=null
+ && p.equals(product)
+ && product.isSellable()
+ && product.getAmountRemaining()>=sale.getProducts().get(p);
+ }
+
+ if(!valid)
+ return -1;
+
+ long saleId = nextSaleId;
+ stockService.applySale(new SaleContent(sale));
+ logSale(sale, saleId);
+ generateNextSaleId();
+ return saleId;
+ }
+
+ private void generateNextSaleId() {
+ nextSaleId++;
+ }
+
+ /**
+ * Read the last sale id written in the log file with title {@link #SALE_LOG_FILE_NAME} or a default value if such id has not been found.
+ *
+ * Different reasons why this method may not find any id :
+ * - file does not exist
+ * - file is empty
+ * - file does not respect the expected syntax for writing sales' data
+ *
+ * The log file with title {@link #SALE_LOG_FILE_NAME} is not created by this method if it doesn't exist yet.
+ * @return last sale id used in the log file, or -1 if none was found.
+ */
+ private long readLastSaleIdFromFile(){
+ long id = DEFAULT_SALE_ID;
+ if(FileUtil.exist(SALE_LOG_FILE_NAME)){
+ try(ReversedLinesFileReader rlfr = new ReversedLinesFileReader(new File("SALE_LOG_FILE_NAME"))){
+ boolean stop = false;
+ do{
+ try {
+ String line = rlfr.readLine();
+ if(line.matches("^[0-9]+$")){
+ id = Long.parseLong(line.substring(1));
+ stop = true;
+ }
+ }catch (EOFException e){
+ stop = true;
+ }
+ }while(!stop);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return id;
+ }else{
+ return id;
+ }
+ }
+
+ @Override
+ public boolean isSaleRevertSupported() {
+ return false;
+ }
+
+ @Override
+ public boolean submitSaleRevert(long id) {
+ //TODO Créer un nouveau dao qui supporte le revert
+ throw new UnsupportedOperationException("Le revert de commandes n'est pas supporté");
+ }
+
+ private void logSale(Sale sale, long saleId){
+ try(FileOutputStream fos = new FileOutputStream(SALE_LOG_FILE_NAME);
+ PrintWriter pw = new PrintWriter(fos)){
+
+ pw.append(renderer.render(sale, saleId));
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private ISaleRenderer getRenderer(){
+ return(sale, id)->{
+ StringBuffer sb = new StringBuffer("\n#").append(id).append("\n");
+ String separator = "-----";
+ DateFormat dateFormat = new SimpleDateFormat("
");
+
+ sb.append("type : ").append(sale.getType().name()).append("\n");
+ sb.append("at : ").append(dateFormat.format(sale.getOrderedAt())).append("\n");
+ if(sale.getOrderedBy()!=null)
+ sb.append("by : ").append(sale.getOrderedBy().getUsername()).append("(").append(sale.getOrderedBy().getPermissionLevel().name()).append(")").append("\n");
+ if(sale.getOrderedFor()!=null)
+ sb.append("for : ").append(sale.getOrderedFor().getUsername()).append("(").append(sale.getOrderedFor().getPermissionLevel().name()).append(")").append("\n");
+ sb.append(separator).append("\n");
+ sb.append("Products : \n");
+ sale.getProducts().keySet().forEach(p->{
+ int productAmount = sale.getProducts().get(p);
+ sb.append(String.format(" * %s (%du, %f€) : %d remaining in stock", p.getName(), productAmount, p.getPrice()*(double)productAmount, p.getAmountRemaining()-productAmount)).append("\n");
+ });
+ sb.append(separator);
+ return sb.toString();
+ };
+ }
+
+ private interface ISaleRenderer{
+ String render(Sale sale, long saleId);
+ }
+}
diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/sale/SaleService.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/sale/SaleService.java
new file mode 100644
index 00000000..54b31fc3
--- /dev/null
+++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/sale/SaleService.java
@@ -0,0 +1,92 @@
+package com.pqt.server.module.sale;
+
+import com.pqt.core.entities.sale.Sale;
+import com.pqt.server.exception.ServerQueryException;
+import com.pqt.server.module.sale.listeners.ISaleFirerer;
+import com.pqt.server.module.sale.listeners.ISaleListener;
+import com.pqt.server.module.sale.listeners.SimpleSaleFirerer;
+import com.pqt.server.module.stock.StockService;
+
+//TODO ajouter logs
+
+/**
+ * Cette classe correspond au service de validation des commandes de produits.
+ *
+ * Ce service est censé pouvoir déterminer si une commmande (classe {@link Sale}) est valide ou non, et doit le cas
+ * échéant effectuer les retraits de produits du stock. A chaque commande validée doit correspondre un identifiant
+ * unique, qui doit être renvoyé en réponse de la validation. Cet identifiant doit permettre de pouvoir annuler
+ * ultérieurement la commande correspondante via la méthode {@link #submitSaleRevert(long)}.
+ *
+ * Une commande est considérée comme valide si tous les produits composants la commande existent dans le stock et que
+ * les quantités demandées dans la commande sont disponibles en stock.
+ *
+ * Ce service met également à disposition la possibilité d'enregistrer des observateurs, qui seront utilisés pour
+ * exécuter des méthodes lors de certains événements, comme la validation d'une commande.
+ *
+ * @see ISaleListener
+ */
+public class SaleService {
+
+ private ISaleDao dao;
+ private ISaleFirerer eventFirerer;
+
+ public SaleService(StockService stockService) {
+ dao = new NoRevertFileSaleDao(stockService);
+ eventFirerer = new SimpleSaleFirerer();
+ }
+
+ /**
+ * Soumet une commande au service pour validation. Si la commande est validée, les stocks seront débités et
+ * l'identifiant de la commande sera renvoyé. Si la commande n'est pas validée, la valeur {@value -1} sera renvoyée
+ * et les stocks resterons inchangés.
+ * @param sale commande à valider
+ * @return l'identifiant positif non-nul attribué à la commande si elle est validée, {@value -1} sinon.
+ */
+ public long submitSale(Sale sale) {
+ long id = dao.submitSale(sale);
+ if(id!=-1) eventFirerer.fireSaleValidatedEvent(sale);
+ return id;
+ }
+
+ /**
+ * Détermine si le rollback de commande est supporté par la configuration actuelle du serveur ou non.
+ *
+ * Tenter d'effectuer un rollback de commande alors que ce dernier n'est pas supporté lèvera une
+ * {@link UnsupportedOperationException}.
+ *
+ * @return {@code true} si le rollback de commande est supporté, {@code false} sinon.
+ */
+ public boolean isSaleRevertSupported(){
+ return dao.isSaleRevertSupported();
+ }
+
+ /**
+ * Demande le rollback d'une commande en se basant sur l'identifiant.
+ * @param id identifiant de la commande à annuler
+ * @return {@code true} si la commande a bel et bien été annulée, {@code false} si aucun changement n'a été fait.
+ */
+ public boolean submitSaleRevert(long id) {
+ if(isSaleRevertSupported())
+ return dao.submitSaleRevert(id);
+ else
+ throw new UnsupportedOperationException("Cette opération ('sale revert') n'est pas supportée par la configuration actuelle du serveur");
+ }
+
+ /**
+ * Ajout un observateur au service, qui sera notifié lorsque certains événements auront lieu.
+ * @param l observateur à ajouter.
+ * @see ISaleListener
+ */
+ public void addListener(ISaleListener l) {
+ eventFirerer.addListener(l);
+ }
+
+ /**
+ * Retire un observateur du service.
+ * @param l observateur à retirer.
+ * @see ISaleListener
+ */
+ public void removeListener(ISaleListener l){
+ eventFirerer.addListener(l);
+ }
+}
diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/sale/listeners/ISaleFirerer.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/sale/listeners/ISaleFirerer.java
new file mode 100644
index 00000000..d8b34efb
--- /dev/null
+++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/sale/listeners/ISaleFirerer.java
@@ -0,0 +1,11 @@
+package com.pqt.server.module.sale.listeners;
+
+import com.pqt.core.entities.sale.Sale;
+
+public interface ISaleFirerer {
+
+ void addListener(ISaleListener l);
+ void removeListener(ISaleListener l);
+
+ void fireSaleValidatedEvent(Sale sale);
+}
diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/sale/listeners/ISaleListener.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/sale/listeners/ISaleListener.java
new file mode 100644
index 00000000..11b70979
--- /dev/null
+++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/sale/listeners/ISaleListener.java
@@ -0,0 +1,9 @@
+package com.pqt.server.module.sale.listeners;
+
+import com.pqt.core.entities.sale.Sale;
+
+import java.util.EventListener;
+
+public interface ISaleListener extends EventListener{
+ void onSaleValidatedEvent(Sale sale);
+}
diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/sale/listeners/SaleListenerAdapter.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/sale/listeners/SaleListenerAdapter.java
new file mode 100644
index 00000000..6315b70d
--- /dev/null
+++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/sale/listeners/SaleListenerAdapter.java
@@ -0,0 +1,10 @@
+package com.pqt.server.module.sale.listeners;
+
+import com.pqt.core.entities.sale.Sale;
+
+public class SaleListenerAdapter implements ISaleListener {
+ @Override
+ public void onSaleValidatedEvent(Sale sale) {
+
+ }
+}
diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/sale/listeners/SimpleSaleFirerer.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/sale/listeners/SimpleSaleFirerer.java
new file mode 100644
index 00000000..1c94abdb
--- /dev/null
+++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/sale/listeners/SimpleSaleFirerer.java
@@ -0,0 +1,31 @@
+package com.pqt.server.module.sale.listeners;
+
+import com.pqt.core.entities.sale.Sale;
+
+import javax.swing.event.EventListenerList;
+
+public class SimpleSaleFirerer implements ISaleFirerer {
+
+ private EventListenerList listeners;
+
+ public SimpleSaleFirerer() {
+ listeners = new EventListenerList();
+ }
+
+ @Override
+ public void addListener(ISaleListener l) {
+ listeners.add(ISaleListener.class, l);
+ }
+
+ @Override
+ public void removeListener(ISaleListener l) {
+ listeners.remove(ISaleListener.class, l);
+ }
+
+ @Override
+ public void fireSaleValidatedEvent(Sale sale) {
+ for(ISaleListener l : listeners.getListeners(ISaleListener.class)){
+ l.onSaleValidatedEvent(sale);
+ }
+ }
+}
diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/state/ServerState.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/state/ServerState.java
new file mode 100644
index 00000000..96cd1190
--- /dev/null
+++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/state/ServerState.java
@@ -0,0 +1,43 @@
+package com.pqt.server.module.state;
+
+public class ServerState {
+
+ private int port;
+
+ private boolean serverState;
+
+ public ServerState() {
+ port = -1;
+ serverState = false;
+ }
+
+ public ServerState(int port) {
+ this.port = port;
+ serverState = false;
+ }
+
+ public ServerState(ServerState clone){
+ this.serverState = clone.serverState;
+ this.port = clone.port;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public void setPort(int port) {
+ this.port = port;
+ }
+
+ public boolean isServerState() {
+ return serverState;
+ }
+
+ public void setServerState(boolean serverState) {
+ this.serverState = serverState;
+ }
+
+ public ServerState copy() {
+ return new ServerState(this);
+ }
+}
diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/state/ServerStateService.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/state/ServerStateService.java
new file mode 100644
index 00000000..107394e6
--- /dev/null
+++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/state/ServerStateService.java
@@ -0,0 +1,65 @@
+package com.pqt.server.module.state;
+
+import com.pqt.core.entities.members.DataServer;
+import com.pqt.core.entities.server_config.ConfigFields;
+import com.pqt.core.entities.server_config.ServerConfig;
+
+import java.util.Date;
+
+/**
+ * Cette classe correspond au service interne du serveur, chargé de conserver les données propres au serveur, comme
+ * son adresse IP ou encore les différents aspects de la configuration actuelle. Il permet également de récupérer un
+ * objet {@link DataServer}, implémentation de {@link com.pqt.core.entities.members.PqtMember}, qui sert à représenter
+ * ce serveur dans les messages, soit comme émetteur, soit comme destinataire.
+ *
+ * @see com.pqt.core.entities.messages.Message
+ *
+ * @author Guillaume "Cess" Prost
+ */
+public class ServerStateService {
+
+ private ServerState serverState;
+ private DataServer server;
+ private ServerConfig config;
+
+ public ServerStateService() {
+ this.server = new DataServer();
+ this.serverState = new ServerState();
+ this.config = new ServerConfig(
+ ConfigFields.ALLOW_ACCOUNT_CONNECT,
+ ConfigFields.ALLOW_SALE_COMMIT,
+ ConfigFields.ALLOW_STOCK_UPDATE,
+ ConfigFields.ALLOW_STOCK_VIEW);
+
+ //TODO config adresse IP
+ //this.com.pqt.server.setAddress(...);
+ }
+
+ public void startServer() {
+ serverState.setServerState(true);
+ }
+
+ public void stopServer() {
+ serverState.setServerState(false);
+ }
+
+ public void changeConnectionPort(int port) {
+ serverState.setPort(port);
+ }
+
+ public DataServer getServer() {
+ return server;
+ }
+
+ public void setServer(DataServer server) {
+ this.server = server;
+ }
+
+ public ServerState getServerStateCopy() {
+ return serverState.copy();
+ }
+
+ public ServerConfig getConfig() {
+ return config;
+ }
+}
diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/statistics/StatisticsService.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/statistics/StatisticsService.java
new file mode 100644
index 00000000..8d925c90
--- /dev/null
+++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/statistics/StatisticsService.java
@@ -0,0 +1,97 @@
+package com.pqt.server.module.statistics;
+
+import com.pqt.core.entities.product.LightweightProduct;
+import com.pqt.core.entities.product.Product;
+import com.pqt.core.entities.sale.Sale;
+import com.pqt.server.module.sale.listeners.ISaleListener;
+import com.pqt.server.module.sale.listeners.SaleListenerAdapter;
+import com.pqt.server.module.stock.StockService;
+import com.pqt.server.module.sale.SaleService;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+//TODO Ajouter logs
+
+/**
+ * Cette classe correspond au sservices de statistiques du serveur, chargé de calculer et de mettre à disposition
+ * diverses données concernant les ventes effectuées et les produits vendus.
+ */
+public class StatisticsService {
+
+ private StockService stockService;
+
+ private int totalSaleAmount, staffSaleAmount, guestSaleAmount;
+ private double totalMoneyMade, totalSaleWorth, staffSaleWorth, guestSaleWorth;
+
+ public StatisticsService(StockService stockService, SaleService saleService) {
+ this.stockService = stockService;
+
+ totalSaleAmount = 0;
+ staffSaleAmount = 0;
+ guestSaleAmount = 0;
+
+ totalMoneyMade = 0;
+ totalSaleWorth = 0;
+ staffSaleWorth = 0;
+ guestSaleWorth = 0;
+
+ saleService.addListener(new SaleListenerAdapter() {
+ @Override
+ public void onSaleValidatedEvent(Sale sale) {
+ double price = sale.getTotalPrice(), worth = sale.getTotalWorth();
+ totalSaleWorth+=worth;
+ totalMoneyMade+=price;
+ totalSaleAmount++;
+ switch (sale.getType()){
+ case OFFERED_GUEST:
+ guestSaleAmount++;
+ guestSaleWorth+=worth;
+ break;
+ case OFFERED_STAFF_MEMBER:
+ staffSaleAmount++;
+ staffSaleWorth+=price;
+ break;
+ }
+ }
+ });
+ }
+
+ public int getTotalAmountSale() {
+ return totalSaleAmount;
+ }
+
+ public double getTotalMoneyMade() {
+ return totalMoneyMade;
+ }
+
+ public double getTotalSaleWorth() {
+ return totalSaleWorth;
+ }
+
+ public List getTopPopularProducts(int amount) {
+ return stockService.getProductList().stream()
+ .sorted(Comparator.comparingInt(Product::getAmountSold))
+ .limit(amount)
+ .map(LightweightProduct::new)
+ .collect(Collectors.toList());
+ }
+
+ public int getStaffSaleAmount() {
+ return staffSaleAmount;
+ }
+
+ public double getStaffSaleWorth() {
+ return staffSaleWorth;
+ }
+
+ public int getGuestSaleAmount() {
+ return guestSaleAmount;
+ }
+
+ public double getGuestSaleWorth() {
+ return guestSaleWorth;
+ }
+}
diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/stock/FileStockDao.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/stock/FileStockDao.java
new file mode 100644
index 00000000..97cbfac7
--- /dev/null
+++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/stock/FileStockDao.java
@@ -0,0 +1,173 @@
+package com.pqt.server.module.stock;
+
+import com.pqt.core.entities.product.Product;
+import com.pqt.server.tools.entities.SaleContent;
+import com.pqt.server.tools.io.ISerialFileManager;
+import com.pqt.server.tools.io.SimpleSerialFileManagerFactory;
+
+import java.lang.IllegalStateException;
+import java.util.*;
+
+/**
+ * Implémentation de l'interface {@link IStockDao} utilisant un fichier de sauvegarde pour assurer la persistance des
+ * données liées aux produits vendus.
+ *
+ * Les données sont écrites et lues dans le fichier grâce au méchanisme de sérialisation/désérialisation. Elles ne sont
+ * pas faites pour être lisibles directement par un humain.
+ *
+ * @author Guillaume "Cess" Prost
+ */
+public class FileStockDao implements IStockDao {
+
+ private static final String STOCK_FILE_NAME = "stock.pqt";
+ private ISerialFileManager fileManager;
+ private long nextProductId;
+ private Random random;
+
+ private Map products;
+
+ FileStockDao() {
+ random = new Random();
+ fileManager = SimpleSerialFileManagerFactory.getFileManager(Product.class, STOCK_FILE_NAME);
+ loadFromFile();
+ generateNextProductId();
+ }
+
+ private void generateNextProductId() {
+ Long newId;
+ do{
+ newId = random.nextLong();
+ }while (products.containsKey(newId));
+ nextProductId = newId;
+ }
+
+ /**
+ * @see com.pqt.server.module.stock.IStockDao#getProductList()
+ */
+ public List getProductList() {
+ return copyOfProductList();
+ }
+
+ private List copyOfProductList() {
+ List copy = new ArrayList<>();
+ products.values().forEach(p->copy.add(new Product(p)));
+ return copy;
+ }
+
+ /**
+ * @see com.pqt.server.module.stock.IStockDao#getProduct(long)
+ */
+ public Product getProduct(long id) {
+ return products.get(id);
+ }
+
+ /**
+ * @see com.pqt.server.module.stock.IStockDao#addProduct(com.pqt.core.entities.product.Product)
+ */
+ public long addProduct(Product product) {
+ product.setId(nextProductId);
+ this.products.put(nextProductId, product);
+ long reply = nextProductId;
+ generateNextProductId();
+ saveToFile();
+ return reply;
+ }
+
+ /**
+ * @see com.pqt.server.module.stock.IStockDao#removeProduct(long)
+ */
+ public void removeProduct(long id) {
+ Product product = getProduct(id);
+ if(product!=null){
+ this.products.remove(product.getId());
+ saveToFile();
+ }
+ }
+
+ /**
+ * @see com.pqt.server.module.stock.IStockDao#modifyProduct(long, com.pqt.core.entities.product.Product)
+ */
+ public void modifyProduct(long id, Product product) {
+ if(this.products.containsKey(id)){
+ product.setId(id);
+ this.products.put(id, product);
+ saveToFile();
+ }
+ }
+
+ @Override
+ public void applySale(SaleContent saleContent) throws IllegalArgumentException {
+ if(saleContent==null)
+ return;
+
+ try {
+ for(Product product : saleContent.getProductList()){
+ applyRecursiveStockRemoval(product, saleContent.getProductAmount(product));
+ applySoldCounterIncrease(product, saleContent.getProductAmount(product));
+ }
+ saveToFile();
+ }catch (IllegalStateException e){
+ loadFromFile();
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Cette méthode augmente le compteur de vente pour un produit donné dans la BDD.
+ *
+ * @param product données à utiliser pour déterminer le produit correspondant dans la BDD dont les données doivent
+ * être manipulées.
+ * @param amount montant à ajouter
+ * @throws IllegalStateException exception levée si le produit donné ne peut pas être trouvé dans la base de donnée.
+ */
+ private void applySoldCounterIncrease(Product product, Integer amount) throws IllegalStateException{
+ Product correspondingProduct = getProduct(product.getId());
+ if(correspondingProduct!=null){
+ correspondingProduct.setAmountSold(correspondingProduct.getAmountSold() + amount);
+ }else{
+ StringBuilder sb = new StringBuilder("StockService>StockDao : Un produit vendu ne correspond pas à un produit connu : ");
+ sb.append(product.getId()).append(" - ").append(product.getName()).append("(").append(product.getCategory()).append(")");
+ throw new IllegalStateException(sb.toString());
+ }
+ }
+
+ /**
+ * Cette méthode retire à un produit donné de la BDD le montant spécifié (diminue la valeur de
+ * {@link Product#amountRemaining}), puis effectue récursivement la même opération pour tous les composants de ce
+ * produit.
+ *
+ * @param product données à utiliser pour déterminer le produit correspondant dans la BDD dont les données doivent
+ * être manipulées.
+ * @param amount montant à déduire
+ * @throws IllegalStateException exception levée si le produit donné ne peut pas être trouvé dans la base de donnée.
+ */
+ private void applyRecursiveStockRemoval(Product product, int amount) throws IllegalStateException {
+ Product correspondingProduct = getProduct(product.getId());
+ if(correspondingProduct!=null) {
+ correspondingProduct.setAmountRemaining(correspondingProduct.getAmountRemaining() - amount);
+ correspondingProduct.getComponents().forEach(component -> applyRecursiveStockRemoval(component, amount));
+ }else{
+ StringBuilder sb = new StringBuilder("StockService>StockDao : Un produit vendu ne correspond pas à un produit connu : ");
+ sb.append(product.getId()).append(" - ").append(product.getName()).append("(").append(product.getCategory()).append(")");
+ throw new IllegalStateException(sb.toString());
+ }
+ }
+
+ /**
+ * Cette méthode charge les données relatives aux produits depuis le fichier de sauvegarde. Si ce fichier n'existe
+ * pas, il est créé et la liste des produits est vidée.
+ */
+ private void loadFromFile() {
+ Map loadedData = new HashMap<>();
+ fileManager.loadListFromFile().forEach(product -> loadedData.put(product.getId(), product));
+ products = new HashMap<>(loadedData);
+ }
+
+ /**
+ * Cette méthode écrit les données relatives aux produits dans le fichier de sauvegarde, écrasant le contenu
+ * précédent.
+ */
+ private void saveToFile() {
+ fileManager.saveListToFile(new ArrayList<>(products.values()));
+ }
+}
diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/stock/IStockDao.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/stock/IStockDao.java
new file mode 100644
index 00000000..1eff2683
--- /dev/null
+++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/stock/IStockDao.java
@@ -0,0 +1,67 @@
+package com.pqt.server.module.stock;
+
+import com.pqt.core.entities.product.Product;
+import com.pqt.server.tools.entities.SaleContent;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Interface définissant les méthodes requises pour tout DAO du service de gestion des commandes {@link StockService}.
+ *
+ * Les implémentations doivent assurer une persistance des données relatives aux produits vendus, et doivent assurer
+ * les modifications et les applications de ventes.
+ *
+ * @see StockService pour de plus amples détails sur le fonctionnement attendu des méthodes
+ *
+ * @author Guillaume "Cess" Prost
+ */
+public interface IStockDao {
+
+ /**
+ * Renvoie une copie de la liste des produits contenus dans la base de donnée.
+ * @return copie de la liste des produits.
+ */
+ List getProductList();
+
+ /**
+ * Renvoie le produit correspondant à l'identifiant donné.
+ * @param id identifiant du produit à récupérer
+ * @return Produit correspondant, ou {@code null} si aucun produit ne correspond
+ */
+ Product getProduct(long id);
+
+ /**
+ * Ajoute un produit dans la base de donnée. Son identifiant sera éventuellement modifié pour éviter les conflits.
+ * Dans tous les cas, l'identifiant final du produit est renvoyé une fois l'ajout effectué.
+ * @param product produit à ajouter$
+ * @return identifiant du produit ajouté.
+ */
+ long addProduct(Product product);
+
+ /**
+ * Supprime le produit correspondant à l'identifiant donné.
+ * @param id identifiant du produit à supprimer.
+ */
+ void removeProduct(long id);
+
+
+ /**
+ * Modifie le produit correspondant à l'identifiant donné en le remplaçant par {@code product}. L'identifiant
+ * reste inchangé.
+ *
+ * Si {@code id} ne correspond à aucun produit, aucune modification n'est effectué. Cela signifie que
+ * {@code product} n'est pas ajouté à la BDD.
+ * @param id identifiant du produit à modifier
+ * @param product nouvelle version du produit
+ */
+ void modifyProduct(long id, Product product);
+
+ /**
+ * Applique les modifications de stocks liées à une commande validée, représenté par {@code saleContent}.
+ *
+ * @param saleContent détail des produits et quantités de la commande validée.
+ * @throws IllegalArgumentException Exception levée si une erreur liée au contenu de {@code saleContent} survient.
+ */
+ void applySale(SaleContent saleContent) throws IllegalArgumentException;
+}
diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/stock/StockService.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/stock/StockService.java
new file mode 100644
index 00000000..fe184569
--- /dev/null
+++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/module/stock/StockService.java
@@ -0,0 +1,72 @@
+package com.pqt.server.module.stock;
+
+import com.pqt.core.entities.product.Product;
+import com.pqt.core.entities.product.ProductUpdate;
+import com.pqt.server.exception.ServerQueryException;
+import com.pqt.server.tools.entities.SaleContent;
+
+import java.util.List;
+import java.util.Map;
+
+//TODO ajouter logs
+
+/**
+ * Cette classe correspond au service de gestion du stock de produits. Il est en charge de la persistance des
+ * données liées aux produits, de founir un accès centralisé à ces données et se charge également d'appliquer les
+ * mises à jour de stock (ajout, modif ou suppr de produits) et les ventes de produits issues des commandes
+ * (modification des quantités).
+ *
+ * Attention : ce service ne se charge pas de valider les commandes, il ne fait que modifier les quantités comme si
+ * la commande avait été validé
+ *
+ * @see Product
+ * @see ProductUpdate
+ * @see SaleContent
+ * @author Guillaume "Cess" Prost
+ */
+public class StockService {
+
+ private IStockDao dao;
+
+ public StockService() {
+ dao = new FileStockDao();
+ }
+
+ public List getProductList() {
+ return dao.getProductList();
+ }
+
+ public Product getProduct(long id) {
+ return dao.getProduct(id);
+ }
+
+ public void applySale(SaleContent saleContent) {
+ dao.applySale(saleContent);
+ }
+
+ public void applyUpdateList(List updates) throws ServerQueryException{
+ for(ProductUpdate upd : updates){
+ if(upd.getOldVersion()==null){
+ addProduct(upd.getNewVersion());
+ }else if(upd.getNewVersion()==null){
+ removeProduct(upd.getOldVersion().getId());
+ }else if(upd.getOldVersion()!=null && upd.getNewVersion()!=null){
+ modifyProduct(upd.getOldVersion().getId(), upd.getNewVersion());
+ }else{
+ throw new ServerQueryException("Object ProductUpdate invalide : old et new vallent tous les deux null");
+ }
+ }
+ }
+
+ private void addProduct(Product product) {
+ dao.addProduct(product);
+ }
+
+ private void removeProduct(long id) {
+ dao.removeProduct(id);
+ }
+
+ private void modifyProduct(long id, Product product) {
+ dao.modifyProduct(id, product);
+ }
+}
diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/servlets/QueryServlet.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/servlets/QueryServlet.java
new file mode 100644
index 00000000..e8626bf6
--- /dev/null
+++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/servlets/QueryServlet.java
@@ -0,0 +1,37 @@
+package com.pqt.server.servlets;
+
+import com.pqt.core.communication.GSonMessageToolFactory;
+import com.pqt.core.communication.IMessageToolFactory;
+import com.pqt.core.entities.messages.Message;
+import com.pqt.server.controller.IMessageHandler;
+import com.pqt.server.controller.SimpleMessageHandler;
+
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+//TODO ajouter logs
+@WebServlet(name = "QueryServlet", urlPatterns = "/")
+public class QueryServlet extends HttpServlet {
+ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ executeServletProcess(request, response);
+ }
+
+ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ executeServletProcess(request, response);
+ }
+
+ private void executeServletProcess(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ IMessageToolFactory messageToolFactory = new GSonMessageToolFactory();
+ IMessageHandler msgHandler = new SimpleMessageHandler();
+
+ if (request.getParameter("message") != null) {
+ Message resp = msgHandler.handleMessage(messageToolFactory.getObjectParser(Message.class).parse(request.getParameter("message")));
+
+ response.getWriter().write(messageToolFactory.getObjectFormatter(Message.class).format(resp));
+ }
+ }
+}
diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/tools/FileUtil.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/tools/FileUtil.java
new file mode 100644
index 00000000..26cc83d0
--- /dev/null
+++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/tools/FileUtil.java
@@ -0,0 +1,40 @@
+package com.pqt.server.tools;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+public class FileUtil {
+
+ /**
+ * @see #createFileIfNotExist(Path)
+ */
+ public static boolean createFileIfNotExist(String filePath) throws IOException {
+ return createFileIfNotExist(Paths.get(filePath));
+ }
+
+ /**
+ * Check if the given file path correspond to an existing file, and create it if it doesn't.
+ *
+ * @param filePath the file path to check
+ *
+ * @return {@code true} if the file has been created, {@code false} if it already existed.
+ * @throws IOException if any IOException happend during this method's execution.
+ */
+ public static boolean createFileIfNotExist(Path filePath) throws IOException {
+ if(FileUtil.exist(filePath)){
+ Files.createFile(filePath);
+ return true;
+ }
+ return false;
+ }
+
+ public static boolean exist(Path path) {
+ return Files.exists(path);
+ }
+
+ public static boolean exist(String path) {
+ return Files.exists(Paths.get(path));
+ }
+}
diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/tools/entities/SaleContent.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/tools/entities/SaleContent.java
new file mode 100644
index 00000000..aeb2e7d8
--- /dev/null
+++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/tools/entities/SaleContent.java
@@ -0,0 +1,43 @@
+package com.pqt.server.tools.entities;
+
+import com.pqt.core.entities.product.Product;
+import com.pqt.core.entities.sale.Sale;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+public class SaleContent {
+ private Map content;
+
+ public SaleContent() {
+ content = new HashMap<>();
+ }
+
+ public SaleContent(Sale sale){
+ content = new HashMap<>(sale.getProducts());
+ }
+
+ public void addProduct(Product product, Integer amount){
+ if(content.containsKey(product)){
+ content.replace(product, content.get(product)+amount);
+ }else{
+ content.put(product, amount);
+ }
+ }
+
+ public Collection getProductList(){
+ return content.keySet();
+ }
+
+ public boolean contains(Product product){
+ return content.containsKey(product);
+ }
+
+ public Integer getProductAmount(Product product){
+ if(content.containsKey(product))
+ return content.get(product);
+
+ return null;
+ }
+}
diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/tools/io/ISerialFileManager.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/tools/io/ISerialFileManager.java
new file mode 100644
index 00000000..66c65e08
--- /dev/null
+++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/tools/io/ISerialFileManager.java
@@ -0,0 +1,12 @@
+package com.pqt.server.tools.io;
+
+import java.util.List;
+import java.util.Set;
+
+//TODO écrire javadoc
+public interface ISerialFileManager {
+ List loadListFromFile();
+ Set loadSetFromFile();
+ void saveListToFile(List list);
+ void saveSetToFile(Set set);
+}
diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/tools/io/SimpleSerialFileManager.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/tools/io/SimpleSerialFileManager.java
new file mode 100644
index 00000000..619532c6
--- /dev/null
+++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/tools/io/SimpleSerialFileManager.java
@@ -0,0 +1,108 @@
+package com.pqt.server.tools.io;
+
+import com.pqt.server.tools.FileUtil;
+
+import java.io.*;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.*;
+
+//TODO vérifier que le save écrase bien le contenu précédent du fichier
+public class SimpleSerialFileManager implements ISerialFileManager {
+
+ private Path filePath;
+ private Class clazz;
+
+ SimpleSerialFileManager(String filePath, Class clazz){
+ this(Paths.get(filePath), clazz);
+ }
+
+ SimpleSerialFileManager(Path filePath, Class clazz){
+ this.filePath = filePath;
+ this.clazz = clazz;
+ try{
+ FileUtil.createFileIfNotExist(filePath);
+ }catch (IOException e){
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public List loadListFromFile() {
+ try{
+ if(!FileUtil.createFileIfNotExist(filePath)){
+ List loadedEntries = new ArrayList<>();
+ fillCollection(loadedEntries);
+ return loadedEntries;
+ }
+ }catch(IOException | ClassNotFoundException e){
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ @Override
+ public Set loadSetFromFile() {
+ try{
+ if(!FileUtil.createFileIfNotExist(filePath)){
+ Set loadedEntries = new HashSet<>();
+ fillCollection(loadedEntries);
+ return loadedEntries;
+ }
+ }catch(IOException | ClassNotFoundException e){
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ private void fillCollection(Collection collection) throws IOException, ClassNotFoundException {
+ if(collection==null) return;
+ try(FileInputStream fis = new FileInputStream(filePath.toString());
+ ObjectInputStream ois = new ObjectInputStream(fis)){
+ boolean end = false;
+ do{
+ try{
+ Object obj = ois.readObject();
+ if(clazz.isInstance(obj)){
+ T ae = clazz.cast(obj);
+ collection.add(ae);
+ }
+ }catch (EOFException e){
+ end = true;
+ }
+ }while(!end);
+ }
+ }
+
+ @Override
+ public void saveListToFile(List list) {
+ save(list);
+ }
+
+ @Override
+ public void saveSetToFile(Set set) {
+ save(set);
+ }
+
+ private void save(Collection collection){
+ try{
+ FileUtil.createFileIfNotExist(filePath);
+ }catch (IOException e){
+ e.printStackTrace();
+ return;
+ }
+ try(FileOutputStream fos = new FileOutputStream(filePath.toString());
+ ObjectOutputStream oos = new ObjectOutputStream(fos)){
+
+ collection.forEach(p -> {
+ try {
+ oos.writeObject(p);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ });
+ }catch(IOException e){
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/tools/io/SimpleSerialFileManagerFactory.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/tools/io/SimpleSerialFileManagerFactory.java
new file mode 100644
index 00000000..2ebecb47
--- /dev/null
+++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/tools/io/SimpleSerialFileManagerFactory.java
@@ -0,0 +1,15 @@
+package com.pqt.server.tools.io;
+
+import java.nio.file.Path;
+
+public class SimpleSerialFileManagerFactory {
+ protected SimpleSerialFileManagerFactory(){}
+
+ public static ISerialFileManager getFileManager(Class clazz, String filePath){
+ return new SimpleSerialFileManager<>(filePath, clazz);
+ }
+
+ public static ISerialFileManager getFileManager(Class clazz, Path filePath){
+ return new SimpleSerialFileManager<>(filePath, clazz);
+ }
+}
diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/tools/security/IHashTool.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/tools/security/IHashTool.java
new file mode 100644
index 00000000..57cee938
--- /dev/null
+++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/tools/security/IHashTool.java
@@ -0,0 +1,5 @@
+package com.pqt.server.tools.security;
+
+public interface IHashTool {
+ String hashAndSalt(String str, String salt);
+}
diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/tools/security/MD5HashTool.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/tools/security/MD5HashTool.java
new file mode 100644
index 00000000..ffb2ebca
--- /dev/null
+++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/tools/security/MD5HashTool.java
@@ -0,0 +1,30 @@
+package com.pqt.server.tools.security;
+
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+public class MD5HashTool implements IHashTool{
+ @Override
+ public String hashAndSalt(String input, String salt) {
+ String md5 = null;
+
+ if(input == null || salt == null) return null;
+
+ try {
+ String str = salt+input;
+ //Create MessageDigest object for MD5
+ MessageDigest digest = MessageDigest.getInstance("MD5");
+
+ //Update input string in message digest
+ digest.update(str.getBytes(), 0, str.length());
+
+ //Converts message digest value in base 16 (hex)
+ md5 = new BigInteger(1, digest.digest()).toString(16);
+
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ }
+ return md5;
+ }
+}