From e8dfd11d9db721497deaa58f7da7e6cd0ffbd764 Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Fri, 24 Jan 2025 10:32:54 +0100
Subject: [PATCH] =?UTF-8?q?fix:=20permettre=20=C3=A0=20un=20admin=20de=20t?=
 =?UTF-8?q?=C3=A9l=C3=A9charger=20le=20fichier=20de=20m=C3=A9tadonn=C3=A9e?=
 =?UTF-8?q?s.=20fixes=20#10571?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .gitignore                                    |   1 +
 DEV_FR.md                                     |   3 +
 .../sido/gwt/server/customuser.properties     |   4 +
 .../sido/gwt/server/DataTransferts.java       |  10 +-
 .../downloadfile/FileDownloadServlet.java     |  89 ++---------
 .../DownloadMetadataServlet.java              | 119 ++++++--------
 .../server/filter/IdentificationFilter.java   |   4 +
 .../sido/gwt/server/mail/MailService.java     |   7 +-
 .../gwt/server/metadata/MetadataHandler.java  | 148 +++++++++---------
 .../gwt/server/servlet/AbstractServlet.java   | 127 +++++++++++++++
 .../sido/gwt/shared/StringUtils.java          |  27 +++-
 .../sido/gwt/shared/StringUtilsTest.java      |  18 ++-
 12 files changed, 323 insertions(+), 234 deletions(-)
 create mode 100644 sido-gwt/src/main/config/dev-om/fr/soeretempo/sido/gwt/server/customuser.properties
 create mode 100644 sido-gwt/src/main/java/fr/soeretempo/sido/gwt/server/servlet/AbstractServlet.java

diff --git a/.gitignore b/.gitignore
index 058c7850..24491601 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
 .checkstyle
 .classpath
 .factorypath
+log
 logs
 .pmd
 .pmdruleset.xml
diff --git a/DEV_FR.md b/DEV_FR.md
index 33bcfbd0..c801bbd6 100644
--- a/DEV_FR.md
+++ b/DEV_FR.md
@@ -25,6 +25,9 @@ postgres=# CREATE USER sido WITH PASSWORD 'sido';
       
 - créer la base de données applicative :
 ```
+$ createdb --owner=sido --encoding=UTF8 --locale=fr_FR.UTF-8 sido
+# ou
+$ psql
 postgres=# CREATE DATABASE sido WITH OWNER = sido ENCODING = 'UTF8' LC_COLLATE = 'fr_FR.UTF-8' LC_CTYPE = 'fr_FR.UTF-8';
 ```
 - quitter psql avec `\q`
diff --git a/sido-gwt/src/main/config/dev-om/fr/soeretempo/sido/gwt/server/customuser.properties b/sido-gwt/src/main/config/dev-om/fr/soeretempo/sido/gwt/server/customuser.properties
new file mode 100644
index 00000000..e354e9f9
--- /dev/null
+++ b/sido-gwt/src/main/config/dev-om/fr/soeretempo/sido/gwt/server/customuser.properties
@@ -0,0 +1,4 @@
+uid=omaury
+username=Olivier Maury
+email=Olivier.Maury@inrae.fr
+organization=INRAE
diff --git a/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/server/DataTransferts.java b/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/server/DataTransferts.java
index b7fdd0b1..a6561de5 100644
--- a/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/server/DataTransferts.java
+++ b/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/server/DataTransferts.java
@@ -24,6 +24,7 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Date;
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -100,6 +101,7 @@ public final class DataTransferts {
      * @return data interface user
      */
     public static DatasourceDTO toDto(final Datasource entity) {
+        Objects.requireNonNull(entity);
         final DatasourceDTO dto = new DatasourceDTO();
         dto.setDatasourceId(entity.getId());
         dto.setName(entity.getName());
@@ -195,13 +197,11 @@ public final class DataTransferts {
             dto.setOrganization(entity.getOrganization());
             dto.setEmail(entity.getEmail());
             dto.setEmailToConfirm(entity.getEmailToConfirm());
-            dto.setCreated(formatter.format(Date.from(
-                    entity.getDateCreate().atZone(ZoneId.systemDefault()).toInstant())));
-            dto.setLastUpdate(formatter.format(Date.from(
-                    entity.getDateUpdate().atZone(ZoneId.systemDefault()).toInstant())));
+            dto.setCreated(formatter.format(localDateTimeToDate(entity.getDateCreate())));
+            dto.setLastUpdate(formatter.format(localDateTimeToDate(entity.getDateUpdate())));
             dto.setDatasources(datasourceToDto(entity.getDatasources()));
             dto.setWorkbook(workbooksToDtos(entity.getWorkbook()));
-            dto.setSidoAdmin(entity.getIsAdmin());
+            dto.setSidoAdmin(Boolean.TRUE.equals(entity.getIsAdmin()));
         }
         return dto;
     }
diff --git a/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/server/downloadfile/FileDownloadServlet.java b/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/server/downloadfile/FileDownloadServlet.java
index 82cfc30f..3d2878e6 100644
--- a/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/server/downloadfile/FileDownloadServlet.java
+++ b/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/server/downloadfile/FileDownloadServlet.java
@@ -18,21 +18,15 @@
 package fr.soeretempo.sido.gwt.server.downloadfile;
 
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
-import java.io.OutputStream;
-import java.util.Locale;
+import java.util.Optional;
 
 import javax.enterprise.context.RequestScoped;
 import javax.inject.Inject;
-import javax.servlet.ServletContext;
 import javax.servlet.annotation.WebServlet;
-import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
 
-import fr.soeretempo.sido.gwt.server.SessionAttribute;
 import fr.soeretempo.sido.gwt.server.SidoConfiguration;
 import fr.soeretempo.sido.gwt.server.SidoConfiguration.Key;
 import fr.soeretempo.sido.gwt.server.dao.DatasourceDao;
@@ -40,6 +34,7 @@ import fr.soeretempo.sido.gwt.server.dao.WorkbookDao;
 import fr.soeretempo.sido.gwt.server.model.Datasource;
 import fr.soeretempo.sido.gwt.server.model.User;
 import fr.soeretempo.sido.gwt.server.model.Workbook;
+import fr.soeretempo.sido.gwt.server.servlet.AbstractServlet;
 import fr.soeretempo.sido.verification.resources.I18n;
 import lombok.extern.log4j.Log4j2;
 
@@ -49,29 +44,19 @@ import lombok.extern.log4j.Log4j2;
 @RequestScoped
 @WebServlet(urlPatterns = { "/SidoGwt/download" })
 @Log4j2
-public class FileDownloadServlet extends HttpServlet {
+public class FileDownloadServlet extends AbstractServlet {
 
     /**
      * serial ID.
      */
     private static final long serialVersionUID = -8740967527638496181L;
 
-    /**
-     * Buffer size.
-     */
-    public static final int BUFFER_SIZE = 10240 * 10240;
-
     /**
      * config.properties.
      */
     @Inject
     private SidoConfiguration config;
 
-    /**
-     * I18n strings from .properties.
-     */
-    private I18n i18n;
-
     /**
      * DAO for Datasource.
      *
@@ -91,32 +76,23 @@ public class FileDownloadServlet extends HttpServlet {
     @Override
     public final void doGet(final HttpServletRequest request,
             final HttpServletResponse response) throws IOException {
-        String filePath = "", headerName = "";
-        // user
-        final HttpSession session = request.getSession();
-        final User user = (User) session
-                .getAttribute(SessionAttribute.USER.name());
-        // I18n.
-        final String tag = (String) session.getAttribute(
-                SessionAttribute.LOCALE.name());
-        Locale locale;
-        if (tag != null) {
-            locale = new Locale.Builder().setLanguageTag(tag).build();
-        } else {
-            locale = request.getLocale();
+        final Optional<User> userOptional = getUser(request, response);
+        if (userOptional.isEmpty()) {
+            return;
         }
-        i18n = new I18n(
-                "fr.soeretempo.sido.gwt.server.resources.messages", locale);
+        final User user = userOptional.get();
+
+        final I18n i18n = getI18n(request);
 
         if (config != null) {
             if (user == null) {
-                LOGGER.trace("1");
-                response.setContentType("text/plain");
+                response.setContentType(CONTENT_TYPE_TEXT_PLAIN);
                 response.sendError(HttpServletResponse.SC_UNAUTHORIZED,
                         i18n.get("msgSessionExpired"));
                 return;
             } else {
-                String fileName = "";
+                final String fileName;
+                final String filePath;
                 if (request.getParameter("idW") != null) {
                     final Workbook work = workbookDao
                             .findBy(Long.valueOf(request.getParameter("idW")));
@@ -160,62 +136,29 @@ public class FileDownloadServlet extends HttpServlet {
                     response.sendError(HttpServletResponse.SC_BAD_REQUEST);
                     return;
                 }
-                headerName = String.format("attachment; filename=\"%s\"",
-                        fileName);
                 try {
                     // reads input file from an absolute path
                     final File downloadFile = new File(filePath);
                     if (!downloadFile.exists()) {
                         LOGGER.warn("File not found at " + filePath);
-                        response.setContentType("text/plain");
+                        response.setContentType(CONTENT_TYPE_TEXT_PLAIN);
                         response.sendError(HttpServletResponse.SC_NOT_FOUND,
                                 i18n.get("msgDowloadFileProblem"));
                         return;
                     }
-                    final FileInputStream inStream = new FileInputStream(
-                            downloadFile);
-                    // if you want to use a relative path to context root:
-                    LOGGER.info("relativePath = "
-                            + getServletContext().getRealPath(""));
-                    // obtains ServletContext
-                    final ServletContext context = getServletContext();
-                    // gets MIME type of the file
-                    String mimeType = context.getMimeType(filePath);
-                    if (mimeType == null) {
-                        // set to binary type if MIME mapping not found
-                        mimeType = "application/octet-stream";
-                    }
-                    LOGGER.info("MIME type: " + mimeType);
-                    // modifies response
-                    response.setContentType(mimeType);
-                    response.setContentLengthLong(downloadFile.length());
-                    // forces download
-                    final String headerKey = "Content-Disposition";
-                    final String headerValue = headerName;
-                    response.setHeader(headerKey, headerValue);
-                    // obtains response's output stream
-                    final OutputStream outStream = response.getOutputStream();
-                    final byte[] buffer = new byte[BUFFER_SIZE];
-                    int bytesRead = -1;
-                    while ((bytesRead = inStream.read(buffer)) != -1) {
-                        outStream.write(buffer, 0, bytesRead);
-                    }
-                    inStream.close();
-                    outStream.close();
+                    writeFile(downloadFile, fileName, response);
                 } catch (final Exception e) {
-                    response.setContentType("text/plain");
+                    response.setContentType(CONTENT_TYPE_TEXT_PLAIN);
                     response.sendError(
                             HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                             e.getMessage());
                     LOGGER.catching(e);
-                    return;
                 }
             }
         } else {
-            response.setContentType("text/plain");
+            response.setContentType(CONTENT_TYPE_TEXT_PLAIN);
             response.sendError(HttpServletResponse.SC_NOT_FOUND,
                     i18n.get("msgConfigPropProblem"));
-            return;
         }
 
     }
diff --git a/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/server/downloadmetadata/DownloadMetadataServlet.java b/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/server/downloadmetadata/DownloadMetadataServlet.java
index d9557bdc..ad81f6c8 100644
--- a/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/server/downloadmetadata/DownloadMetadataServlet.java
+++ b/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/server/downloadmetadata/DownloadMetadataServlet.java
@@ -1,26 +1,28 @@
 package fr.soeretempo.sido.gwt.server.downloadmetadata;
 
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.OutputStream;
+import java.util.Collection;
 import java.util.List;
+import java.util.Optional;
 import java.util.stream.Collectors;
 
 import javax.enterprise.context.RequestScoped;
 import javax.inject.Inject;
 import javax.servlet.annotation.MultipartConfig;
 import javax.servlet.annotation.WebServlet;
-import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
-import fr.soeretempo.sido.gwt.server.SessionAttribute;
 import fr.soeretempo.sido.gwt.server.SidoConfiguration;
+import fr.soeretempo.sido.gwt.server.dao.DatasetDao;
 import fr.soeretempo.sido.gwt.server.metadata.MetadataHandler;
 import fr.soeretempo.sido.gwt.server.model.Dataset;
+import fr.soeretempo.sido.gwt.server.model.Datasource;
 import fr.soeretempo.sido.gwt.server.model.User;
+import fr.soeretempo.sido.gwt.server.servlet.AbstractServlet;
+import fr.soeretempo.sido.gwt.shared.StringUtils;
 import fr.soeretempo.sido.rs.HttpStatus;
 import lombok.extern.log4j.Log4j2;
 
@@ -28,7 +30,7 @@ import lombok.extern.log4j.Log4j2;
 @WebServlet(urlPatterns = { "/SidoGwt/MetadataDownloadServlet" })
 @MultipartConfig
 @Log4j2
-public class DownloadMetadataServlet extends HttpServlet {
+public class DownloadMetadataServlet extends AbstractServlet {
 
     /**
      * UID.
@@ -40,6 +42,20 @@ public class DownloadMetadataServlet extends HttpServlet {
      */
     public static final int TAILLE_TAMPON = 10240 * 10240;
 
+    private static void bufferEmptyMetadataFile(final HttpServletResponse response,
+            final MetadataHandler metadataHandler) throws IOException {
+        LOGGER.traceEntry();
+        try (InputStream inStream = metadataHandler.getEmptyMetadataFileInputStream()) {
+            writeFile(inStream, MetadataHandler.EMPTY_METADATA_FILE_NAME, response);
+        }
+    }
+
+    /**
+     * DAO for dataset.
+     */
+    @Inject
+    private DatasetDao datasetDao;
+
     /**
      * config.properties.
      */
@@ -47,98 +63,53 @@ public class DownloadMetadataServlet extends HttpServlet {
     private SidoConfiguration config;
 
     @Override
-    public final void doGet(final HttpServletRequest request,
-            final HttpServletResponse response) throws IOException {
+    public final void doGet(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
         LOGGER.traceEntry();
-        final User user = (User) request.getSession()
-                .getAttribute(SessionAttribute.USER.name());
-        if (user == null) {
-            response.sendError(HttpStatus.UNAUTHORIZED.value(),
-                    "Session expired");
+        final Optional<User> userOptional = getUser(request, response);
+        if (userOptional.isEmpty()) {
             return;
         }
+        final User user = userOptional.get();
         final MetadataHandler metadataHandler = new MetadataHandler(null, config);
-        final String fileName = request.getParameter("name");
-        final String datasetId = request.getParameter("datasetid");
-        if (fileName == null || datasetId == null || fileName.isEmpty() || datasetId.isEmpty()) {
+        final String datasetIdStr = request.getParameter("datasetid");
+        final Optional<Long> idOptional = StringUtils.parseLong(datasetIdStr);
+        if (datasetIdStr == null || datasetIdStr.isEmpty() || idOptional.isEmpty()) {
             bufferEmptyMetadataFile(response, metadataHandler);
             return;
         }
-        final Dataset dataset = getDataset(user, datasetId, fileName);
+        final Long datasetId = idOptional.get();
+        final Dataset dataset = getDataset(user, datasetId);
         if (dataset == null) {
             response.sendError(HttpStatus.UNAUTHORIZED.value(),
-                    "Unauthorized");
+                    "You're not authorized to download metadata file of dataset #" + datasetId);
             return;
         }
         final File downloadFile = metadataHandler.getMetadataFile(dataset);
         LOGGER.info("file Path = " + downloadFile.getPath());
         if (!downloadFile.exists()) {
             response.sendError(HttpStatus.INTERNAL_SERVER_ERROR.value(),
-                    "File missing");
+                    "Metadata file of dataset #" + datasetId + " does not exist.");
             return;
         }
-        // verif du format
-        if (downloadFile.getName().endsWith(".xlsx")) {
-            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
-        } else if (downloadFile.getName().endsWith(".xls")) {
-            response.setContentType("application/vnd.ms-excel");
-        } else {
-            response.sendError(HttpStatus.INTERNAL_SERVER_ERROR.value(),
-                    "Internal error, please contact administrator");
-            return;
-        }
-        bufferFile(downloadFile, response);
+        writeFile(downloadFile, downloadFile.getName(), response);
     }
 
-    private void bufferEmptyMetadataFile(final HttpServletResponse response, final MetadataHandler metadataHandler)
-            throws IOException {
-        try (InputStream inStream = metadataHandler.getEmptyMetadataFileInputStream();
-                OutputStream outStream = response.getOutputStream();) {
-            response.setHeader("Content-Disposition",
-                    "attachment; filename=\""
-                            + MetadataHandler.EMPTY_METADATA_FILE_NAME + "\"");
-            // obtains response's output stream
-            final byte[] buffer = new byte[TAILLE_TAMPON];
-            int bytesRead = -1;
-            while ((bytesRead = inStream.read(buffer)) != -1) {
-                outStream.write(buffer, 0, bytesRead);
-            }
-        } catch (final Exception e) {
-            LOGGER.catching(e);
-            response.setContentType("text/plain");
-            response.sendError(HttpStatus.INTERNAL_SERVER_ERROR.value(),
-                    "File missing");
+    private Dataset getDataset(final User user, final Long datasetId) {
+        LOGGER.traceEntry();
+        if (Boolean.TRUE.equals(user.getIsAdmin())) {
+            return datasetDao.findById(datasetId);
         }
-    }
-
-    private void bufferFile(final File downloadFile, final HttpServletResponse response) throws IOException {
-        try (FileInputStream inStream = new FileInputStream(downloadFile);
-                OutputStream outStream = response.getOutputStream();) {
-            response.setContentLengthLong(downloadFile.length());
-            response.setHeader("Content-Disposition",
-                    "attachment; filename=\""
-                            + downloadFile.getName() + "\"");
-            // obtains response's output stream
-            final byte[] buffer = new byte[TAILLE_TAMPON];
-            int bytesRead = -1;
-            while ((bytesRead = inStream.read(buffer)) != -1) {
-                outStream.write(buffer, 0, bytesRead);
-            }
-        } catch (final Exception e) {
-            LOGGER.catching(e);
-            response.setContentType("text/plain");
-            response.sendError(HttpStatus.INTERNAL_SERVER_ERROR.value(),
-                    "File missing");
+        if (user.getDatasources().isEmpty()) {
+            LOGGER.info("Strange, no datasource for user {}", user.getUserName());
+            return null;
         }
-    }
-
-    private Dataset getDataset(final User user, final String datasetId, final String fileName) {
         final List<Dataset> accessibleDatasets = user.getDatasources().stream()
-                .map(datasource -> datasource .getDatasets())
-                .flatMap(datasetList -> datasetList.stream()).collect(Collectors.toList());
+                .map(Datasource::getDatasets) //
+                .flatMap(Collection<Dataset>::stream) //
+                .collect(Collectors.toList());
         for (final Dataset dataset : accessibleDatasets) {
-            if (dataset.getId().equals(Long.parseLong(datasetId))
-                    && dataset.getMetadataFile().equals(fileName)) {
+            LOGGER.info("Accessible dataset: {}", dataset);
+            if (dataset.getId().equals(datasetId)) {
                 return dataset;
             }
         }
diff --git a/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/server/filter/IdentificationFilter.java b/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/server/filter/IdentificationFilter.java
index 2256f3b6..71156e85 100644
--- a/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/server/filter/IdentificationFilter.java
+++ b/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/server/filter/IdentificationFilter.java
@@ -43,6 +43,7 @@ import fr.soeretempo.sido.gwt.server.dao.UserDao;
 import fr.soeretempo.sido.gwt.server.model.User;
 import fr.soeretempo.sido.gwt.server.servlet.CookiesServlet;
 import fr.soeretempo.sido.rs.HttpStatus;
+import lombok.extern.log4j.Log4j2;
 
 /**
  * Filter to identify the user.
@@ -53,6 +54,7 @@ import fr.soeretempo.sido.rs.HttpStatus;
  *
  * @author melhasnaoui
  */
+@Log4j2
 @WebFilter(urlPatterns = { "/", "/SidoGwt.html", "/SidoGwt/rpc/*" })
 public final class IdentificationFilter implements Filter {
 
@@ -131,7 +133,9 @@ public final class IdentificationFilter implements Filter {
             resp.sendRedirect(config.get(Key.APP_URL) + "/login" + localeTag);
             return;
         case "file":
+            LOGGER.info("Getting user from properties");
             user = getUserFromProperties();
+            LOGGER.info("Storing user in session: {}", user);
             storeUserInSession(session, user);
             chain.doFilter(request, response);
             break;
diff --git a/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/server/mail/MailService.java b/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/server/mail/MailService.java
index 1dae6c06..96897551 100644
--- a/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/server/mail/MailService.java
+++ b/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/server/mail/MailService.java
@@ -152,6 +152,10 @@ public class MailService {
      *             exception
      */
     public void send(final Mail mail) throws SendMailException {
+        if (!isAvailable()) {
+            LOGGER.info("Properties not set, the email will not be sent!");
+            return;
+        }
         String to;
         String from;
         final String subject = mail.getSubject();
@@ -203,8 +207,7 @@ public class MailService {
             }
             msg.setSubject(subject);
             msg.setSentDate(new Date());
-            // If the desired charset is known, you can use
-            // setText(text, charset)
+            // If the desired charset is known, you can use setText(text, charset)
             msg.setContent(mail.getMessage(), mail.getFullMimeType());
 
             if (!mail.getAttachements().isEmpty()) {
diff --git a/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/server/metadata/MetadataHandler.java b/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/server/metadata/MetadataHandler.java
index 2bffced7..1444b7cf 100644
--- a/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/server/metadata/MetadataHandler.java
+++ b/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/server/metadata/MetadataHandler.java
@@ -5,6 +5,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.nio.file.StandardCopyOption;
 import java.util.Arrays;
@@ -46,6 +47,16 @@ public class MetadataHandler {
     @Inject
     private MailService mailService;
 
+    /**
+     * DataSet DAO.
+     */
+    private final DatasetDao datasetDao = new DatasetDaoImpl();
+
+    /**
+     * config.properties.
+     */
+    private final SidoConfiguration config;
+
     /**
      * Constructor.
      * @param setLocale locale used to generate error reports.
@@ -56,15 +67,75 @@ public class MetadataHandler {
         config = sidoConfig;
     }
 
+    private MetadataReport checkMetadataFile(final File metadataFile) {
+        final MetadataParser parser = new MetadataParser(locale);
+        parser.parseMetadata(metadataFile, true);
+        return parser.getMetadataReport();
+    }
+
     /**
-     * DataSet DAO.
+     * Gets a facade for a deleted / empty dataset in the dataverse.
+     * @return minimal facade
      */
-    private final DatasetDao datasetDao = new DatasetDaoImpl();
+    public DatasetFacade getEmptyFacade() {
+        return DatasetFacade.builder().geographicBoundingBox(null)
+                .timePeriodCovered(null)
+                .title("Deleted").build();
+    }
 
     /**
-     * config.properties.
+     * Retrieve the empty metadata file content.
+     * @return empty metadata file content from resource folder
      */
-    private final SidoConfiguration config;
+    public InputStream getEmptyMetadataFileInputStream() {
+        return this.getClass().getResourceAsStream(EMPTY_METADATA_FILE_NAME);
+    }
+
+    /**
+     * Get the MetadataReport with the facade loaded if all set correctly.
+     * @param datasetId dataset's id in sido
+     * @return a MetadataReport
+     */
+    public MetadataReport getFacade(final long datasetId) {
+        LOGGER.traceEntry();
+        final Dataset dataset = datasetDao.findById(datasetId);
+        final MetadataParser parser = new MetadataParser(locale);
+        parser.parseMetadata(getMetadataFile(dataset), false);
+        try {
+            final OutilsVerifXML verifXml = new OutilsVerifXML(config.get(Key.MODEL_DIR) + File.separator
+                    + dataset.getDatasource().getFileParam());
+            final Map<String, String[]> mappedColumns = verifXml.getMappedColumns();
+            LOGGER.trace("Mapped columns in .xml: " + mappedColumns);
+            if (parser.getMetadataReport().getDatasetFacade() == null) {
+                throw new IOException("The DatasetFacade should not be null !");
+            } else {
+                parser.parseAdditionnalMetadata(dataset, mappedColumns);
+            }
+        } catch (final JAXBException | IOException e) {
+            parser.getMetadataReport().addErrors(Arrays.asList("Something went wrong reading the parameter file (.xml)"
+                    + " on the server. Please contact SIDO administration."));
+            mailService.errorMail(e, config.get(Key.APP_URL), mailService.getAdditionnalInfos(dataset, null));
+            LOGGER.catching(e);
+        }
+        LOGGER.traceExit();
+        return parser.getMetadataReport();
+    }
+
+    /**
+     * Get the metadata file of a dataset (in sido filesystem).
+     * @param dataset the dataset object
+     * @return the file if present (metadata file is valid) null otherwise
+     */
+    public File getMetadataFile(final Dataset dataset) {
+        final Path path = Paths.get(config.get(Key.INSERTED_DIR), dataset.getId().toString(),
+                dataset.getMetadataFile());
+        final File metadataFile = path.toFile();
+        if (metadataFile.exists()) {
+            return metadataFile;
+        } else {
+            return null;
+        }
+    }
 
     /**
      * Get the temporary folder for a metadata file for a specific dataset.
@@ -125,12 +196,6 @@ public class MetadataHandler {
         }
     }
 
-    private MetadataReport checkMetadataFile(final File metadataFile) {
-        final MetadataParser parser = new MetadataParser(locale);
-        parser.parseMetadata(metadataFile, true);
-        return parser.getMetadataReport();
-    }
-
     /**
      * Save a metadata file in the temporary directory of the server.
      * @param filePart
@@ -167,67 +232,4 @@ public class MetadataHandler {
         return null;
     }
 
-    /**
-     * Get the metadata file of a dataset (in sido filesystem).
-     * @param dataset the dataset object
-     * @return the file if present (metadata file is valid) null otherwise
-     */
-    public File getMetadataFile(final Dataset dataset) {
-        final File metadataFile =
-                new File(config.get(Key.INSERTED_DIR) + dataset.getId() + "/" + dataset.getMetadataFile());
-        if (metadataFile.exists()) {
-            return metadataFile;
-        } else {
-            return null;
-        }
-    }
-
-    /**
-     * Get the MetadataReport with the facade loaded if all set correctly.
-     * @param datasetId dataset's id in sido
-     * @return a MetadataReport
-     */
-    public MetadataReport getFacade(final long datasetId) {
-        LOGGER.traceEntry();
-        final Dataset dataset = datasetDao.findById(datasetId);
-        final MetadataParser parser = new MetadataParser(locale);
-        parser.parseMetadata(getMetadataFile(dataset), false);
-        try {
-            final OutilsVerifXML verifXml = new OutilsVerifXML(config.get(Key.MODEL_DIR) + File.separator
-                    + dataset.getDatasource().getFileParam());
-            final Map<String, String[]> mappedColumns = verifXml.getMappedColumns();
-            LOGGER.trace("Mapped columns in .xml: " + mappedColumns);
-            if (parser.getMetadataReport().getDatasetFacade() == null) {
-                throw new IOException("The DatasetFacade should not be null !");
-            } else {
-                parser.parseAdditionnalMetadata(dataset, mappedColumns);
-            }
-        } catch (final JAXBException | IOException e) {
-            parser.getMetadataReport().addErrors(Arrays.asList("Something went wrong reading the parameter file (.xml)"
-                    + " on the server. Please contact SIDO administration."));
-            mailService.errorMail(e, config.get(Key.APP_URL), mailService.getAdditionnalInfos(dataset, null));
-            LOGGER.catching(e);
-        }
-        LOGGER.traceExit();
-        return parser.getMetadataReport();
-    }
-
-    /**
-     * Gets a facade for a deleted / empty dataset in the dataverse.
-     * @return minimal facade
-     */
-    public DatasetFacade getEmptyFacade() {
-        return DatasetFacade.builder().geographicBoundingBox(null)
-                .timePeriodCovered(null)
-                .title("Deleted").build();
-    }
-
-    /**
-     * Retrieve the empty metadata file content.
-     * @return empty metadata file content from resource folder
-     */
-    public InputStream getEmptyMetadataFileInputStream() {
-        return this.getClass().getResourceAsStream(EMPTY_METADATA_FILE_NAME);
-    }
-
 }
diff --git a/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/server/servlet/AbstractServlet.java b/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/server/servlet/AbstractServlet.java
new file mode 100644
index 00000000..faad871d
--- /dev/null
+++ b/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/server/servlet/AbstractServlet.java
@@ -0,0 +1,127 @@
+package fr.soeretempo.sido.gwt.server.servlet;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Locale;
+import java.util.Optional;
+
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import fr.soeretempo.sido.gwt.server.SessionAttribute;
+import fr.soeretempo.sido.gwt.server.model.User;
+import fr.soeretempo.sido.rs.HttpStatus;
+import fr.soeretempo.sido.verification.resources.I18n;
+import lombok.extern.log4j.Log4j2;
+
+/**
+ * Common methods for servlets.
+ *
+ * @author Olivier Maury
+ */
+@Log4j2
+public class AbstractServlet extends HttpServlet {
+
+    /**
+     * UID for Serializable.
+     */
+    private static final long serialVersionUID = 6952425792876348L;
+
+    /**
+     * The content type of the response being sent to
+     * the client, including character encoding
+     * specification.
+     */
+    protected static final String CONTENT_TYPE_TEXT_PLAIN = "text/plain;charset=UTF-8";
+
+    /**
+     * Write file to response output stream.
+     *
+     * @param inStream file to write
+     * @param fileName file name for the browser
+     * @param response response
+     * @exception IOException If an input or output exception occurs while sending
+     *                        error
+     */
+    protected static void writeFile(final InputStream inStream, final String fileName,
+            final HttpServletResponse response) throws IOException {
+        final int bufferSize = 1048;
+        try (OutputStream outStream = response.getOutputStream()) {
+            response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
+            final byte[] buffer = new byte[bufferSize];
+            int bytesRead = -1;
+            while ((bytesRead = inStream.read(buffer)) != -1) {
+                outStream.write(buffer, 0, bytesRead);
+            }
+        } catch (final Exception e) {
+            LOGGER.error("Error while writing file to response output stream.", e);
+            response.setContentType(CONTENT_TYPE_TEXT_PLAIN);
+            response.sendError(HttpStatus.INTERNAL_SERVER_ERROR.value(),
+                    "Error while writing file to response output stream.");
+        }
+    }
+
+    /**
+     * @param request request to get session and locale tag
+     * @return I18n
+     */
+    protected I18n getI18n(final HttpServletRequest request) {
+        // I18n.
+        final String tag = (String) request.getSession().getAttribute(SessionAttribute.LOCALE.name());
+        final Locale locale;
+        if (tag != null) {
+            locale = new Locale.Builder().setLanguageTag(tag).build();
+        } else {
+            locale = request.getLocale();
+        }
+        return new I18n("fr.soeretempo.sido.gwt.server.resources.messages", locale);
+    }
+
+    /**
+     * @param request  request to get session
+     * @param response response to send error
+     * @return user from session or empty if error was sent to response
+     * @exception IOException If an input or output exception occurs
+     */
+    protected Optional<User> getUser(final HttpServletRequest request, final HttpServletResponse response)
+            throws IOException {
+        final User user = (User) request.getSession().getAttribute(SessionAttribute.USER.name());
+        if (user == null) {
+            final I18n i18n = getI18n(request);
+            response.setContentType(CONTENT_TYPE_TEXT_PLAIN);
+            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, i18n.get("msgSessionExpired"));
+            return Optional.empty();
+        }
+        return Optional.of(user);
+    }
+
+    /**
+     * Write file to response output stream.
+     *
+     * @param file     file to write
+     * @param fileName file name for the browser
+     * @param response response
+     * @exception IOException If an input or output exception occurs while sending
+     *                        error
+     */
+    protected void writeFile(final File file, final String fileName, final HttpServletResponse response)
+            throws IOException {
+        try (FileInputStream inStream = new FileInputStream(file)) {
+            response.setContentLengthLong(file.length());
+            // obtains ServletContext
+            final ServletContext context = getServletContext();
+            // gets MIME type of the file
+            String mimeType = context.getMimeType(file.getAbsolutePath());
+            if (mimeType == null) {
+                // set to binary type if MIME mapping not found
+                mimeType = "application/octet-stream";
+            }
+            writeFile(inStream, fileName, response);
+        }
+    }
+}
diff --git a/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/shared/StringUtils.java b/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/shared/StringUtils.java
index 7a9ff1ae..1d741136 100644
--- a/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/shared/StringUtils.java
+++ b/sido-gwt/src/main/java/fr/soeretempo/sido/gwt/shared/StringUtils.java
@@ -17,6 +17,8 @@
  */
 package fr.soeretempo.sido.gwt.shared;
 
+import java.util.Optional;
+
 import lombok.NonNull;
 
 /**
@@ -35,12 +37,6 @@ public final class StringUtils {
         return path.substring(index + 1);
     }
 
-    /**
-     * Construct.
-     */
-    private StringUtils() {
-    }
-
     /**
      * Accented characters for normalization.
      */
@@ -59,4 +55,23 @@ public final class StringUtils {
         return unaccentedResult.toLowerCase();
     }
 
+    /**
+     * @param value String to parse
+     * @return parsed or empty().
+     */
+    public static Optional<Long> parseLong(final String value) {
+        try {
+            final Long parsed = Long.valueOf(value);
+            return Optional.of(parsed);
+        } catch (final Exception e) {
+
+        }
+        return Optional.empty();
+    }
+
+    /**
+     * Construct.
+     */
+    private StringUtils() {
+    }
 }
diff --git a/sido-gwt/src/test/java/fr/soeretempo/sido/gwt/shared/StringUtilsTest.java b/sido-gwt/src/test/java/fr/soeretempo/sido/gwt/shared/StringUtilsTest.java
index d763161c..67afb8af 100644
--- a/sido-gwt/src/test/java/fr/soeretempo/sido/gwt/shared/StringUtilsTest.java
+++ b/sido-gwt/src/test/java/fr/soeretempo/sido/gwt/shared/StringUtilsTest.java
@@ -17,20 +17,36 @@
  */
 package fr.soeretempo.sido.gwt.shared;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import java.util.Optional;
+
 import org.junit.Test;
 
 /**
  * @author melhasnaoui
+ * @author Olivier Maury
  */
 public class StringUtilsTest {
 
-    @Test()
+    @Test
     public void getFileNameFromPathTest() {
         final String path = "C:\\fakepath\\fichier.extension";
         final String result = StringUtils.getFileNameFromPath(path);
         assertTrue(result.equals("fichier.extension"));
     }
 
+    @Test
+    public void parseLong() {
+        assertTrue(StringUtils.parseLong(null).isEmpty());
+        assertTrue(StringUtils.parseLong("").isEmpty());
+        assertTrue(StringUtils.parseLong("test").isEmpty());
+        final String value = "42";
+        final Optional<Long> expected = Optional.of(42L);
+        final Optional<Long> actual = StringUtils.parseLong(value);
+        final String message = "42 must be found!";
+        assertEquals(message, expected, actual);
+    }
+
 }
-- 
GitLab