diff --git a/Documentation/Diagrammes/diagrammes.asta b/Documentation/Diagrammes/diagrammes.asta index 06c8e081..b16a0a47 100644 Binary files a/Documentation/Diagrammes/diagrammes.asta and b/Documentation/Diagrammes/diagrammes.asta differ diff --git a/Documentation/Interactions_serveur-clients.ods b/Documentation/Interactions_serveur-clients.ods index 3757740b..7b99c96b 100644 Binary files a/Documentation/Interactions_serveur-clients.ods and b/Documentation/Interactions_serveur-clients.ods differ diff --git a/Workspace/.idea/artifacts/server_war.xml b/Workspace/.idea/artifacts/server_war.xml new file mode 100644 index 00000000..0f04e2c1 --- /dev/null +++ b/Workspace/.idea/artifacts/server_war.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/server/target + + + + + \ No newline at end of file diff --git a/Workspace/.idea/artifacts/server_war_exploded.xml b/Workspace/.idea/artifacts/server_war_exploded.xml new file mode 100644 index 00000000..ac4990b5 --- /dev/null +++ b/Workspace/.idea/artifacts/server_war_exploded.xml @@ -0,0 +1,23 @@ + + + $PROJECT_DIR$/server/target/server-1.0-SNAPSHOT + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Workspace/.idea/codeStyleSettings.xml b/Workspace/.idea/codeStyleSettings.xml new file mode 100644 index 00000000..5555dd26 --- /dev/null +++ b/Workspace/.idea/codeStyleSettings.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/Workspace/.idea/compiler.xml b/Workspace/.idea/compiler.xml index e4c04dd0..63c22bca 100644 --- a/Workspace/.idea/compiler.xml +++ b/Workspace/.idea/compiler.xml @@ -8,12 +8,13 @@ + - + diff --git a/Workspace/.idea/misc.xml b/Workspace/.idea/misc.xml index 82ba6c90..769b4698 100644 --- a/Workspace/.idea/misc.xml +++ b/Workspace/.idea/misc.xml @@ -1,15 +1,14 @@ + + + - diff --git a/Workspace/.idea/modules.xml b/Workspace/.idea/modules.xml index 464e4d5d..1796f0ba 100644 --- a/Workspace/.idea/modules.xml +++ b/Workspace/.idea/modules.xml @@ -5,6 +5,7 @@ + \ No newline at end of file diff --git a/Workspace/core/src/main/java/com/pqt/core/entities/members/Client.java b/Workspace/core/src/main/java/com/pqt/core/entities/members/Client.java index 2accc6d4..a4498012 100644 --- a/Workspace/core/src/main/java/com/pqt/core/entities/members/Client.java +++ b/Workspace/core/src/main/java/com/pqt/core/entities/members/Client.java @@ -1,6 +1,5 @@ package com.pqt.core.entities.members; -import java.io.Serializable; import java.util.Date; import java.util.Objects; @@ -10,22 +9,14 @@ import java.util.Objects; public class Client extends PqtMember{ private String address; - private Date lastUpdate; public Client() { super(-1, PqtMemberType.CLIENT); } - public Client(int id, String address) { + public Client(long id, String address) { super(id, PqtMemberType.CLIENT); this.address = address; - this.lastUpdate = new Date(); - } - - public Client(int id, String address, Date lastUpdate) { - super(id, PqtMemberType.CLIENT); - this.address = address; - this.lastUpdate = lastUpdate; } public String getAddress() { @@ -36,12 +27,19 @@ public class Client extends PqtMember{ this.address = address; } - public Date getLastUpdate() { - return lastUpdate; + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Client client = (Client) o; + + return address.equals(client.address) && id==client.id && type.equals(client.type); } - public void setLastUpdate(Date lastUpdate) { - this.lastUpdate = lastUpdate; + @Override + public int hashCode() { + return address.hashCode() + type.hashCode() + Integer.class.cast(id); } @Override diff --git a/Workspace/core/src/main/java/com/pqt/core/entities/members/DataServer.java b/Workspace/core/src/main/java/com/pqt/core/entities/members/DataServer.java index 1966cfc8..0ebb1a07 100644 --- a/Workspace/core/src/main/java/com/pqt/core/entities/members/DataServer.java +++ b/Workspace/core/src/main/java/com/pqt/core/entities/members/DataServer.java @@ -1,13 +1,11 @@ package com.pqt.core.entities.members; -import java.io.Serializable; import java.util.Date; import java.util.Objects; public class DataServer extends PqtMember{ private String address; - private Date lastUpdate; public DataServer() { super(-1, PqtMemberType.DATA_SERVER); @@ -16,15 +14,8 @@ public class DataServer extends PqtMember{ public DataServer(long id, String address) { super(id, PqtMemberType.DATA_SERVER); this.address = address; - this.lastUpdate = new Date(); } - public DataServer(long id, String address, Date lastUpdate) { - super(id, PqtMemberType.DATA_SERVER); - this.address = address; - this.lastUpdate = lastUpdate; - } - public String getAddress() { return address; } @@ -33,14 +24,6 @@ public class DataServer extends PqtMember{ this.address = address; } - public Date getLastUpdate() { - return lastUpdate; - } - - public void setLastUpdate(Date lastUpdate) { - this.lastUpdate = lastUpdate; - } - @Override public int hashCode() { return Objects.hash(super.hashCode(), address); diff --git a/Workspace/core/src/main/java/com/pqt/core/entities/members/PqtMember.java b/Workspace/core/src/main/java/com/pqt/core/entities/members/PqtMember.java index 5e16b5d5..e10090e5 100644 --- a/Workspace/core/src/main/java/com/pqt/core/entities/members/PqtMember.java +++ b/Workspace/core/src/main/java/com/pqt/core/entities/members/PqtMember.java @@ -7,8 +7,8 @@ import java.util.Objects; public class PqtMember implements ILoggable, Serializable { - private long id; - private PqtMemberType type; + protected long id; + protected PqtMemberType type; public PqtMember() { } diff --git a/Workspace/core/src/main/java/com/pqt/core/entities/messages/Message.java b/Workspace/core/src/main/java/com/pqt/core/entities/messages/Message.java index d58bf6b8..c3a62e45 100644 --- a/Workspace/core/src/main/java/com/pqt/core/entities/messages/Message.java +++ b/Workspace/core/src/main/java/com/pqt/core/entities/messages/Message.java @@ -1,6 +1,7 @@ package com.pqt.core.entities.messages; import com.pqt.core.entities.members.PqtMember; +import com.pqt.core.entities.user_account.Account; import java.util.*; @@ -9,15 +10,19 @@ public class Message { private Map fields; private MessageType type; private PqtMember emitter, receiver; + private Account user; + private Message replyTo; - public Message(MessageType type, PqtMember emitter, PqtMember receiver) { - this(type, emitter, receiver, null); + public Message(MessageType type, PqtMember emitter, PqtMember receiver, Account user, Message replyTo) { + this(type, emitter, receiver, user, replyTo, null); } - public Message(MessageType type, PqtMember emitter, PqtMember receiver, Map fields) { + public Message(MessageType type, PqtMember emitter, PqtMember receiver, Account user, Message replyTo, Map fields) { this.emitter = emitter; this.receiver = receiver; this.type = type; + this.user = user; + this.replyTo = replyTo; this.fields = new HashMap<>(); if(fields!=null) for(String key : fields.keySet()){ @@ -56,10 +61,10 @@ public class Message { @Override public boolean equals(Object obj) { - if(this == obj) + if (this == obj) return true; - if(!this.getClass().isInstance(obj)) + if (!this.getClass().isInstance(obj)) return false; Message other = Message.class.cast(obj); @@ -68,4 +73,12 @@ public class Message { && Objects.equals(this.receiver, other.receiver) && Objects.equals(this.type, other.type); } + + public Account getUser() { + return user; + } + + public Message getReplyTo() { + return replyTo; + } } diff --git a/Workspace/core/src/main/java/com/pqt/core/entities/messages/MessageType.java b/Workspace/core/src/main/java/com/pqt/core/entities/messages/MessageType.java index 1c2a3170..14d4a887 100644 --- a/Workspace/core/src/main/java/com/pqt/core/entities/messages/MessageType.java +++ b/Workspace/core/src/main/java/com/pqt/core/entities/messages/MessageType.java @@ -1,38 +1,36 @@ package com.pqt.core.entities.messages; public enum MessageType { - QUERY_CONNECT, - ACK_CONNECT, - ERR_CONNECT, - REF_CONNECT, + ERROR_QUERY, + REFUSED_QUERY, QUERY_SALE, ACK_SALE, - ERR_SALE, - REF_SALE, QUERY_REVERT_SALE, ACK_REVERT_SALE, - ERR_REVERT_SALE, - REF_REVERT_SALE, + + QUERY_LAST_SALES_LIST, + MSG_LAST_SALES_LIST, QUERY_STAT, MSG_STAT, - ERR_STAT, - REF_STAT, QUERY_STOCK, MSG_STOCK, - ERR_STOCK, - REF_STOCK, - QUERY_LOGIN, - ACK_LOGIN, - ERR_LOGIN, - REF_LOGIN, + QUERY_ACCOUNT_LIST, + MSG_ACCOUNT_LIST, + + QUERY_CONNECT_ACCOUNT, + ACK_CONNECT_ACCOUNT, QUERY_UPDATE, ACK_UPDATE, - ERR_UPDATE, - REF_UPDATE + + QUERY_PING, + ACK_PING, + + QUERY_CONFIG_LIST, + MSG_CONFIG_LIST } diff --git a/Workspace/core/src/main/java/com/pqt/core/entities/product/Product.java b/Workspace/core/src/main/java/com/pqt/core/entities/product/Product.java index 90136f82..ec5d5074 100644 --- a/Workspace/core/src/main/java/com/pqt/core/entities/product/Product.java +++ b/Workspace/core/src/main/java/com/pqt/core/entities/product/Product.java @@ -34,10 +34,14 @@ public class Product implements ILoggable, Serializable{ this.category = category; this.components = new ArrayList<>(); if(components!=null){ - this.components.addAll(components); + components.stream().forEach(p->this.components.add(new Product(p))); } } + public Product(Product p) { + this(p.id, p.name, p.amountRemaining, p.amountSold, p.sellable, p.price, p.components, p.category); + } + public long getId() { return id; } diff --git a/Workspace/core/src/main/java/com/pqt/core/entities/query/ConnectQuery.java b/Workspace/core/src/main/java/com/pqt/core/entities/query/ConnectQuery.java deleted file mode 100644 index f5caa308..00000000 --- a/Workspace/core/src/main/java/com/pqt/core/entities/query/ConnectQuery.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.pqt.core.entities.query; - -public class ConnectQuery extends SimpleQuery { - - private String serverAddress; - - public ConnectQuery(String serverAddress) { - super(QueryType.CONNECT); - this.serverAddress = serverAddress; - } - - public String getServerAddress() { - return serverAddress; - } - - public void setServerAddress(String serverAddress) { - this.serverAddress = serverAddress; - } -} diff --git a/Workspace/core/src/main/java/com/pqt/core/entities/query/LogQuery.java b/Workspace/core/src/main/java/com/pqt/core/entities/query/LogQuery.java deleted file mode 100644 index 106d5fd7..00000000 --- a/Workspace/core/src/main/java/com/pqt/core/entities/query/LogQuery.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.pqt.core.entities.query; - -import com.pqt.core.entities.user_account.Account; - -public class LogQuery extends SimpleQuery { - - private Account account; - - private boolean newDesiredState; - - public LogQuery(Account account, boolean newDesiredState) { - super(QueryType.LOG); - this.account = account; - this.newDesiredState = newDesiredState; - } - - public Account getAccount() { - return account; - } - - public void setAccount(Account account) { - this.account = account; - } - - public boolean isNewDesiredState() { - return newDesiredState; - } - - public void setNewDesiredState(boolean newDesiredState) { - this.newDesiredState = newDesiredState; - } -} diff --git a/Workspace/core/src/main/java/com/pqt/core/entities/sale/Sale.java b/Workspace/core/src/main/java/com/pqt/core/entities/sale/Sale.java index b474f9a7..127ca7c1 100644 --- a/Workspace/core/src/main/java/com/pqt/core/entities/sale/Sale.java +++ b/Workspace/core/src/main/java/com/pqt/core/entities/sale/Sale.java @@ -109,10 +109,10 @@ public class Sale implements ILoggable, Serializable{ @Override public boolean equals(Object obj) { - if(this == obj) + if (this == obj) return true; - if(!this.getClass().isInstance(obj)) + if (!this.getClass().isInstance(obj)) return false; Sale other = Sale.class.cast(obj); @@ -123,4 +123,18 @@ public class Sale implements ILoggable, Serializable{ && Objects.equals(this.orderedWith, other.orderedWith) && Objects.equals(this.type, other.type); } + + public double getTotalPrice() { + if(type.getPriceMultiplier()==0) + return 0; + return getTotalWorth()*type.getPriceMultiplier(); + } + + public double getTotalWorth(){ + double totalWorth = 0; + for(Product product : this.products.keySet()){ + totalWorth+=product.getPrice()*(double)this.products.get(product); + } + return totalWorth; + } } diff --git a/Workspace/core/src/main/java/com/pqt/core/entities/sale/SaleType.java b/Workspace/core/src/main/java/com/pqt/core/entities/sale/SaleType.java index 7dbf33c8..415738dc 100644 --- a/Workspace/core/src/main/java/com/pqt/core/entities/sale/SaleType.java +++ b/Workspace/core/src/main/java/com/pqt/core/entities/sale/SaleType.java @@ -4,5 +4,15 @@ package com.pqt.core.entities.sale; * Created by Notmoo on 18/07/2017. */ public enum SaleType { - CASH, BANK_CHECK, STUDENT_ASSOCIATION_ACCOUNT, OFFERED_GUEST, OFFERED_STAFF_MEMBER + CASH(1), BANK_CHECK(1), STUDENT_ASSOCIATION_ACCOUNT(1), OFFERED_GUEST(0), OFFERED_STAFF_MEMBER(0); + + private double priceMultiplier; + + SaleType(double priceMultiplier) { + this.priceMultiplier = priceMultiplier; + } + + public double getPriceMultiplier() { + return priceMultiplier; + } } diff --git a/Workspace/core/src/main/java/com/pqt/core/entities/server_config/ConfigFields.java b/Workspace/core/src/main/java/com/pqt/core/entities/server_config/ConfigFields.java new file mode 100644 index 00000000..a17de0cb --- /dev/null +++ b/Workspace/core/src/main/java/com/pqt/core/entities/server_config/ConfigFields.java @@ -0,0 +1,12 @@ +package com.pqt.core.entities.server_config; + +public enum ConfigFields { + ALLOW_SALE_COMMIT, + ALLOW_SALE_REVERT, + + ALLOW_STOCK_VIEW, + ALLOW_STOCK_UPDATE, + + ALLOW_ACCOUNT_CONNECT, + ALLOW_ACCOUNT_MODIFICATION +} diff --git a/Workspace/core/src/main/java/com/pqt/core/entities/server_config/ServerConfig.java b/Workspace/core/src/main/java/com/pqt/core/entities/server_config/ServerConfig.java new file mode 100644 index 00000000..1111a8df --- /dev/null +++ b/Workspace/core/src/main/java/com/pqt/core/entities/server_config/ServerConfig.java @@ -0,0 +1,53 @@ +package com.pqt.core.entities.server_config; + +import java.util.Arrays; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; + +public class ServerConfig { + + private Map fields; + + public ServerConfig() { + } + + public ServerConfig(Map fields) { + this.fields = fields; + } + + public ServerConfig(ConfigFields... configFields) { + fields = new HashMap<>(); + Arrays.stream(configFields).forEach(field->fields.put(field, true)); + + EnumSet.allOf(ConfigFields.class).stream().filter(field->!fields.containsKey(field)).forEach(field->fields.put(field, false)); + } + + public Map getFields() { + return fields; + } + + public void setFields(Map fields) { + this.fields = fields; + } + + public boolean isSupported(ConfigFields field){ + return fields.containsKey(field) && fields.get(field); + } + + public void switchFieldValue(ConfigFields field){ + if(fields.containsKey(field)){ + fields.replace(field, !fields.get(field)); + }else{ + fields.put(field, true); + } + } + + public boolean add(ConfigFields field, boolean value){ + if(!fields.containsKey(field)){ + fields.put(field, value); + return true; + } + return false; + } +} diff --git a/Workspace/core/src/main/java/com/pqt/core/entities/user_account/Account.java b/Workspace/core/src/main/java/com/pqt/core/entities/user_account/Account.java index b497886e..be090371 100644 --- a/Workspace/core/src/main/java/com/pqt/core/entities/user_account/Account.java +++ b/Workspace/core/src/main/java/com/pqt/core/entities/user_account/Account.java @@ -10,31 +10,19 @@ import java.util.Objects; * Created by Notmoo on 18/07/2017. */ public class Account implements ILoggable, Serializable { - private int id; private String username; - private String passwordHash; - private Date creationDate; + private String password; private AccountLevel permissionLevel; public Account() { } - public Account(int id, String username, String passwordHash, Date creationDate, AccountLevel permissionLevel) { - this.id = id; + public Account(String username, String password, AccountLevel permissionLevel) { this.username = username; - this.passwordHash = passwordHash; - this.creationDate = creationDate; + this.password = password; this.permissionLevel = permissionLevel; } - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - public String getUsername() { return username; } @@ -43,20 +31,12 @@ public class Account implements ILoggable, Serializable { this.username = username; } - public String getPasswordHash() { - return passwordHash; + public String getPassword() { + return password; } - public void setPasswordHash(String passwordHash) { - this.passwordHash = passwordHash; - } - - public Date getCreationDate() { - return creationDate; - } - - public void setCreationDate(Date creationDate) { - this.creationDate = creationDate; + public void setPassword(String password) { + this.password = password; } public AccountLevel getPermissionLevel() { diff --git a/Workspace/core/src/main/java/com/pqt/core/entities/user_account/AccountLevel.java b/Workspace/core/src/main/java/com/pqt/core/entities/user_account/AccountLevel.java index dd068a72..8875aecd 100644 --- a/Workspace/core/src/main/java/com/pqt/core/entities/user_account/AccountLevel.java +++ b/Workspace/core/src/main/java/com/pqt/core/entities/user_account/AccountLevel.java @@ -1,8 +1,21 @@ package com.pqt.core.entities.user_account; +import java.util.Arrays; +import java.util.Comparator; +import java.util.stream.Collector; +import java.util.stream.Collectors; + /** * Created by Notmoo on 18/07/2017. */ public enum AccountLevel { - GUEST, STAFF, WAITER, MASTER + LOWEST, GUEST, STAFF, WAITER, MASTER; + + public static AccountLevel getLowest(){ + return Arrays.stream(AccountLevel.values()).sorted(Comparator.naturalOrder()).findFirst().orElse(null); + } + + public static AccountLevel getHighest(){ + return Arrays.stream(AccountLevel.values()).sorted(Comparator.reverseOrder()).findFirst().orElse(null); + } } diff --git a/Workspace/pom.xml b/Workspace/pom.xml index b882372d..3bda6309 100644 --- a/Workspace/pom.xml +++ b/Workspace/pom.xml @@ -10,6 +10,7 @@ core client + server pom @@ -22,6 +23,13 @@ 1.8 + + org.apache.maven.plugins + maven-war-plugin + + false + + \ No newline at end of file diff --git a/Workspace/server/pom.xml b/Workspace/server/pom.xml new file mode 100644 index 00000000..53fc9c3a --- /dev/null +++ b/Workspace/server/pom.xml @@ -0,0 +1,37 @@ + + + + Main + com.pqt + 1.0-SNAPSHOT + + 4.0.0 + war + + server + + + + + javax + javaee-api + 7.0 + provided + + + com.pqt + core + 1.0-SNAPSHOT + + + + commons-io + commons-io + 2.4 + + + + + \ No newline at end of file diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/controller/IMessageHandler.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/controller/IMessageHandler.java new file mode 100644 index 00000000..37abc1ad --- /dev/null +++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/controller/IMessageHandler.java @@ -0,0 +1,28 @@ +package com.pqt.server.controller; + +import com.pqt.core.entities.messages.Message; + +/** + * Cette interface définit le type général correspondant à un élément censé traiter les objets de la classe {@link Message} arrivant au serveur. + * + * @author Guillaume "Cess" Prost + */ +public interface IMessageHandler { + /** + * Traite le message {@code message} passé en paramètre. Renvoie un message de réponse en tant que retour de méthode. + *

+ * Cette méthode doit toujours renvoyer un objet message autre que {@code null}. Un message de type + * {@link com.pqt.core.entities.messages.MessageType#ERROR_QUERY} ou de type + * {@link com.pqt.core.entities.messages.MessageType#REFUSED_QUERY} doit-être renvoyé si le message donné ne peut + * être pris en charge.
+ * Cela signifie aussi que cette méthode ne doit pas lever d'exception, et que ces dernières doivent être + * gérées en interne. + *

+ * Pour plus de détail sur les messages, leurs significations et les réponses attendues, voir la documentation du + * projet. + * @param message Objet de la classe {@link Message} à traiter. + * + * @return Objet de la classe {@link Message} correspondant à la réponse au paramètre {@code message}. + */ + Message handleMessage(Message message); +} diff --git a/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/controller/SimpleMessageHandler.java b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/controller/SimpleMessageHandler.java new file mode 100644 index 00000000..e60176bd --- /dev/null +++ b/Workspace/server/src/main/WEB-INF/classes/com/pqt/server/controller/SimpleMessageHandler.java @@ -0,0 +1,349 @@ +package com.pqt.server.controller; + +import com.pqt.core.communication.GSonMessageToolFactory; +import com.pqt.core.communication.IMessageToolFactory; +import com.pqt.core.entities.messages.Message; +import com.pqt.core.entities.messages.MessageType; +import com.pqt.core.entities.product.LightweightProduct; +import com.pqt.core.entities.product.Product; +import com.pqt.core.entities.product.ProductUpdate; +import com.pqt.core.entities.sale.Sale; +import com.pqt.core.entities.server_config.ServerConfig; +import com.pqt.core.entities.user_account.Account; +import com.pqt.core.entities.user_account.AccountLevel; +import com.pqt.server.exception.ServerQueryException; +import com.pqt.server.module.account.AccountService; +import com.pqt.server.module.sale.SaleService; +import com.pqt.server.module.state.ServerStateService; +import com.pqt.server.module.statistics.StatisticsService; +import com.pqt.server.module.stock.StockService; + +import java.util.*; + +//TODO Paramétrer les supports de query et leurs permissions via un meilleur système (config file, etc ...) +/** + * Implémentation de l'interface {@link IMessageHandler}. Cette classe définit le gestionnaire de message par défaut du + * serveur de données du projet PQT. + *

+ * Liste des requêtes supportées :
+ *

    + *
  • QUERY_STOCK (WAITER)
  • + *
  • QUERY_SALE (WAITER)
  • + *
  • QUER_STAT (WAITER)
  • + *
  • QUERY_UPDATE (MASTER)
  • + *
  • QUERY_ACCOUNT_LIST (NONE)
  • + *
  • QUERY_CONNECT_ACCOUNT (NONE)
  • + *
  • QUERY_PING (NONE)
  • + *
  • QUERY_CONFIG_LIST (NONE)
  • + *
+ *

+ * Liste des requêtes non-supportées :
+ *

    + *
  • QUERY_REVERT_SALE
  • + *
  • QUERY_LAST_SALES_LIST
  • + *
+ * @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; + } +}