Merge branch 'feature/#3_Développement_code_métier_serveur' into develop

This commit is contained in:
Notmoo 2017-08-10 10:39:16 +02:00
commit 2f15f382b5
54 changed files with 2324 additions and 145 deletions

View File

@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="war" name="server:war">
<output-path>$PROJECT_DIR$/server/target</output-path>
<root id="archive" name="server-1.0-SNAPSHOT.war">
<element id="artifact" artifact-name="server:war exploded" />
</root>
</artifact>
</component>

View File

@ -0,0 +1,23 @@
<component name="ArtifactManager">
<artifact type="exploded-war" name="server:war exploded">
<output-path>$PROJECT_DIR$/server/target/server-1.0-SNAPSHOT</output-path>
<root id="root">
<element id="directory" name="WEB-INF">
<element id="directory" name="classes">
<element id="module-output" name="server" />
</element>
<element id="directory" name="lib">
<element id="archive" name="core-1.0-SNAPSHOT.jar">
<element id="module-output" name="core" />
</element>
<element id="library" level="project" name="Maven: com.google.code.gson:gson:2.8.1" />
<element id="library" level="project" name="Maven: commons-io:commons-io:2.4" />
</element>
</element>
<element id="directory" name="META-INF">
<element id="file-copy" path="$PROJECT_DIR$/server/target/server-1.0-SNAPSHOT/META-INF/MANIFEST.MF" />
</element>
<element id="javaee-facet-resources" facet="server/web/Web" />
</root>
</artifact>
</component>

9
Workspace/.idea/codeStyleSettings.xml generated Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectCodeStyleSettingsManager">
<option name="PER_PROJECT_SETTINGS">
<value />
</option>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</component>
</project>

View File

@ -8,12 +8,13 @@
<outputRelativeToContentRoot value="true" /> <outputRelativeToContentRoot value="true" />
<module name="client" /> <module name="client" />
<module name="core" /> <module name="core" />
<module name="server" />
</profile> </profile>
</annotationProcessing> </annotationProcessing>
<bytecodeTargetLevel> <bytecodeTargetLevel>
<module name="client" target="1.8" /> <module name="client" target="1.8" />
<module name="core" target="1.8" /> <module name="core" target="1.8" />
<module name="server" target="1.5" /> <module name="server" target="1.8" />
<module name="Workspace" target="1.8" /> <module name="Workspace" target="1.8" />
</bytecodeTargetLevel> </bytecodeTargetLevel>
</component> </component>

View File

@ -1,15 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$/server" />
</component>
<component name="MavenProjectsManager"> <component name="MavenProjectsManager">
<option name="originalFiles"> <option name="originalFiles">
<list> <list>
<option value="$PROJECT_DIR$/pom.xml" /> <option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
<option name="ignoredFiles">
<set>
<option value="$PROJECT_DIR$/server/pom.xml" /> <option value="$PROJECT_DIR$/server/pom.xml" />
</set> </list>
</option> </option>
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">

View File

@ -5,6 +5,7 @@
<module fileurl="file://$PROJECT_DIR$/Workspace.iml" filepath="$PROJECT_DIR$/Workspace.iml" /> <module fileurl="file://$PROJECT_DIR$/Workspace.iml" filepath="$PROJECT_DIR$/Workspace.iml" />
<module fileurl="file://$PROJECT_DIR$/client/client.iml" filepath="$PROJECT_DIR$/client/client.iml" /> <module fileurl="file://$PROJECT_DIR$/client/client.iml" filepath="$PROJECT_DIR$/client/client.iml" />
<module fileurl="file://$PROJECT_DIR$/core/core.iml" filepath="$PROJECT_DIR$/core/core.iml" /> <module fileurl="file://$PROJECT_DIR$/core/core.iml" filepath="$PROJECT_DIR$/core/core.iml" />
<module fileurl="file://$PROJECT_DIR$/server/server.iml" filepath="$PROJECT_DIR$/server/server.iml" />
</modules> </modules>
</component> </component>
</project> </project>

View File

@ -1,6 +1,5 @@
package com.pqt.core.entities.members; package com.pqt.core.entities.members;
import java.io.Serializable;
import java.util.Date; import java.util.Date;
import java.util.Objects; import java.util.Objects;
@ -10,22 +9,14 @@ import java.util.Objects;
public class Client extends PqtMember{ public class Client extends PqtMember{
private String address; private String address;
private Date lastUpdate;
public Client() { public Client() {
super(-1, PqtMemberType.CLIENT); super(-1, PqtMemberType.CLIENT);
} }
public Client(int id, String address) { public Client(long id, String address) {
super(id, PqtMemberType.CLIENT); super(id, PqtMemberType.CLIENT);
this.address = address; 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() { public String getAddress() {
@ -36,12 +27,19 @@ public class Client extends PqtMember{
this.address = address; this.address = address;
} }
public Date getLastUpdate() { @Override
return lastUpdate; 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) { @Override
this.lastUpdate = lastUpdate; public int hashCode() {
return address.hashCode() + type.hashCode() + Integer.class.cast(id);
} }
@Override @Override

View File

@ -1,13 +1,11 @@
package com.pqt.core.entities.members; package com.pqt.core.entities.members;
import java.io.Serializable;
import java.util.Date; import java.util.Date;
import java.util.Objects; import java.util.Objects;
public class DataServer extends PqtMember{ public class DataServer extends PqtMember{
private String address; private String address;
private Date lastUpdate;
public DataServer() { public DataServer() {
super(-1, PqtMemberType.DATA_SERVER); super(-1, PqtMemberType.DATA_SERVER);
@ -16,15 +14,8 @@ public class DataServer extends PqtMember{
public DataServer(long id, String address) { public DataServer(long id, String address) {
super(id, PqtMemberType.DATA_SERVER); super(id, PqtMemberType.DATA_SERVER);
this.address = address; 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() { public String getAddress() {
return address; return address;
} }
@ -33,14 +24,6 @@ public class DataServer extends PqtMember{
this.address = address; this.address = address;
} }
public Date getLastUpdate() {
return lastUpdate;
}
public void setLastUpdate(Date lastUpdate) {
this.lastUpdate = lastUpdate;
}
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(super.hashCode(), address); return Objects.hash(super.hashCode(), address);

View File

@ -7,8 +7,8 @@ import java.util.Objects;
public class PqtMember implements ILoggable, Serializable { public class PqtMember implements ILoggable, Serializable {
private long id; protected long id;
private PqtMemberType type; protected PqtMemberType type;
public PqtMember() { public PqtMember() {
} }

View File

@ -1,6 +1,7 @@
package com.pqt.core.entities.messages; package com.pqt.core.entities.messages;
import com.pqt.core.entities.members.PqtMember; import com.pqt.core.entities.members.PqtMember;
import com.pqt.core.entities.user_account.Account;
import java.util.*; import java.util.*;
@ -9,15 +10,19 @@ public class Message {
private Map<String, String> fields; private Map<String, String> fields;
private MessageType type; private MessageType type;
private PqtMember emitter, receiver; private PqtMember emitter, receiver;
private Account user;
private Message replyTo;
public Message(MessageType type, PqtMember emitter, PqtMember receiver) { public Message(MessageType type, PqtMember emitter, PqtMember receiver, Account user, Message replyTo) {
this(type, emitter, receiver, null); this(type, emitter, receiver, user, replyTo, null);
} }
public Message(MessageType type, PqtMember emitter, PqtMember receiver, Map<String, String> fields) { public Message(MessageType type, PqtMember emitter, PqtMember receiver, Account user, Message replyTo, Map<String, String> fields) {
this.emitter = emitter; this.emitter = emitter;
this.receiver = receiver; this.receiver = receiver;
this.type = type; this.type = type;
this.user = user;
this.replyTo = replyTo;
this.fields = new HashMap<>(); this.fields = new HashMap<>();
if(fields!=null) if(fields!=null)
for(String key : fields.keySet()){ for(String key : fields.keySet()){
@ -56,10 +61,10 @@ public class Message {
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if(this == obj) if (this == obj)
return true; return true;
if(!this.getClass().isInstance(obj)) if (!this.getClass().isInstance(obj))
return false; return false;
Message other = Message.class.cast(obj); Message other = Message.class.cast(obj);
@ -68,4 +73,12 @@ public class Message {
&& Objects.equals(this.receiver, other.receiver) && Objects.equals(this.receiver, other.receiver)
&& Objects.equals(this.type, other.type); && Objects.equals(this.type, other.type);
} }
public Account getUser() {
return user;
}
public Message getReplyTo() {
return replyTo;
}
} }

View File

@ -1,38 +1,36 @@
package com.pqt.core.entities.messages; package com.pqt.core.entities.messages;
public enum MessageType { public enum MessageType {
QUERY_CONNECT, ERROR_QUERY,
ACK_CONNECT, REFUSED_QUERY,
ERR_CONNECT,
REF_CONNECT,
QUERY_SALE, QUERY_SALE,
ACK_SALE, ACK_SALE,
ERR_SALE,
REF_SALE,
QUERY_REVERT_SALE, QUERY_REVERT_SALE,
ACK_REVERT_SALE, ACK_REVERT_SALE,
ERR_REVERT_SALE,
REF_REVERT_SALE, QUERY_LAST_SALES_LIST,
MSG_LAST_SALES_LIST,
QUERY_STAT, QUERY_STAT,
MSG_STAT, MSG_STAT,
ERR_STAT,
REF_STAT,
QUERY_STOCK, QUERY_STOCK,
MSG_STOCK, MSG_STOCK,
ERR_STOCK,
REF_STOCK,
QUERY_LOGIN, QUERY_ACCOUNT_LIST,
ACK_LOGIN, MSG_ACCOUNT_LIST,
ERR_LOGIN,
REF_LOGIN, QUERY_CONNECT_ACCOUNT,
ACK_CONNECT_ACCOUNT,
QUERY_UPDATE, QUERY_UPDATE,
ACK_UPDATE, ACK_UPDATE,
ERR_UPDATE,
REF_UPDATE QUERY_PING,
ACK_PING,
QUERY_CONFIG_LIST,
MSG_CONFIG_LIST
} }

View File

@ -34,10 +34,14 @@ public class Product implements ILoggable, Serializable{
this.category = category; this.category = category;
this.components = new ArrayList<>(); this.components = new ArrayList<>();
if(components!=null){ 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() { public long getId() {
return id; return id;
} }

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -109,10 +109,10 @@ public class Sale implements ILoggable, Serializable{
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if(this == obj) if (this == obj)
return true; return true;
if(!this.getClass().isInstance(obj)) if (!this.getClass().isInstance(obj))
return false; return false;
Sale other = Sale.class.cast(obj); 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.orderedWith, other.orderedWith)
&& Objects.equals(this.type, other.type); && 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;
}
} }

View File

@ -4,5 +4,15 @@ package com.pqt.core.entities.sale;
* Created by Notmoo on 18/07/2017. * Created by Notmoo on 18/07/2017.
*/ */
public enum SaleType { 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;
}
} }

View File

@ -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
}

View File

@ -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<ConfigFields, Boolean> fields;
public ServerConfig() {
}
public ServerConfig(Map<ConfigFields, Boolean> 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<ConfigFields, Boolean> getFields() {
return fields;
}
public void setFields(Map<ConfigFields, Boolean> 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;
}
}

View File

@ -10,31 +10,19 @@ import java.util.Objects;
* Created by Notmoo on 18/07/2017. * Created by Notmoo on 18/07/2017.
*/ */
public class Account implements ILoggable, Serializable { public class Account implements ILoggable, Serializable {
private int id;
private String username; private String username;
private String passwordHash; private String password;
private Date creationDate;
private AccountLevel permissionLevel; private AccountLevel permissionLevel;
public Account() { public Account() {
} }
public Account(int id, String username, String passwordHash, Date creationDate, AccountLevel permissionLevel) { public Account(String username, String password, AccountLevel permissionLevel) {
this.id = id;
this.username = username; this.username = username;
this.passwordHash = passwordHash; this.password = password;
this.creationDate = creationDate;
this.permissionLevel = permissionLevel; this.permissionLevel = permissionLevel;
} }
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUsername() { public String getUsername() {
return username; return username;
} }
@ -43,20 +31,12 @@ public class Account implements ILoggable, Serializable {
this.username = username; this.username = username;
} }
public String getPasswordHash() { public String getPassword() {
return passwordHash; return password;
} }
public void setPasswordHash(String passwordHash) { public void setPassword(String password) {
this.passwordHash = passwordHash; this.password = password;
}
public Date getCreationDate() {
return creationDate;
}
public void setCreationDate(Date creationDate) {
this.creationDate = creationDate;
} }
public AccountLevel getPermissionLevel() { public AccountLevel getPermissionLevel() {

View File

@ -1,8 +1,21 @@
package com.pqt.core.entities.user_account; 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. * Created by Notmoo on 18/07/2017.
*/ */
public enum AccountLevel { 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);
}
} }

View File

@ -10,6 +10,7 @@
<modules> <modules>
<module>core</module> <module>core</module>
<module>client</module> <module>client</module>
<module>server</module>
</modules> </modules>
<packaging>pom</packaging> <packaging>pom</packaging>
@ -22,6 +23,13 @@
<target>1.8</target> <target>1.8</target>
</configuration> </configuration>
</plugin> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins> </plugins>
</build> </build>
</project> </project>

37
Workspace/server/pom.xml Normal file
View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>Main</artifactId>
<groupId>com.pqt</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>war</packaging>
<artifactId>server</artifactId>
<dependencies>
<!-- https://mvnrepository.com/artifact/javax/javaee-api -->
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>7.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.pqt</groupId>
<artifactId>core</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
</dependencies>
</project>

View File

@ -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.
* <p/>
* Cette méthode doit toujours renvoyer un objet message <b>autre que {@code null}</b>. 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.<br/>
* Cela signifie aussi que cette méthode <b>ne doit pas lever d'exception</b>, et que ces dernières doivent être
* gérées en interne.
* <p/>
* 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);
}

View File

@ -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.
* <p/>
* Liste des requêtes supportées :<br/>
* <ul>
* <li>QUERY_STOCK (WAITER)</li>
* <li>QUERY_SALE (WAITER)</li>
* <li>QUER_STAT (WAITER)</li>
* <li>QUERY_UPDATE (MASTER)</li>
* <li>QUERY_ACCOUNT_LIST (NONE)</li>
* <li>QUERY_CONNECT_ACCOUNT (NONE)</li>
* <li>QUERY_PING (NONE)</li>
* <li>QUERY_CONFIG_LIST (NONE)</li>
* </ul>
* <p/>
* Liste des requêtes non-supportées :<br/>
* <ul>
* <li>QUERY_REVERT_SALE</li>
* <li>QUERY_LAST_SALES_LIST</li>
* </ul>
* @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<String, String> 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<String, String> 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<String, String> 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<ProductUpdate> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<MessageTypeEntry> 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.
* <p/>
* @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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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é.
* <p/>
* 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. <b>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.</b>
* <p/>
* Pour cette méthode, seul le nom d'utilisateur est pris en compte pour établir une correspondance.
* <p/>
* Dans le cas 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 ê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}).
* <p/>
* 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. <b>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.</b>
* <p/>
* 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.
* <p/>
* Une fois la correspondance effectuée, une tentative de changement d'état sera faite pour le compte correspondant.
* Cette tentative peut échouer si :
* <ul>
* <li>Le compte est déjà dans l'état désiré</li>
* <li>Le mot de passe ne correspond pas (uniquement pour une connexion)</li>
* </ul>
* 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.
* <p/>
* 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. <b>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.</b>
* <p/>
* 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 ê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.
* <p/>
* 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. <b>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.</b>
* <p/>
* 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 être établie entre {@code account} et un compte utilisateur de la base de donnée, et {@code null} si aucune
* correspondance n'a ê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}. <b>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}.</b>
* @return Une liste d'objet {@link Account} représentant les différents comptes utilisateurs existant dans la base
* de données.
*/
public List<Account> getAccountList(){
return dao.getAccountList();
}
}

View File

@ -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.
* <p/>
* 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.
* <p/>
* 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<AccountEntry> accountEntries;
private Set<AccountEntry> connectedAccount;
private IHashTool hashTool;
private ISerialFileManager<AccountEntry> 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<AccountEntry> 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<Account> 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.
* <p/>
* <b>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.</b>
*/
private void loadFromFile(){
this.accountEntries = new HashSet<>(fileManager.loadSetFromFile());
//TODO faire check des comptes au lieu de tout déconnecter?
this.connectedAccount.clear();
}
}

View File

@ -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}.
* <p/>
* 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<Account> getAccountList();
}

View File

@ -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;
}
}

View File

@ -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.
* <p/>
* 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.
* <p/>
* 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<ClientEntry> 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 ê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<Client> 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());
}
}
}

View File

@ -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}.
* <p/>
* 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.
* <p/>
* 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)}. <b>Le support de cette fonctionnalité
* est optionnel</b>.
*
* @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);
}

View File

@ -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. <b>Cette implémentation ne supporte pas le rollback de commandes</b>.
* <p/>.
* 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.
* <p/>
* 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 <b>positifs et non-nuls</b>.
*/
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<Product> 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.
* <p/>
* Different reasons why this method may not find any id :<br/>
* - file does not exist<br/>
* - file is empty<br/>
* - file does not respect the expected syntax for writing sales' data<br/>
* <p/>
* 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("<dd/ww/yyyy - HH:mm:ss>");
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);
}
}

View File

@ -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.
* <p/>
* 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)}.
* <p/>
* 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.
* <p/>
* 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.
* <p/>
* 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);
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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) {
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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<LightweightProduct> 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;
}
}

View File

@ -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.
* <p/>
* 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<Product> fileManager;
private long nextProductId;
private Random random;
private Map<Long, Product> 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<Product> getProductList() {
return copyOfProductList();
}
private List<Product> copyOfProductList() {
List<Product> 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<Long, Product> 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()));
}
}

View File

@ -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}.
* <p/>
* 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<Product> 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é.
* <p/>
* <b>Si {@code id} ne correspond à aucun produit, aucune modification n'est effectué. Cela signifie que
* {@code product} n'est pas ajouté à la BDD.</b>
* @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}.
* <p/>
* @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;
}

View File

@ -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).
* <p/>
* <b>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é</b>
*
* @see Product
* @see ProductUpdate
* @see SaleContent
* @author Guillaume "Cess" Prost
*/
public class StockService {
private IStockDao dao;
public StockService() {
dao = new FileStockDao();
}
public List<Product> 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<ProductUpdate> 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);
}
}

View File

@ -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));
}
}
}

View File

@ -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));
}
}

View File

@ -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<Product, Integer> 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<Product> 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;
}
}

View File

@ -0,0 +1,12 @@
package com.pqt.server.tools.io;
import java.util.List;
import java.util.Set;
//TODO écrire javadoc
public interface ISerialFileManager<T> {
List<T> loadListFromFile();
Set<T> loadSetFromFile();
void saveListToFile(List<T> list);
void saveSetToFile(Set<T> set);
}

View File

@ -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<T> implements ISerialFileManager<T> {
private Path filePath;
private Class<T> clazz;
SimpleSerialFileManager(String filePath, Class<T> clazz){
this(Paths.get(filePath), clazz);
}
SimpleSerialFileManager(Path filePath, Class<T> clazz){
this.filePath = filePath;
this.clazz = clazz;
try{
FileUtil.createFileIfNotExist(filePath);
}catch (IOException e){
e.printStackTrace();
}
}
@Override
public List<T> loadListFromFile() {
try{
if(!FileUtil.createFileIfNotExist(filePath)){
List<T> loadedEntries = new ArrayList<>();
fillCollection(loadedEntries);
return loadedEntries;
}
}catch(IOException | ClassNotFoundException e){
e.printStackTrace();
}
return null;
}
@Override
public Set<T> loadSetFromFile() {
try{
if(!FileUtil.createFileIfNotExist(filePath)){
Set<T> loadedEntries = new HashSet<>();
fillCollection(loadedEntries);
return loadedEntries;
}
}catch(IOException | ClassNotFoundException e){
e.printStackTrace();
}
return null;
}
private void fillCollection(Collection<T> 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<T> list) {
save(list);
}
@Override
public void saveSetToFile(Set<T> set) {
save(set);
}
private void save(Collection<T> 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();
}
}
}

View File

@ -0,0 +1,15 @@
package com.pqt.server.tools.io;
import java.nio.file.Path;
public class SimpleSerialFileManagerFactory {
protected SimpleSerialFileManagerFactory(){}
public static <T> ISerialFileManager<T> getFileManager(Class<T> clazz, String filePath){
return new SimpleSerialFileManager<>(filePath, clazz);
}
public static <T> ISerialFileManager<T> getFileManager(Class<T> clazz, Path filePath){
return new SimpleSerialFileManager<>(filePath, clazz);
}
}

View File

@ -0,0 +1,5 @@
package com.pqt.server.tools.security;
public interface IHashTool {
String hashAndSalt(String str, String salt);
}

View File

@ -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;
}
}