Sixième article sur LoFiDroX: les services.
Objectif des services#
Les services permettent d’établir le lien entre les contrôleurs et les repositories. Pour rappel, un contrôleur est responsable de gérer les requêtes et les réponses, le repository est lui responsable de l’accès aux données.
Le service est alors chargé d’implémenter la logique de chaque traitement.
Service FileDescriptorService#
Ce service gère le stockage sur disque et les liens avec les utilisateurs.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
| package ch.gobothegeek.lofidrox.services;
import ch.gobothegeek.lofidrox.exceptions.LfdException;
import ch.gobothegeek.lofidrox.model.entities.FileDescriptor;
import ch.gobothegeek.lofidrox.model.entities.FileRecipient;
import ch.gobothegeek.lofidrox.model.json.file.JsonFileDownload;
import ch.gobothegeek.lofidrox.repositories.FileDescriptorRepository;
import ch.gobothegeek.lofidrox.repositories.FileRecipientRepository;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.FileUtils;
import org.apache.deltaspike.core.api.config.ConfigResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.transaction.Transactional;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
@ApplicationScoped
@Transactional
public class FileDescriptorService {
private static final Logger logger = LoggerFactory.getLogger(FileDescriptorService.class);
private static final String BASE64_DATA = "data:";
private static final String BASE64_TAG = ";base64,";
@Inject private FileDescriptorRepository fileDescriptorRepository;
@Inject private FileRecipientService fileRecipientService;
@Inject private FileRecipientRepository fileRecipientRepository;
@Transactional(Transactional.TxType.REQUIRED)
public FileDescriptor addFile(String filename, String userSrc) {
if ((null != filename) && (null != userSrc)) {
return this.fileDescriptorRepository.createFile(filename, null, userSrc, null);
}
return null;
}
// upload a file to multiples receivers
@Transactional(Transactional.TxType.REQUIRED)
public boolean uploadFileToUsers(String[] users, String filename, String base64Data, String userPost) {
FileDescriptor file;
String folder;
File fileOut;
String base64Head;
byte[] content;
if ((null != users) && (0 < users.length) && (null != filename) && (null != base64Data)) {
// prepare upload: add file name to database
file = this.addFile(filename, userPost);
try {
// compute destination folder name
folder = ConfigResolver.getPropertyValue("application.storage.base");
folder = folder.replace("{source}", userPost);
// compute filename
fileOut = new File(folder, UUID.randomUUID().toString());
// ensure this path exists
FileUtils.forceMkdir(new File(folder));
// extract base64 header before decoding
base64Head = base64Data.substring(BASE64_DATA.length(), base64Data.indexOf(BASE64_TAG));
// get Base64 content
content = Base64.decodeBase64(base64Data.substring(base64Data.indexOf(BASE64_TAG) + BASE64_TAG.length()));
FileUtils.writeByteArrayToFile(fileOut, content);
// update file in DB
if (!this.updateFilePathAndType(file.getId(), fileOut.getAbsolutePath(), base64Head)) {
logger.error("Unable to update file [" + file.getId() + "]");
throw new LfdException("Unable to update file [" + file.getId() + "]");
}
return (null != this.fileRecipientService.addRecipients(file.getId(), users));
} catch (Exception e) {
System.out.println("Unable to write file [" + filename + "]");
e.printStackTrace();
// delete file from DB
this.deleteFile(file);
}
}
logger.error("Missing parameters to upload file [users: " + ((null != users) && (0 < users.length)) + "], [filename=" + (null != filename) + "], " +
"[data=" + (null != base64Data) + "]");
return false;
}
@Transactional(Transactional.TxType.REQUIRED)
public Boolean updateFilePathAndType(Integer fileId, String path, String type) {
FileDescriptor file;
if ((null != fileId) && (null != path)) {
file = this.fileDescriptorRepository.updateFilePathAndType(fileId, path, type);
return (null != file);
}
return Boolean.FALSE;
}
@Transactional(Transactional.TxType.REQUIRED)
public void deleteFile(FileDescriptor file) {
if (null != file) {
FileUtils.deleteQuietly(new File(file.getPath()));
this.fileDescriptorRepository.deleteFile(file.getId());
}
}
@Transactional(Transactional.TxType.REQUIRED)
public JsonFileDownload downloadFile(Integer fileId, String user) {
FileRecipient rec;
JsonFileDownload jsonDL;
StringBuilder content;
jsonDL = new JsonFileDownload();
// start by finding file in database
rec = this.fileRecipientService.findFileByIdAndOwner(fileId, user);
if ((null != rec) && null != rec.getFile()) {
// mark file as read
this.fileRecipientService.markRead(rec);
// add filename (without path)
jsonDL.setFilename(rec.getFile().getName());
try {
content = new StringBuilder();
// add Base64 header
content.append(BASE64_DATA);
content.append(rec.getFile().getDataType());
content.append(BASE64_TAG);
// add Base64 file content
content.append(Base64.encodeBase64String(FileUtils.readFileToByteArray(new File(rec.getFile().getPath()))));
jsonDL.setType(rec.getFile().getDataType());
jsonDL.setData(content.toString());
return jsonDL;
} catch (IOException e) {
logger.error("Unable to read file [" + rec.getFile().getPath() + "]", e);
e.printStackTrace();
}
} else {
logger.error("Unable to fetch file with id [" + fileId + "] for user [" + user + "]");
}
return null;
}
@Transactional(Transactional.TxType.REQUIRED)
public int deleteFiles(List<Integer> files, String user) {
List<FileDescriptor> filesDesc;
List< FileRecipient> recips;
if ((null != files) && (0 < files.size()) && (null != user)) {
// find files by id
filesDesc = this.fileDescriptorRepository.findByIdIn(files);
if (filesDesc.size() == files.size()) { // we got all required files
for (FileDescriptor fileDesc : filesDesc) {
// break link between recipient and file
this.fileRecipientRepository.deleteLink(user, fileDesc.getId());
// check if a user hold this file
recips = this.fileRecipientRepository.findAnyByFileId(fileDesc.getId());
if ((null == recips) || (0 == recips.size())) {
// no one hold the file, remove it
this.deleteFile(fileDesc);
}
}
return filesDesc.size();
}
}
return -1;
}
}
|
Méthode | But |
---|
addFile | Etablit le lien entre un fichier et un utilisateur. Afin d’économiser de l’espace disque, chaque fichier est stocké de manière unique puis mis à disposition des destinataires. |
uploadFileToUsers | Créer un dossier de dépôt pour l’émetteur (si besoin) puis transformer le contenu Base64 en fichier et le stocker sur disque et finalement mettre à disposition des destinataires le fichier stocké (méthode FileRecipientService.addRecipients). |
updateFilePathAndType | Stocker en base l’identifiant du fichier, son chemin d’accès (relatif) et son type MIME (utile pour déclencher un téléchargement correct). |
deleteFile | Efface le fichier désigné. |
downloadFile | lit le fichier désigné, le transforme en Base64 et prépare la réponse JSON pour le navigateur |
deleteFiles | A partir des identifiants des fichiers, trouve les fichiers en base de données, supprimer les liens vers l’utilisateur ayant demandé la suppression et finalement effacer le fichier, pour autant qu’il n’ait plus de destinataire. |
Service FileRecipientService#
Ce service a pour but de gérer les destinataires de fichiers.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
| package ch.gobothegeek.lofidrox.services;
import ch.gobothegeek.lofidrox.model.entities.FileRecipient;
import ch.gobothegeek.lofidrox.repositories.FileDescriptorRepository;
import ch.gobothegeek.lofidrox.repositories.FileRecipientRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.transaction.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@ApplicationScoped
@Transactional
public class FileRecipientService {
private static final Logger logger = LoggerFactory.getLogger(FileRecipientService.class);
@Inject private FileDescriptorRepository fileDescriptorRepository;
@Inject private FileRecipientRepository fileRecipientRepository;
// add recipients for a file descriptor
@Transactional(Transactional.TxType.REQUIRED)
public List<FileRecipient> addRecipients(Integer fileId, String[] users) {
List<FileRecipient> recipients;
FileRecipient rec;
if ((null != fileId) && (null != users) && (0 < users.length)) {
recipients = new ArrayList<>();
for (String userTo : users) {
rec = this.fileRecipientRepository.createLink(fileId, userTo, false);
recipients.add(rec);
}
return recipients;
}
logger.error("Missing parameters to add recipients [fileId=" + (null != fileId) + "], [users=" + ((null != users) && (0 < users.length)) + "]");
return null;
}
// return list of files for a specific user
@Transactional(Transactional.TxType.REQUIRED)
public List<FileRecipient> listFilesForUser(String owner) {
return this.fileRecipientRepository.listFilesForUser(owner);
}
@Transactional(Transactional.TxType.REQUIRED)
public FileRecipient findFileByIdAndOwner(Integer id, String owner) {
Optional<FileRecipient> file;
file = this.fileRecipientRepository.findFileByIdAndUserTo(id, owner);
return (file.orElse(null));
}
@Transactional(Transactional.TxType.REQUIRED)
public FileRecipient markRead(FileRecipient recipient) {
recipient = this.fileRecipientRepository.markRead(recipient);
return recipient;
}
}
|
Méthode | But |
---|
addRecipients | Génére les liens entre un fichier donné et les utilisateurs concernés. |
listFilesForUser | Retourne la liste des fichiers reçus par un utilisateur. |
findFileByIdAndOwner | Permet de vérifier si un fichier donné est bien lié à l’utilisateur indiqué. |
markRead | Note que le fichier a été téléchargé par l’utilisateur. |
Service RequestService#
Ce service possède un seul objectif: trouver l’adresse IP de l’appelant via la méthode getRemoteAddress
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| package ch.gobothegeek.lofidrox.services;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
@RequestScoped
public class RequestService {
private final Logger logger = LoggerFactory.getLogger(RequestService.class);
@Inject private HttpServletRequest request;
public String getRemoteAddress() {
return request.getRemoteAddr();
}
}
|
Service SessionService#
Ici on va gérer les sessions utilisateurs: création, récupération et suppression.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
| package ch.gobothegeek.lofidrox.services;
import ch.gobothegeek.lofidrox.exceptions.LfdSessionException;
import ch.gobothegeek.lofidrox.security.sessions.LfdSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.enterprise.context.ApplicationScoped;
import java.util.HashMap;
@ApplicationScoped
public class SessionService {
private static final Logger logger = LoggerFactory.getLogger(SessionService.class);
private HashMap<String, LfdSession> sessions;
public SessionService() {
this.sessions = new HashMap<>();
}
public void create(String username) {
if (!this.sessions.containsKey(username)) {
this.sessions.put(username, new LfdSession());
}
}
public void add(String username, String key, Object obj) throws LfdSessionException {
if (this.sessions.containsKey(username)) {
this.sessions.get(username).add(key, obj);
return;
}
logger.error("No session for [" + username + "]");
throw new LfdSessionException("No session for this user (" + username + ")");
}
public Object get(String username, String key) throws LfdSessionException {
if (this.sessions.containsKey(username)) {
return this.sessions.get(username).get(key);
}
logger.error("No session for [" + username + "]");
throw new LfdSessionException("No session for this user (" + username + ")");
}
public void delete(String username) {
if (this.sessions.containsKey(username)) {
this.sessions.get(username).destroy();
this.sessions.remove(username);
}
}
public boolean hasSession(String username) {
return this.sessions.containsKey(username);
}
}
|
Méthode | But |
---|
create | Créer une nouvelle session pour l’utilisateur, s’il n’en possède pas encore. |
add | Ajoute un objet à la session de l’utilisateur. |
get | Retourne un objet de la session de l’utilisateur. |
delete | Supprime la session de l’utilisateur. |
hasSession | Indique si l’utilisateur possède une session. |
Service UserService#
Ce service permet le pilotage des utilisateurs: enregistrement, connexion, déconnexion.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
| package ch.gobothegeek.lofidrox.services;
import ch.gobothegeek.lofidrox.exceptions.LfdException;
import ch.gobothegeek.lofidrox.model.entities.User;
import ch.gobothegeek.lofidrox.repositories.UserRepository;
import ch.gobothegeek.lofidrox.security.LfdUserBean;
import ch.gobothegeek.lofidrox.utils.GtgPasswordUtils;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.apache.deltaspike.core.api.config.ConfigResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.transaction.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@ApplicationScoped
@Transactional
public class UserService {
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
@Inject private UserRepository userRepository;
@Inject private SessionService sessionService;
@Inject private LfdUserBean lfdUserBean;
@Transactional(Transactional.TxType.REQUIRED)
public Boolean loginUser(String username, String pwd) {
Optional<User> user;
String encPwd;
try {
encPwd = GtgPasswordUtils.hashPassword(pwd, Hex.decodeHex(ConfigResolver.getPropertyValue("application.password.salt")),
ConfigResolver.getPropertyValue("application.password.algorithm"),
Integer.parseInt(ConfigResolver.getPropertyValue("application.password.iteration")),
Integer.parseInt(ConfigResolver.getPropertyValue("application.password.keylength")));
user = this.userRepository.findByUsernameAndPwd(username, encPwd);
if (user.isPresent()) {
this.sessionService.create(username);
//this.lfdUserLoggedEvent.fire(new LfdUserLoggedEvent());
this.lfdUserBean.setUser(new ch.gobothegeek.lofidrox.security.sessions.LfdUser(username));
return Boolean.TRUE;
}
logger.error("User not found or wrong password [" + username + "]");
return Boolean.FALSE;
} catch (DecoderException e) {
System.out.println("Unable to decode salt [" + ConfigResolver.getPropertyValue("application.password.salt") + "]");
e.printStackTrace();
logger.error("Unable to decode salt [" + ConfigResolver.getPropertyValue("application.password.salt") + "]", e);
}
return Boolean.FALSE;
}
@Transactional(Transactional.TxType.REQUIRED)
public User createUser(String username, String pwd) throws LfdException {
Optional<User> userReg;
String encPwd;
if (null != pwd) {
userReg = this.userRepository.findByUsername(username);
if (userReg.isEmpty()) {
try {
encPwd = GtgPasswordUtils.hashPassword(pwd, Hex.decodeHex(ConfigResolver.getPropertyValue("application.password.salt")), ConfigResolver.getPropertyValue("application.password.algorithm"), Integer.parseInt(ConfigResolver.getPropertyValue("application.password.iteration")), Integer.parseInt(ConfigResolver.getPropertyValue("application.password.keylength")));
return this.userRepository.createUser(username, encPwd);
} catch (DecoderException e) {
logger.error("Unable to decode salt [" + ConfigResolver.getPropertyValue("application.password.salt") + "]", e);
throw new LfdException("Unable to hash password");
}
}
}
return null;
}
@Transactional(Transactional.TxType.REQUIRED)
public void logoffUser(String username) {
if (this.sessionService.hasSession(username)) {
this.sessionService.delete(username);
}
}
@Transactional(Transactional.TxType.REQUIRED)
public List<User> listUsers() {
return this.userRepository.findAllOrderByUsernameAsc();
}
@Transactional(Transactional.TxType.REQUIRED)
public List<String> listUsernames() {
List<User> users = this.userRepository.findAllOrderByUsernameAsc();
List<String> usernames;
if (null != users) {
usernames = new ArrayList<>();
if (0 < users.size()) {
for (User usr : users) { usernames.add(usr.getUsername()); }
}
return usernames;
}
return null;
}
}
|
Méthode | But |
---|
loginUser | Réalise la connexion de l’utilisateur |
createUser | Enregistre un nouvel utilisateur |
logoffUser | Déconnecte l’utilisateur |
listUsers | Retourne la liste des utilisateurs |
listUsernames | Retourne la liste des noms des utilisateurs |
Conclusion#
L’écriture des services avec Apache Deltaspike est intuitive et leur injection est vraiment simple (pas de déclaration XML comme Spring par exemple).
Code source#
Comme annoncé dans l’article Fuyez GitHub, le code n’est plus disponible sur GitHub. L’intégralité du code est disponible sur mon CodeBerg: https://codeberg.org/GoboTheGeek/LoFiDroX