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.

    
  1package ch.gobothegeek.lofidrox.services;
  2
  3import ch.gobothegeek.lofidrox.exceptions.LfdException;
  4import ch.gobothegeek.lofidrox.model.entities.FileDescriptor;
  5import ch.gobothegeek.lofidrox.model.entities.FileRecipient;
  6import ch.gobothegeek.lofidrox.model.json.file.JsonFileDownload;
  7import ch.gobothegeek.lofidrox.repositories.FileDescriptorRepository;
  8import ch.gobothegeek.lofidrox.repositories.FileRecipientRepository;
  9import org.apache.commons.codec.binary.Base64;
 10import org.apache.commons.io.FileUtils;
 11import org.apache.deltaspike.core.api.config.ConfigResolver;
 12import org.slf4j.Logger;
 13import org.slf4j.LoggerFactory;
 14import javax.enterprise.context.ApplicationScoped;
 15import javax.inject.Inject;
 16import javax.transaction.Transactional;
 17import java.io.File;
 18import java.io.IOException;
 19import java.util.List;
 20import java.util.UUID;
 21
 22@ApplicationScoped
 23@Transactional
 24public class FileDescriptorService {
 25	private static final Logger logger = LoggerFactory.getLogger(FileDescriptorService.class);
 26	private static final String BASE64_DATA = "data:";
 27	private static final String BASE64_TAG = ";base64,";
 28
 29	@Inject private FileDescriptorRepository fileDescriptorRepository;
 30	@Inject private FileRecipientService fileRecipientService;
 31	@Inject private FileRecipientRepository fileRecipientRepository;
 32
 33	@Transactional(Transactional.TxType.REQUIRED)
 34	public FileDescriptor addFile(String filename, String userSrc) {
 35
 36		if ((null != filename) && (null != userSrc)) {
 37			return this.fileDescriptorRepository.createFile(filename, null, userSrc, null);
 38		}
 39		return null;
 40	}
 41
 42	// upload a file to multiples receivers
 43	@Transactional(Transactional.TxType.REQUIRED)
 44	public boolean uploadFileToUsers(String[] users, String filename, String base64Data, String userPost) {
 45		FileDescriptor file;
 46		String folder;
 47		File fileOut;
 48		String base64Head;
 49		byte[] content;
 50
 51		if ((null != users) && (0 < users.length) && (null != filename) && (null != base64Data)) {
 52			// prepare upload: add file name to database
 53			file = this.addFile(filename, userPost);
 54			try {
 55				// compute destination folder name
 56				folder = ConfigResolver.getPropertyValue("application.storage.base");
 57				folder = folder.replace("{source}", userPost);
 58				// compute filename
 59				fileOut = new File(folder, UUID.randomUUID().toString());
 60				// ensure this path exists
 61				FileUtils.forceMkdir(new File(folder));
 62				// extract base64 header before decoding
 63				base64Head = base64Data.substring(BASE64_DATA.length(), base64Data.indexOf(BASE64_TAG));
 64				// get Base64 content
 65				content = Base64.decodeBase64(base64Data.substring(base64Data.indexOf(BASE64_TAG) + BASE64_TAG.length()));
 66				FileUtils.writeByteArrayToFile(fileOut, content);
 67				// update file in DB
 68				if (!this.updateFilePathAndType(file.getId(), fileOut.getAbsolutePath(), base64Head)) {
 69					logger.error("Unable to update file [" + file.getId() + "]");
 70					throw new LfdException("Unable to update file [" + file.getId() + "]");
 71				}
 72				return (null != this.fileRecipientService.addRecipients(file.getId(), users));
 73			} catch (Exception e) {
 74				System.out.println("Unable to write file [" + filename + "]");
 75				e.printStackTrace();
 76				// delete file from DB
 77				this.deleteFile(file);
 78			}
 79		}
 80		logger.error("Missing parameters to upload file [users: " + ((null != users) && (0 < users.length)) + "], [filename=" + (null != filename) + "], " +
 81				"[data=" + (null != base64Data) + "]");
 82		return false;
 83	}
 84
 85	@Transactional(Transactional.TxType.REQUIRED)
 86	public Boolean updateFilePathAndType(Integer fileId, String path, String type) {
 87		FileDescriptor file;
 88
 89		if ((null != fileId) && (null != path)) {
 90			file = this.fileDescriptorRepository.updateFilePathAndType(fileId, path, type);
 91			return (null != file);
 92		}
 93		return Boolean.FALSE;
 94	}
 95
 96	@Transactional(Transactional.TxType.REQUIRED)
 97	public void deleteFile(FileDescriptor file) {
 98		if (null != file) {
 99			FileUtils.deleteQuietly(new File(file.getPath()));
100			this.fileDescriptorRepository.deleteFile(file.getId());
101		}
102	}
103
104	@Transactional(Transactional.TxType.REQUIRED)
105	public JsonFileDownload downloadFile(Integer fileId, String user) {
106		FileRecipient rec;
107		JsonFileDownload jsonDL;
108		StringBuilder content;
109
110		jsonDL = new JsonFileDownload();
111		// start by finding file in database
112		rec = this.fileRecipientService.findFileByIdAndOwner(fileId, user);
113		if ((null != rec) && null != rec.getFile()) {
114			// mark file as read
115			this.fileRecipientService.markRead(rec);
116			// add filename (without path)
117			jsonDL.setFilename(rec.getFile().getName());
118			try {
119				content = new StringBuilder();
120				// add Base64 header
121				content.append(BASE64_DATA);
122				content.append(rec.getFile().getDataType());
123				content.append(BASE64_TAG);
124				// add Base64 file content
125				content.append(Base64.encodeBase64String(FileUtils.readFileToByteArray(new File(rec.getFile().getPath()))));
126				jsonDL.setType(rec.getFile().getDataType());
127				jsonDL.setData(content.toString());
128				return jsonDL;
129			} catch (IOException e) {
130				logger.error("Unable to read file [" + rec.getFile().getPath() + "]", e);
131				e.printStackTrace();
132			}
133		} else {
134			logger.error("Unable to fetch file with id [" + fileId + "] for user [" + user + "]");
135		}
136		return null;
137	}
138
139	@Transactional(Transactional.TxType.REQUIRED)
140	public int deleteFiles(List<Integer> files, String user) {
141		List<FileDescriptor> filesDesc;
142		List< FileRecipient> recips;
143
144		if ((null != files) && (0 < files.size()) && (null != user)) {
145			// find files by id
146			filesDesc = this.fileDescriptorRepository.findByIdIn(files);
147			if (filesDesc.size() == files.size()) { // we got all required files
148				for (FileDescriptor fileDesc : filesDesc) {
149					// break link between recipient and file
150					this.fileRecipientRepository.deleteLink(user, fileDesc.getId());
151					// check if a user hold this file
152					recips = this.fileRecipientRepository.findAnyByFileId(fileDesc.getId());
153					if ((null == recips) || (0 == recips.size())) {
154						// no one hold the file, remove it
155						this.deleteFile(fileDesc);
156					}
157				}
158				return filesDesc.size();
159			}
160		}
161		return -1;
162	}
163}
MéthodeBut
addFileEtablit 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.
uploadFileToUsersCré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).
updateFilePathAndTypeStocker 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).
deleteFileEfface le fichier désigné.
downloadFilelit le fichier désigné, le transforme en Base64 et prépare la réponse JSON pour le navigateur
deleteFilesA 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.

    
 1package ch.gobothegeek.lofidrox.services;
 2
 3import ch.gobothegeek.lofidrox.model.entities.FileRecipient;
 4import ch.gobothegeek.lofidrox.repositories.FileDescriptorRepository;
 5import ch.gobothegeek.lofidrox.repositories.FileRecipientRepository;
 6import org.slf4j.Logger;
 7import org.slf4j.LoggerFactory;
 8import javax.enterprise.context.ApplicationScoped;
 9import javax.inject.Inject;
10import javax.transaction.Transactional;
11import java.util.ArrayList;
12import java.util.List;
13import java.util.Optional;
14
15@ApplicationScoped
16@Transactional
17public class FileRecipientService {
18	private static final Logger logger = LoggerFactory.getLogger(FileRecipientService.class);
19
20	@Inject private FileDescriptorRepository fileDescriptorRepository;
21	@Inject private FileRecipientRepository fileRecipientRepository;
22
23	// add recipients for a file descriptor
24	@Transactional(Transactional.TxType.REQUIRED)
25	public List<FileRecipient> addRecipients(Integer fileId, String[] users) {
26		List<FileRecipient> recipients;
27		FileRecipient rec;
28
29		if ((null != fileId) && (null != users) && (0 < users.length)) {
30			recipients = new ArrayList<>();
31			for (String userTo : users) {
32				rec = this.fileRecipientRepository.createLink(fileId, userTo, false);
33				recipients.add(rec);
34			}
35			return recipients;
36		}
37		logger.error("Missing parameters to add recipients [fileId=" + (null != fileId) + "], [users=" + ((null != users) && (0 < users.length)) + "]");
38		return null;
39	}
40
41	// return list of files for a specific user
42	@Transactional(Transactional.TxType.REQUIRED)
43	public List<FileRecipient> listFilesForUser(String owner) {
44		return this.fileRecipientRepository.listFilesForUser(owner);
45	}
46
47	@Transactional(Transactional.TxType.REQUIRED)
48	public FileRecipient findFileByIdAndOwner(Integer id, String owner) {
49		Optional<FileRecipient> file;
50
51		file = this.fileRecipientRepository.findFileByIdAndUserTo(id, owner);
52		return (file.orElse(null));
53	}
54
55	@Transactional(Transactional.TxType.REQUIRED)
56	public FileRecipient markRead(FileRecipient recipient) {
57		recipient = this.fileRecipientRepository.markRead(recipient);
58		return recipient;
59	}
60}
MéthodeBut
addRecipientsGénére les liens entre un fichier donné et les utilisateurs concernés.
listFilesForUserRetourne la liste des fichiers reçus par un utilisateur.
findFileByIdAndOwnerPermet de vérifier si un fichier donné est bien lié à l’utilisateur indiqué.
markReadNote 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.

    
 1package ch.gobothegeek.lofidrox.services;
 2
 3import org.slf4j.Logger;
 4import org.slf4j.LoggerFactory;
 5import javax.enterprise.context.RequestScoped;
 6import javax.inject.Inject;
 7import javax.servlet.http.HttpServletRequest;
 8
 9@RequestScoped
10public class RequestService {
11	private final Logger logger = LoggerFactory.getLogger(RequestService.class);
12
13	@Inject	private HttpServletRequest request;
14
15	public String getRemoteAddress() {
16		return request.getRemoteAddr();
17	}
18}

Service SessionService

Ici on va gérer les sessions utilisateurs: création, récupération et suppression.

    
 1package ch.gobothegeek.lofidrox.services;
 2
 3import ch.gobothegeek.lofidrox.exceptions.LfdSessionException;
 4import ch.gobothegeek.lofidrox.security.sessions.LfdSession;
 5import org.slf4j.Logger;
 6import org.slf4j.LoggerFactory;
 7import javax.enterprise.context.ApplicationScoped;
 8import java.util.HashMap;
 9
10@ApplicationScoped
11public class SessionService {
12	private static final Logger logger = LoggerFactory.getLogger(SessionService.class);
13	private HashMap<String, LfdSession> sessions;
14
15	public SessionService() {
16		this.sessions = new HashMap<>();
17	}
18
19	public void create(String username) {
20		if (!this.sessions.containsKey(username)) {
21			this.sessions.put(username, new LfdSession());
22		}
23	}
24
25	public void add(String username, String key, Object obj) throws LfdSessionException {
26		if (this.sessions.containsKey(username)) {
27			this.sessions.get(username).add(key, obj);
28			return;
29		}
30		logger.error("No session for [" + username + "]");
31		throw new LfdSessionException("No session for this user (" + username + ")");
32	}
33
34	public Object get(String username, String key) throws LfdSessionException {
35		if (this.sessions.containsKey(username)) {
36			return this.sessions.get(username).get(key);
37		}
38		logger.error("No session for [" + username + "]");
39		throw new LfdSessionException("No session for this user (" + username + ")");
40	}
41
42	public void delete(String username) {
43		if (this.sessions.containsKey(username)) {
44			this.sessions.get(username).destroy();
45			this.sessions.remove(username);
46		}
47	}
48
49	public boolean hasSession(String username) {
50		return this.sessions.containsKey(username);
51	}
52}
MéthodeBut
createCréer une nouvelle session pour l’utilisateur, s’il n’en possède pas encore.
addAjoute un objet à la session de l’utilisateur.
getRetourne un objet de la session de l’utilisateur.
deleteSupprime la session de l’utilisateur.
hasSessionIndique si l’utilisateur possède une session.

Service UserService

Ce service permet le pilotage des utilisateurs: enregistrement, connexion, déconnexion.

    
  1package ch.gobothegeek.lofidrox.services;
  2
  3import ch.gobothegeek.lofidrox.exceptions.LfdException;
  4import ch.gobothegeek.lofidrox.model.entities.User;
  5import ch.gobothegeek.lofidrox.repositories.UserRepository;
  6import ch.gobothegeek.lofidrox.security.LfdUserBean;
  7import ch.gobothegeek.lofidrox.utils.GtgPasswordUtils;
  8import org.apache.commons.codec.DecoderException;
  9import org.apache.commons.codec.binary.Hex;
 10import org.apache.deltaspike.core.api.config.ConfigResolver;
 11import org.slf4j.Logger;
 12import org.slf4j.LoggerFactory;
 13import javax.enterprise.context.ApplicationScoped;
 14import javax.inject.Inject;
 15import javax.transaction.Transactional;
 16import java.util.ArrayList;
 17import java.util.List;
 18import java.util.Optional;
 19
 20@ApplicationScoped
 21@Transactional
 22public class UserService {
 23	private static final Logger logger = LoggerFactory.getLogger(UserService.class);
 24	@Inject private UserRepository userRepository;
 25	@Inject private SessionService sessionService;
 26	@Inject private LfdUserBean lfdUserBean;
 27
 28	@Transactional(Transactional.TxType.REQUIRED)
 29	public Boolean loginUser(String username, String pwd) {
 30		Optional<User> user;
 31		String encPwd;
 32
 33		try {
 34			encPwd = GtgPasswordUtils.hashPassword(pwd, Hex.decodeHex(ConfigResolver.getPropertyValue("application.password.salt")),
 35				ConfigResolver.getPropertyValue("application.password.algorithm"),
 36				Integer.parseInt(ConfigResolver.getPropertyValue("application.password.iteration")),
 37				Integer.parseInt(ConfigResolver.getPropertyValue("application.password.keylength")));
 38			user = this.userRepository.findByUsernameAndPwd(username, encPwd);
 39			if (user.isPresent()) {
 40				this.sessionService.create(username);
 41				//this.lfdUserLoggedEvent.fire(new LfdUserLoggedEvent());
 42				this.lfdUserBean.setUser(new ch.gobothegeek.lofidrox.security.sessions.LfdUser(username));
 43				return Boolean.TRUE;
 44			}
 45			logger.error("User not found or wrong password [" + username + "]");
 46			return Boolean.FALSE;
 47		} catch (DecoderException e) {
 48			System.out.println("Unable to decode salt [" + ConfigResolver.getPropertyValue("application.password.salt") + "]");
 49			e.printStackTrace();
 50			logger.error("Unable to decode salt [" + ConfigResolver.getPropertyValue("application.password.salt") + "]", e);
 51		}
 52		return Boolean.FALSE;
 53	}
 54
 55	@Transactional(Transactional.TxType.REQUIRED)
 56	public User createUser(String username, String pwd) throws LfdException {
 57		Optional<User> userReg;
 58		String encPwd;
 59
 60		if (null != pwd) {
 61			userReg = this.userRepository.findByUsername(username);
 62			if (userReg.isEmpty()) {
 63				try {
 64					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")));
 65					return this.userRepository.createUser(username, encPwd);
 66				} catch (DecoderException e) {
 67					logger.error("Unable to decode salt [" + ConfigResolver.getPropertyValue("application.password.salt") + "]", e);
 68					throw new LfdException("Unable to hash password");
 69				}
 70			}
 71		}
 72		return null;
 73	}
 74
 75	@Transactional(Transactional.TxType.REQUIRED)
 76	public void logoffUser(String username) {
 77		if (this.sessionService.hasSession(username)) {
 78			this.sessionService.delete(username);
 79		}
 80	}
 81
 82	@Transactional(Transactional.TxType.REQUIRED)
 83	public List<User> listUsers() {
 84		return this.userRepository.findAllOrderByUsernameAsc();
 85	}
 86
 87	@Transactional(Transactional.TxType.REQUIRED)
 88	public List<String> listUsernames() {
 89		List<User> users = this.userRepository.findAllOrderByUsernameAsc();
 90		List<String> usernames;
 91
 92		if (null != users) {
 93			usernames = new ArrayList<>();
 94			if (0 < users.size()) {
 95				for (User usr : users) { usernames.add(usr.getUsername()); }
 96			}
 97			return usernames;
 98		}
 99		return null;
100	}
101}
MéthodeBut
loginUserRéalise la connexion de l’utilisateur
createUserEnregistre un nouvel utilisateur
logoffUserDéconnecte l’utilisateur
listUsersRetourne la liste des utilisateurs
listUsernamesRetourne 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