From 5165735b356523e37189647256348cddbddcd390 Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Tue, 20 Feb 2024 17:55:46 +0100
Subject: [PATCH 1/5] Ajouter la table simulation. refs #47

---
 sql/init_data.h2.sql                          |  5 ++
 sql/migration.sql                             | 11 ++++
 sql/schema.tables.sql                         | 10 ++++
 .../www/server/dao/SimulationDao.java         | 17 ++++++
 .../server/dao/SimulationDaoHibernate.java    | 29 ++++++++++
 .../www/server/model/DailyValue.java          |  2 +-
 .../www/server/model/Simulation.java          | 55 +++++++++++++++++++
 .../dao/SimulationDaoHibernateTest.java       | 27 +++++++++
 .../test/resources/META-INF/persistence.xml   |  1 +
 9 files changed, 156 insertions(+), 1 deletion(-)
 create mode 100644 www-server/src/main/java/fr/agrometinfo/www/server/dao/SimulationDao.java
 create mode 100644 www-server/src/main/java/fr/agrometinfo/www/server/dao/SimulationDaoHibernate.java
 create mode 100644 www-server/src/main/java/fr/agrometinfo/www/server/model/Simulation.java
 create mode 100644 www-server/src/test/java/fr/agrometinfo/www/server/dao/SimulationDaoHibernateTest.java

diff --git a/sql/init_data.h2.sql b/sql/init_data.h2.sql
index 6869996..6f76bd4 100644
--- a/sql/init_data.h2.sql
+++ b/sql/init_data.h2.sql
@@ -115,3 +115,8 @@ INSERT INTO normalvalue (indicator, cell, doy, computedvalue)
     JOIN indicator AS i ON i.code=t.indicator
     JOIN period AS p ON p.id=i.period
     WHERE p.code=t.period;
+
+-- simulation
+INSERT INTO simulation (date, simulationid, started, ended) VALUES
+	('2024-02-19', 1, '2024-02-20 12:00:00', '2024-02-20 12:30:00'),
+	('2024-02-20', 2, '2024-02-21 13:00:00', NULL);
\ No newline at end of file
diff --git a/sql/migration.sql b/sql/migration.sql
index 761d96d..e92b32b 100644
--- a/sql/migration.sql
+++ b/sql/migration.sql
@@ -144,6 +144,17 @@ END
 $BODY$
 language plpgsql;
 
+--
+-- 47
+--
+CREATE OR REPLACE FUNCTION upgrade20240220() RETURNS boolean AS $BODY$
+BEGIN
+	INSERT INTO simulation (date, simulationid, started, ended) VALUES
+		('2024-02-19', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
+	RETURN true;
+END
+$BODY$
+language plpgsql;
 ---
 --
 -- Keep this call at the end to apply migration functions.
diff --git a/sql/schema.tables.sql b/sql/schema.tables.sql
index 1c1fdd9..ee92d5e 100644
--- a/sql/schema.tables.sql
+++ b/sql/schema.tables.sql
@@ -1,6 +1,16 @@
 -- Schema for AgroMetInfo database
 -- MUST be compatible with H2 and PostgreSQL
 
+CREATE TABLE IF NOT EXISTS simulation (
+    id SERIAL,
+    date DATE NOT NULL,
+    simulationid INTEGER NOT NULL,
+    started TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    ended TIMESTAMP,
+    CONSTRAINT "PK_simulation" PRIMARY KEY (id)
+);
+COMMENT ON TABLE simulation IS 'Simulation run to produce the values.';
+
 CREATE TABLE IF NOT EXISTS locale (
     id SERIAL,
     languagetag VARCHAR(20) NOT NULL,
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/dao/SimulationDao.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/SimulationDao.java
new file mode 100644
index 0000000..9f8492f
--- /dev/null
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/SimulationDao.java
@@ -0,0 +1,17 @@
+package fr.agrometinfo.www.server.dao;
+
+import java.time.LocalDateTime;
+
+import fr.agrometinfo.www.server.model.Simulation;
+
+/**
+ * DAO for {@link Simulation}.
+ *
+ * @author Olivier Maury
+ */
+public interface SimulationDao {
+    /**
+     * @return date of last simulation successfully run
+     */
+    LocalDateTime findLastSimulationEnd();
+}
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/dao/SimulationDaoHibernate.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/SimulationDaoHibernate.java
new file mode 100644
index 0000000..25733ff
--- /dev/null
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/SimulationDaoHibernate.java
@@ -0,0 +1,29 @@
+package fr.agrometinfo.www.server.dao;
+
+import java.time.LocalDateTime;
+
+import fr.agrometinfo.www.server.model.Simulation;
+import jakarta.enterprise.context.ApplicationScoped;
+
+/**
+ * Hibernate implementation of {@link SimulationDao}.
+ *
+ * @author Olivier Maury
+ */
+@ApplicationScoped
+public class SimulationDaoHibernate extends DaoHibernate<Simulation> implements SimulationDao {
+
+    /**
+     * Constructor.
+     */
+    public SimulationDaoHibernate() {
+        super(Simulation.class);
+    }
+
+    @Override
+    public final LocalDateTime findLastSimulationEnd() {
+        final var jpql = "SELECT MAX(t.ended) FROM Simulation t WHERE t.ended IS NOT NULL";
+        return super.findOneByJPQL(jpql, null, LocalDateTime.class);
+    }
+
+}
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/model/DailyValue.java b/www-server/src/main/java/fr/agrometinfo/www/server/model/DailyValue.java
index 650b655..3f8cf9c 100644
--- a/www-server/src/main/java/fr/agrometinfo/www/server/model/DailyValue.java
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/model/DailyValue.java
@@ -49,7 +49,7 @@ public class DailyValue {
     @Column(name = "date", nullable = false)
     private final LocalDate date = LocalDate.now();
     /**
-     * ID: SAFRAN cell number.
+     * PK.
      */
     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/model/Simulation.java b/www-server/src/main/java/fr/agrometinfo/www/server/model/Simulation.java
new file mode 100644
index 0000000..f3cb4e0
--- /dev/null
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/model/Simulation.java
@@ -0,0 +1,55 @@
+package fr.agrometinfo.www.server.model;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.Data;
+
+/**
+ * Simulation run to produce the values.
+ *
+ * @author Olivier Maury
+ */
+@Data
+@Entity
+@Table(name = "simulation")
+public class Simulation {
+    /**
+     * Simulation start.
+     */
+    @Column(name = "started", nullable = false)
+    private LocalDateTime created;
+
+    /**
+     * Simulated date.
+     */
+    @Column(name = "date", nullable = false)
+    private LocalDate date;
+
+    /**
+     * Simulation end.
+     */
+    @Column(name = "ended", nullable = true)
+    private LocalDateTime ended;
+
+    /**
+     * PK.
+     */
+    @Id
+    @GeneratedValue(strategy = GenerationType.AUTO)
+    @Column(name = "id")
+    private long id;
+
+    /**
+     * Simulation ID.
+     */
+    @Column(name = "simulationid")
+    private long simulationId;
+
+}
diff --git a/www-server/src/test/java/fr/agrometinfo/www/server/dao/SimulationDaoHibernateTest.java b/www-server/src/test/java/fr/agrometinfo/www/server/dao/SimulationDaoHibernateTest.java
new file mode 100644
index 0000000..d956079
--- /dev/null
+++ b/www-server/src/test/java/fr/agrometinfo/www/server/dao/SimulationDaoHibernateTest.java
@@ -0,0 +1,27 @@
+package fr.agrometinfo.www.server.dao;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.time.LocalDateTime;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test SimulationDao Hibernate implementation.
+ */
+class SimulationDaoHibernateTest {
+    /**
+     * DAO to test.
+     */
+    private final SimulationDao dao = new SimulationDaoHibernate();
+
+    /**
+     * Ensure reading is OK.
+     */
+    @Test
+    void findLastSimulationEnd() {
+        final var actual = dao.findLastSimulationEnd();
+        final var expected = LocalDateTime.parse("2024-02-20T12:30:00");
+        assertEquals(expected, actual);
+    }
+}
diff --git a/www-server/src/test/resources/META-INF/persistence.xml b/www-server/src/test/resources/META-INF/persistence.xml
index 1de816e..74353e4 100644
--- a/www-server/src/test/resources/META-INF/persistence.xml
+++ b/www-server/src/test/resources/META-INF/persistence.xml
@@ -16,6 +16,7 @@
     <class>fr.agrometinfo.www.server.model.Pra</class>
     <class>fr.agrometinfo.www.server.model.PraDailyValue</class>
     <class>fr.agrometinfo.www.server.model.Region</class>
+    <class>fr.agrometinfo.www.server.model.Simulation</class>
     <properties>
       <property name="jakarta.persistence.jdbc.url" value="jdbc:h2:mem:agrometinfo;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;INIT=RUNSCRIPT FROM '../sql/schema.types.h2.sql'\;RUNSCRIPT FROM '../sql/schema.tables.sql'\;RUNSCRIPT FROM '../sql/init_data.h2.sql';" />
       <property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver" />
-- 
GitLab


From 6421b2aff1440e8946b9e61a9be6a127902ead55 Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Wed, 21 Feb 2024 11:21:40 +0100
Subject: [PATCH 2/5] Ajouter SimulationDao.findLastSimulatedDate(). refs #47

---
 .../fr/agrometinfo/www/server/dao/SimulationDao.java  |  6 ++++++
 .../www/server/dao/SimulationDaoHibernate.java        |  7 +++++++
 .../www/server/dao/SimulationDaoHibernateTest.java    | 11 +++++++++++
 3 files changed, 24 insertions(+)

diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/dao/SimulationDao.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/SimulationDao.java
index 9f8492f..b1dfc6c 100644
--- a/www-server/src/main/java/fr/agrometinfo/www/server/dao/SimulationDao.java
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/SimulationDao.java
@@ -1,5 +1,6 @@
 package fr.agrometinfo.www.server.dao;
 
+import java.time.LocalDate;
 import java.time.LocalDateTime;
 
 import fr.agrometinfo.www.server.model.Simulation;
@@ -10,6 +11,11 @@ import fr.agrometinfo.www.server.model.Simulation;
  * @author Olivier Maury
  */
 public interface SimulationDao {
+    /**
+     * @return the last simulated date
+     */
+    LocalDate findLastSimulatedDate();
+
     /**
      * @return date of last simulation successfully run
      */
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/dao/SimulationDaoHibernate.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/SimulationDaoHibernate.java
index 25733ff..9b8c8c6 100644
--- a/www-server/src/main/java/fr/agrometinfo/www/server/dao/SimulationDaoHibernate.java
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/SimulationDaoHibernate.java
@@ -1,5 +1,6 @@
 package fr.agrometinfo.www.server.dao;
 
+import java.time.LocalDate;
 import java.time.LocalDateTime;
 
 import fr.agrometinfo.www.server.model.Simulation;
@@ -20,6 +21,12 @@ public class SimulationDaoHibernate extends DaoHibernate<Simulation> implements
         super(Simulation.class);
     }
 
+    @Override
+    public LocalDate findLastSimulatedDate() {
+        final var jpql = "SELECT MAX(t.date) FROM Simulation t WHERE t.ended IS NOT NULL";
+        return super.findOneByJPQL(jpql, null, LocalDate.class);
+    }
+
     @Override
     public final LocalDateTime findLastSimulationEnd() {
         final var jpql = "SELECT MAX(t.ended) FROM Simulation t WHERE t.ended IS NOT NULL";
diff --git a/www-server/src/test/java/fr/agrometinfo/www/server/dao/SimulationDaoHibernateTest.java b/www-server/src/test/java/fr/agrometinfo/www/server/dao/SimulationDaoHibernateTest.java
index d956079..bca9611 100644
--- a/www-server/src/test/java/fr/agrometinfo/www/server/dao/SimulationDaoHibernateTest.java
+++ b/www-server/src/test/java/fr/agrometinfo/www/server/dao/SimulationDaoHibernateTest.java
@@ -2,6 +2,7 @@ package fr.agrometinfo.www.server.dao;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
+import java.time.LocalDate;
 import java.time.LocalDateTime;
 
 import org.junit.jupiter.api.Test;
@@ -15,6 +16,16 @@ class SimulationDaoHibernateTest {
      */
     private final SimulationDao dao = new SimulationDaoHibernate();
 
+    /**
+     * Ensure reading is OK.
+     */
+    @Test
+    void findLastSimulatedDate() {
+        final var actual = dao.findLastSimulatedDate();
+        final var expected = LocalDate.parse("2024-02-19");
+        assertEquals(expected, actual);
+    }
+
     /**
      * Ensure reading is OK.
      */
-- 
GitLab


From 9bc754c2e2319cea7192da1b351b0132307def3f Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Wed, 21 Feb 2024 17:03:56 +0100
Subject: [PATCH 3/5] =?UTF-8?q?Mettre=20en=20cache=20les=20r=C3=A9ponses?=
 =?UTF-8?q?=20des=20web=20services.=20refs=20#47?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/migration.sql                             |   2 +-
 src/site/markdown/development.md              |   9 +
 www-server/pom.xml                            |  12 ++
 .../www/server/AgroMetInfoConfiguration.java  |  23 ++
 .../server/dao/SimulationDaoHibernate.java    |   6 +-
 .../www/server/rs/IndicatorResource.java      | 116 +++++++++-
 .../www/server/service/CacheService.java      | 200 ++++++++++++++++++
 www-server/src/main/resources/log4j2.xml      |   1 +
 www-server/src/main/tomcat10xconf/context.xml |   1 +
 .../agrometinfo/www/shared/dto/ChoiceDTO.java |   8 +-
 .../www/shared/dto/IndicatorDTO.java          |   8 +-
 .../agrometinfo/www/shared/dto/PeriodDTO.java |   4 +
 .../www/shared/dto/SimpleFeature.java         |   8 +-
 .../www/shared/dto/SummaryDTO.java            |   7 +-
 .../src/main/java/org/geojson/Feature.java    |   4 +
 .../java/org/geojson/FeatureCollection.java   |   5 +
 .../main/java/org/geojson/GeoJsonObject.java  |   8 +-
 .../src/main/java/org/geojson/LngLatAlt.java  |   8 +-
 .../main/java/org/geojson/MultiPolygon.java   |   5 +
 .../src/main/java/org/geojson/Polygon.java    |   5 +
 20 files changed, 421 insertions(+), 19 deletions(-)
 create mode 100644 www-server/src/main/java/fr/agrometinfo/www/server/service/CacheService.java

diff --git a/sql/migration.sql b/sql/migration.sql
index e92b32b..9406e3a 100644
--- a/sql/migration.sql
+++ b/sql/migration.sql
@@ -150,7 +150,7 @@ language plpgsql;
 CREATE OR REPLACE FUNCTION upgrade20240220() RETURNS boolean AS $BODY$
 BEGIN
 	INSERT INTO simulation (date, simulationid, started, ended) VALUES
-		('2024-02-19', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
+		('2024-02-19', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
 	RETURN true;
 END
 $BODY$
diff --git a/src/site/markdown/development.md b/src/site/markdown/development.md
index f9fc082..e243a0c 100644
--- a/src/site/markdown/development.md
+++ b/src/site/markdown/development.md
@@ -57,6 +57,15 @@ To package sources, you must have:
                 validationQuery="select 1" />
 ```
 
+Ensure JVM args contains in the server launch configuration:
+
+```
+--add-opens=java.base/java.math=ALL-UNNAMED
+--add-opens=java.base/java.net=ALL-UNNAMED
+--add-opens=java.base/java.text=ALL-UNNAMED
+--add-opens=java.sql/java.sql=ALL-UNNAMED
+```
+
 If CodeServer fails to launch in Eclipse, use the script
 
 ```
diff --git a/www-server/pom.xml b/www-server/pom.xml
index e622b46..cfeac5b 100644
--- a/www-server/pom.xml
+++ b/www-server/pom.xml
@@ -155,6 +155,18 @@
       <artifactId>postgresql</artifactId>
       <version>42.7.1</version>
     </dependency>
+    <!-- fast-serialization -->
+    <!-- https://mvnrepository.com/artifact/de.ruedigermoeller/fst -->
+    <dependency>
+        <groupId>com.fasterxml.jackson.core</groupId>
+        <artifactId>jackson-core</artifactId>
+        <version>2.16.0</version>
+    </dependency>
+    <dependency>
+        <groupId>de.ruedigermoeller</groupId>
+        <artifactId>fst</artifactId>
+        <version>3.0.4-jdk17</version>
+    </dependency>
     <!-- SAVA -->
     <dependency>
       <groupId>fr.inrae.agroclim</groupId>
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/AgroMetInfoConfiguration.java b/www-server/src/main/java/fr/agrometinfo/www/server/AgroMetInfoConfiguration.java
index 217c016..78b496f 100644
--- a/www-server/src/main/java/fr/agrometinfo/www/server/AgroMetInfoConfiguration.java
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/AgroMetInfoConfiguration.java
@@ -1,12 +1,15 @@
 package fr.agrometinfo.www.server;
 
+import java.io.File;
 import java.util.EnumMap;
 import java.util.Map;
 import java.util.Objects;
 
 import jakarta.annotation.PostConstruct;
 import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.inject.Produces;
 import jakarta.inject.Inject;
+import jakarta.inject.Named;
 import jakarta.servlet.ServletContext;
 import lombok.Getter;
 import lombok.NonNull;
@@ -28,6 +31,10 @@ public class AgroMetInfoConfiguration {
          * The application URL.
          */
         APP_URL("app.url"),
+        /**
+         * Cache directory path.
+         */
+        CACHE_DIRECTORY("cache.directory"),
         /**
          * Target environment (dev, preprod, prod).
          */
@@ -99,6 +106,15 @@ public class AgroMetInfoConfiguration {
         return values.get(key);
     }
 
+    /**
+     * @return cache directory path
+     */
+    @Named("cacheDirectory")
+    @Produces
+    public String getCacheDirectory() {
+        return get(ConfigurationKey.CACHE_DIRECTORY);
+    }
+
     /**
      * Initialize configuration from context.xml.
      */
@@ -112,5 +128,12 @@ public class AgroMetInfoConfiguration {
             Objects.requireNonNull(value, "Key " + strKey + " must have value in context.xml");
             values.put(key, value);
         }
+        final File dir = new File(getCacheDirectory());
+        if (!dir.exists()) {
+            LOGGER.info("Creating directory {}", dir.getAbsolutePath());
+            if (!dir.mkdirs()) {
+                LOGGER.fatal("Cache directory {} failed to create!", dir.getAbsolutePath());
+            }
+        }
     }
 }
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/dao/SimulationDaoHibernate.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/SimulationDaoHibernate.java
index 9b8c8c6..d9d748b 100644
--- a/www-server/src/main/java/fr/agrometinfo/www/server/dao/SimulationDaoHibernate.java
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/SimulationDaoHibernate.java
@@ -5,6 +5,8 @@ import java.time.LocalDateTime;
 
 import fr.agrometinfo.www.server.model.Simulation;
 import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.inject.Produces;
+import jakarta.inject.Named;
 
 /**
  * Hibernate implementation of {@link SimulationDao}.
@@ -22,11 +24,13 @@ public class SimulationDaoHibernate extends DaoHibernate<Simulation> implements
     }
 
     @Override
-    public LocalDate findLastSimulatedDate() {
+    public final LocalDate findLastSimulatedDate() {
         final var jpql = "SELECT MAX(t.date) FROM Simulation t WHERE t.ended IS NOT NULL";
         return super.findOneByJPQL(jpql, null, LocalDate.class);
     }
 
+    @Named("lastModification")
+    @Produces
     @Override
     public final LocalDateTime findLastSimulationEnd() {
         final var jpql = "SELECT MAX(t.ended) FROM Simulation t WHERE t.ended IS NOT NULL";
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/rs/IndicatorResource.java b/www-server/src/main/java/fr/agrometinfo/www/server/rs/IndicatorResource.java
index 706665d..33055df 100644
--- a/www-server/src/main/java/fr/agrometinfo/www/server/rs/IndicatorResource.java
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/rs/IndicatorResource.java
@@ -16,17 +16,20 @@ import java.util.TreeMap;
 import org.geojson.Feature;
 import org.geojson.FeatureCollection;
 
+import fr.agrometinfo.www.server.AgroMetInfoConfiguration;
 import fr.agrometinfo.www.server.dao.CellDao;
 import fr.agrometinfo.www.server.dao.IndicatorDao;
 import fr.agrometinfo.www.server.dao.MonthlyValueDao;
 import fr.agrometinfo.www.server.dao.PraDailyValueDao;
 import fr.agrometinfo.www.server.dao.PraDao;
 import fr.agrometinfo.www.server.dao.RegionDao;
+import fr.agrometinfo.www.server.dao.SimulationDao;
 import fr.agrometinfo.www.server.model.Indicator;
 import fr.agrometinfo.www.server.model.MonthlyValue;
 import fr.agrometinfo.www.server.model.Pra;
 import fr.agrometinfo.www.server.model.PraDailyValue;
 import fr.agrometinfo.www.server.model.Region;
+import fr.agrometinfo.www.server.service.CacheService;
 import fr.agrometinfo.www.server.util.DateUtils;
 import fr.agrometinfo.www.server.util.LocaleUtils;
 import fr.agrometinfo.www.shared.dto.ChoiceDTO;
@@ -37,7 +40,6 @@ import fr.agrometinfo.www.shared.dto.PeriodDTO;
 import fr.agrometinfo.www.shared.dto.SimpleFeature;
 import fr.agrometinfo.www.shared.dto.SummaryDTO;
 import fr.agrometinfo.www.shared.service.IndicatorService;
-import jakarta.annotation.PostConstruct;
 import jakarta.enterprise.context.RequestScoped;
 import jakarta.inject.Inject;
 import jakarta.servlet.http.HttpServletRequest;
@@ -47,7 +49,9 @@ import jakarta.ws.rs.Produces;
 import jakarta.ws.rs.QueryParam;
 import jakarta.ws.rs.WebApplicationException;
 import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.HttpHeaders;
 import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Request;
 import jakarta.ws.rs.core.Response;
 import lombok.extern.log4j.Log4j2;
 
@@ -112,6 +116,12 @@ public class IndicatorResource implements IndicatorService {
         return feature;
     }
 
+    /**
+     * Cache service for server-side and browser-side.
+     */
+    @Inject
+    private CacheService cacheService;
+
     /**
      * DAO for cells.
      */
@@ -154,6 +164,30 @@ public class IndicatorResource implements IndicatorService {
     @Inject
     private RegionDao regionDao;
 
+    /**
+     * JAX-RS request.
+     */
+    @Context
+    private Request request;
+
+    /**
+     * HTTP headers for response.
+     */
+    @Context
+    private HttpHeaders httpHeaders;
+
+    /**
+     * Application configuration.
+     */
+    @Inject
+    private AgroMetInfoConfiguration configuration;
+
+    /**
+     * Dao for Simulation.
+     */
+    @Inject
+    private SimulationDao simulationDao;
+
     /**
      * Ensure the value of query parameter is not null and not blank.
      *
@@ -164,7 +198,7 @@ public class IndicatorResource implements IndicatorService {
     private void checkRequired(final Object value, final String queryParamName) {
         if (value instanceof final String str && str.isBlank() || value == null) {
             final var status = Response.Status.BAD_REQUEST;
-            throw new WebApplicationException(
+            throw new WebApplicationException(//
                     Response.status(status) //
                     .entity(ErrorResponseDTO.of(status.getStatusCode(), //
                             status.getReasonPhrase(), //
@@ -176,14 +210,25 @@ public class IndicatorResource implements IndicatorService {
     /**
      * @return indicator categories with their indicators
      */
+    @SuppressWarnings("unchecked")
     @GET
     @Path(IndicatorService.PATH_LIST)
     @Produces(MediaType.APPLICATION_JSON)
     @Override
     public List<PeriodDTO> getPeriods() {
-        // TODO : ajouter un cache (CacheControl, E-Tag et WebFilter)
         LOGGER.traceEntry();
         final var locale = LocaleUtils.getLocale(httpServletRequest);
+        // HTTP cache headers
+        cacheService.setCacheKey(IndicatorService.PATH, IndicatorService.PATH_LIST, locale);
+        cacheService.setHeaders(httpHeaders);
+        if (!cacheService.needsResponse(request)) {
+            return List.of();
+        }
+        // cached response
+        if (cacheService.isCached()) {
+            return (List<PeriodDTO>) cacheService.getCache();
+        }
+        //
         final var indicators = praDailyValueDao.findIndicators();
         final Map<Long, PeriodDTO> dtos = new LinkedHashMap<>();
         for (final Indicator indicator : indicators) {
@@ -205,18 +250,33 @@ public class IndicatorResource implements IndicatorService {
         }
         final List<PeriodDTO> periods = new ArrayList<>(dtos.values());
         Collections.sort(periods, (o1, o2) -> o1.getDescription().compareTo(o2.getDescription()));
+        cacheService.setCache(periods);
         return periods;
     }
 
+    @SuppressWarnings("unchecked")
     @GET
     @Path(IndicatorService.PATH_REGIONS)
     @Produces(MediaType.APPLICATION_JSON)
     @Override
     public Map<String, String> getRegions() {
-        return regionDao.findAll().stream()//
+        // HTTP cache headers
+        cacheService.setCacheKey(IndicatorService.PATH, IndicatorService.PATH_REGIONS);
+        cacheService.setHeaders(httpHeaders);
+        if (!cacheService.needsResponse(request)) {
+            return Map.of();
+        }
+        // cached response
+        if (cacheService.isCached()) {
+            return (Map<String, String>) cacheService.getCache();
+        }
+        //
+        final Map<String, String> result = regionDao.findAll().stream()//
                 .collect(LinkedHashMap::new, //
                         (map, item) -> map.put(String.valueOf(item.getId()), item.getName()), //
                         Map::putAll);
+        cacheService.setCache(result);
+        return result;
     }
 
     @GET
@@ -230,8 +290,19 @@ public class IndicatorResource implements IndicatorService {
         checkRequired(indicatorUid, "indicator");
         checkRequired(periodCode, "period");
         checkRequired(year, "year");
-
         final var locale = LocaleUtils.getLocale(httpServletRequest);
+        // HTTP cache headers
+        cacheService.setCacheKey(IndicatorService.PATH, IndicatorService.PATH_SUMMARY, locale, indicatorUid, periodCode,
+                level, id, year);
+        cacheService.setHeaders(httpHeaders);
+        if (!cacheService.needsResponse(request)) {
+            return null;
+        }
+        // cached response
+        if (cacheService.isCached()) {
+            return (SummaryDTO) cacheService.getCache();
+        }
+        //
         final var indicator = indicatorDao.findByCodeAndPeriod(indicatorUid, periodCode);
         if (indicator == null) {
             final var status = Response.Status.BAD_REQUEST;
@@ -322,6 +393,7 @@ public class IndicatorResource implements IndicatorService {
         dto.setMonthlyValues(monthlyValues);
         dto.setParentFeature(parentFeature);
         dto.setPeriod(getTranslation(indicator.getPeriod().getNames(), locale));
+        cacheService.setCache(dto);
         return dto;
     }
 
@@ -336,6 +408,18 @@ public class IndicatorResource implements IndicatorService {
         checkRequired(indicatorUid, "indicator");
         checkRequired(periodCode, "period");
         checkRequired(year, "year");
+        // HTTP cache headers
+        cacheService.setCacheKey(IndicatorService.PATH, IndicatorService.PATH_VALUES, indicatorUid, periodCode,
+                regionId, year, comparison);
+        cacheService.setHeaders(httpHeaders);
+        if (!cacheService.needsResponse(request)) {
+            return null;
+        }
+        // cached response
+        if (cacheService.isCached()) {
+            return (FeatureCollection) cacheService.getCache();
+        }
+        //
         final FeatureCollection collection = new FeatureCollection();
         final Indicator indicator = indicatorDao.findByCodeAndPeriod(indicatorUid, periodCode);
         final LocalDate date = praDailyValueDao.findLastDate(indicator, year);
@@ -361,20 +445,30 @@ public class IndicatorResource implements IndicatorService {
             .map(IndicatorResource::toFeatureWithComparedValue) //
             .forEach(collection::add);
         }
+        cacheService.setCache(collection);
         return collection;
     }
 
+    @SuppressWarnings("unchecked")
     @GET
     @Path(IndicatorService.PATH_YEARS)
     @Produces(MediaType.APPLICATION_JSON)
     @Override
     public List<Integer> getYears() {
-        return praDailyValueDao.findYears();
-    }
-
-    @PostConstruct
-    public void init() {
-        LOGGER.traceEntry();
+        // HTTP cache headers
+        cacheService.setCacheKey(IndicatorService.PATH, IndicatorService.PATH_YEARS);
+        cacheService.setHeaders(httpHeaders);
+        if (!cacheService.needsResponse(request)) {
+            return List.of();
+        }
+        // cached response
+        if (cacheService.isCached()) {
+            return (List<Integer>) cacheService.getCache();
+        }
+        //
+        final List<Integer> result = praDailyValueDao.findYears();
+        cacheService.setCache(result);
+        return result;
     }
 
     private Map<Date, Float> toMonthlyValues(final List<MonthlyValue> values) {
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/service/CacheService.java b/www-server/src/main/java/fr/agrometinfo/www/server/service/CacheService.java
new file mode 100644
index 0000000..8c038d9
--- /dev/null
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/service/CacheService.java
@@ -0,0 +1,200 @@
+package fr.agrometinfo.www.server.service;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.Date;
+import java.util.StringJoiner;
+
+import org.nustaq.serialization.FSTConfiguration;
+import org.nustaq.serialization.FSTObjectInput;
+import org.nustaq.serialization.FSTObjectOutput;
+
+import jakarta.enterprise.context.RequestScoped;
+import jakarta.inject.Inject;
+import jakarta.inject.Named;
+import jakarta.ws.rs.core.CacheControl;
+import jakarta.ws.rs.core.EntityTag;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.Request;
+import jakarta.ws.rs.ext.RuntimeDelegate;
+import lombok.Setter;
+import lombok.extern.log4j.Log4j2;
+
+/**
+ * Server-side cache service and HTTP headers managements for browser-side
+ * cache.
+ *
+ * @author Olivier Maury
+ */
+@RequestScoped
+@Log4j2
+public class CacheService {
+    /**
+     * Config of FST.
+     *
+     * https://github.com/RuedigerMoeller/fast-serialization
+     *
+     * FST is 4x faster than JDK serialization for this data.
+     *
+     * FST is 5x faster than JDK deserialization for this data, so 3x faster than
+     * reading from remote database.
+     */
+    private static final FSTConfiguration FSTCONF = FSTConfiguration.createDefaultConfiguration();
+
+    /**
+     * Number of milliseconds in a day.
+     */
+    private static final long MILLISECONDS_IN_A_DAY = 60 * 60 * 24 * 1000;
+
+    /**
+     * Cache directory path.
+     */
+    @Setter
+    @Inject
+    @Named("cacheDirectory")
+    private String cacheDirectory;
+
+    /**
+     * Date of last modification of indicators values in database.
+     */
+    @Setter
+    @Inject
+    @Named("lastModification")
+    private LocalDateTime lastModification;
+
+    /**
+     * Key related to object to cache.
+     */
+    private String cacheKey;
+
+    /**
+     * Cache retention in days.
+     */
+    @Setter
+    private int nbOfDays = 1;
+
+    /**
+     * @return object from cache or null.
+     */
+    public Object getCache() {
+        final File cacheFile = getCacheFile();
+        if (!cacheFile.exists()) {
+            return null;
+        }
+        try (FileInputStream fis = new FileInputStream(cacheFile); FSTObjectInput in = new FSTObjectInput(fis)) {
+            return in.readObject();
+        } catch (final IOException | ClassNotFoundException e) {
+            LOGGER.fatal(e);
+        }
+        return null;
+    }
+
+    /**
+     * Create the HTTP Cache-Control response header according to lastModification
+     * and 1 retention day.
+     *
+     * @return cache control
+     */
+    private CacheControl getCacheControl() {
+        final CacheControl cc = new CacheControl();
+        final LocalDateTime dayAfter = lastModification.plusDays(nbOfDays);
+        final Long maxAge = ChronoUnit.SECONDS.between(LocalDateTime.now(), dayAfter);
+        // max age = time inn seconds
+        cc.setMaxAge(maxAge.intValue());
+        return cc;
+    }
+
+    private File getCacheFile() {
+        return Paths.get(cacheDirectory, cacheKey).toFile();
+    }
+
+    /**
+     * Create the HTTP Entity Tag, used as the value of an ETag response header,
+     * according to cacheKey.
+     *
+     * @return Entity Tag
+     */
+    private EntityTag getEtag() {
+        if (cacheKey == null) {
+            throw new IllegalStateException("cacheKey must be set before.");
+        }
+        return new EntityTag(Integer.toString(cacheKey.hashCode()));
+    }
+
+    /**
+     * @return if cache key as related cache on disk and cache is up to date.
+     */
+    public boolean isCached() {
+        final File cacheFile = getCacheFile();
+        return cacheFile.exists() && new Date().getTime() - cacheFile.lastModified() < nbOfDays * MILLISECONDS_IN_A_DAY;
+    }
+
+    /**
+     * @param request JAX-RS request
+     * @return if HTTP headers do not match current Entity Tag
+     */
+    public boolean needsResponse(final Request request) {
+        if (request == null) {
+            LOGGER.error("Request must not be null!");
+            return true;
+        }
+        return request.evaluatePreconditions(getEtag()) == null;
+    }
+
+    /**
+     * Store object in cache.
+     *
+     * @param object object to cache
+     */
+    public void setCache(final Object object) {
+        LOGGER.traceEntry("{}", object);
+        final File cacheFile = getCacheFile();
+        LOGGER.info("file Path = {}", cacheFile);
+        try (FileOutputStream fos = new FileOutputStream(cacheFile);
+                FSTObjectOutput out = FSTCONF.getObjectOutput(fos)) {
+            out.writeObject(object);
+            out.flush();
+        } catch (final IOException e) {
+            LOGGER.fatal(e);
+        }
+        LOGGER.traceExit("cached in {}", cacheFile);
+
+    }
+
+    /**
+     * @param objects objects to create a cache key
+     */
+    public void setCacheKey(final Object... objects) {
+        final StringJoiner sj = new StringJoiner("-");
+        for (final Object object : objects) {
+            if (object == null) {
+                sj.add("null");
+            } else {
+                sj.add(object.toString());
+            }
+        }
+        cacheKey = sj.toString();
+    }
+
+    /**
+     * Write HTTP headers to JAX-RS response.
+     *
+     * @param httpHeaders JAX-RS HTTP headers
+     */
+    public void setHeaders(final HttpHeaders httpHeaders) {
+        if (httpHeaders == null) {
+            LOGGER.error("HttpHeaders must not be null!");
+            return;
+        }
+        final var delegate = RuntimeDelegate.getInstance();
+        httpHeaders.getRequestHeaders().putSingle("Cache-Control",
+                delegate.createHeaderDelegate(CacheControl.class).toString(getCacheControl()));
+        httpHeaders.getRequestHeaders().putSingle("ETag",
+                delegate.createHeaderDelegate(EntityTag.class).toString(getEtag()));
+    }
+}
diff --git a/www-server/src/main/resources/log4j2.xml b/www-server/src/main/resources/log4j2.xml
index 8c0d772..8163370 100644
--- a/www-server/src/main/resources/log4j2.xml
+++ b/www-server/src/main/resources/log4j2.xml
@@ -28,6 +28,7 @@
                         <AppenderRef ref="console" level="trace" />
                         <AppenderRef ref="file" level="trace" />
                 </Root>
+                <Logger name="fr.agrometinfo" level="trace" />
                 <Logger name="org.hibernate" level="warn" />
                 <Logger name="org.jboss" level="warn" />
         </Loggers>
diff --git a/www-server/src/main/tomcat10xconf/context.xml b/www-server/src/main/tomcat10xconf/context.xml
index 0125add..197593d 100644
--- a/www-server/src/main/tomcat10xconf/context.xml
+++ b/www-server/src/main/tomcat10xconf/context.xml
@@ -3,6 +3,7 @@
 <Context path="/www-server" reloadable="true">
   <Parameter name="agrometinfo.app.email" value="agrometinfoXXXX@inrae.fr" />
   <Parameter name="agrometinfo.app.url" value="http://localhost:8080/www-server/" />
+  <Parameter name="agrometinfo.cache.directory" value="/tmp/agrometinfo/" />
   <Parameter name="agrometinfo.environment" value="dev" /> <!-- dev / preprod / prod -->
   <Parameter name="agrometinfo.log.email" value="agrometinfoXXXX@inrae.fr" />
   <Parameter name="agrometinfo.smtp.host" value="smtp.inrae.fr" />
diff --git a/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/ChoiceDTO.java b/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/ChoiceDTO.java
index b35993c..b93db59 100644
--- a/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/ChoiceDTO.java
+++ b/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/ChoiceDTO.java
@@ -1,5 +1,7 @@
 package fr.agrometinfo.www.shared.dto;
 
+import java.io.Serializable;
+
 import org.dominokit.jackson.annotation.JSONMapper;
 
 import com.fasterxml.jackson.annotation.JsonIgnore;
@@ -10,7 +12,11 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
  * @author Olivier Maury
  */
 @JSONMapper
-public final class ChoiceDTO {
+public final class ChoiceDTO implements Serializable {
+    /**
+     * UID for Serializable.
+     */
+    private static final long serialVersionUID = 5820780438006585101L;
 
     /**
      * The user wants to compare with normal.
diff --git a/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/IndicatorDTO.java b/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/IndicatorDTO.java
index ba05025..1fe02e5 100644
--- a/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/IndicatorDTO.java
+++ b/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/IndicatorDTO.java
@@ -1,5 +1,7 @@
 package fr.agrometinfo.www.shared.dto;
 
+import java.io.Serializable;
+
 import org.dominokit.jackson.annotation.JSONMapper;
 
 /**
@@ -8,7 +10,11 @@ import org.dominokit.jackson.annotation.JSONMapper;
  * @author Olivier Maury
  */
 @JSONMapper
-public class IndicatorDTO {
+public class IndicatorDTO implements Serializable {
+    /**
+     * UID for Serializable.
+     */
+    private static final long serialVersionUID = 5820780438006585102L;
     /**
      * Localized description.
      */
diff --git a/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/PeriodDTO.java b/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/PeriodDTO.java
index 3f60f35..89af772 100644
--- a/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/PeriodDTO.java
+++ b/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/PeriodDTO.java
@@ -11,6 +11,10 @@ import org.dominokit.jackson.annotation.JSONMapper;
  */
 @JSONMapper
 public final class PeriodDTO extends IndicatorDTO {
+    /**
+     * UID for Serializable.
+     */
+    private static final long serialVersionUID = 5820780438006585103L;
     /**
      * The indicators related to this period.
      */
diff --git a/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/SimpleFeature.java b/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/SimpleFeature.java
index 0f7d4b3..054c139 100644
--- a/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/SimpleFeature.java
+++ b/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/SimpleFeature.java
@@ -1,11 +1,17 @@
 package fr.agrometinfo.www.shared.dto;
 
+import java.io.Serializable;
+
 /**
  * A geographic object.
  *
  * @author Olivier Maury
  */
-public class SimpleFeature {
+public class SimpleFeature implements Serializable {
+    /**
+     * UID for Serializable.
+     */
+    private static final long serialVersionUID = 5820780438006585104L;
     /**
      * Unique identifier.
      */
diff --git a/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/SummaryDTO.java b/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/SummaryDTO.java
index e48ed9c..ebdb8de 100644
--- a/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/SummaryDTO.java
+++ b/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/SummaryDTO.java
@@ -1,5 +1,6 @@
 package fr.agrometinfo.www.shared.dto;
 
+import java.io.Serializable;
 import java.util.Date;
 import java.util.Map;
 
@@ -11,7 +12,11 @@ import org.dominokit.jackson.annotation.JSONMapper;
  * @author Olivier Maury
  */
 @JSONMapper
-public class SummaryDTO {
+public class SummaryDTO implements Serializable {
+    /**
+     * UID for Serializable.
+     */
+    private static final long serialVersionUID = 5820780438006585105L;
     /**
      * Average daily value of the indicator for the user choice.
      */
diff --git a/www-shared/src/main/java/org/geojson/Feature.java b/www-shared/src/main/java/org/geojson/Feature.java
index 227de20..c0b0e3f 100644
--- a/www-shared/src/main/java/org/geojson/Feature.java
+++ b/www-shared/src/main/java/org/geojson/Feature.java
@@ -12,6 +12,10 @@ import org.dominokit.jackson.annotation.JSONMapper;
  */
 @JSONMapper
 public final class Feature extends GeoJsonObject {
+    /**
+     * UID for Serializable.
+     */
+    private static final long serialVersionUID = 5820780438006585201L;
 
     /**
      * Associated properties.
diff --git a/www-shared/src/main/java/org/geojson/FeatureCollection.java b/www-shared/src/main/java/org/geojson/FeatureCollection.java
index 702e0d5..8527be6 100644
--- a/www-shared/src/main/java/org/geojson/FeatureCollection.java
+++ b/www-shared/src/main/java/org/geojson/FeatureCollection.java
@@ -13,6 +13,11 @@ import org.dominokit.jackson.annotation.JSONMapper;
  */
 @JSONMapper
 public final class FeatureCollection extends GeoJsonObject {
+    /**
+     * UID for Serializable.
+     */
+    private static final long serialVersionUID = 5820780438006585202L;
+
     /**
      * The Features within this FeatureCollection.
      */
diff --git a/www-shared/src/main/java/org/geojson/GeoJsonObject.java b/www-shared/src/main/java/org/geojson/GeoJsonObject.java
index 288a6d2..8f4a8d5 100644
--- a/www-shared/src/main/java/org/geojson/GeoJsonObject.java
+++ b/www-shared/src/main/java/org/geojson/GeoJsonObject.java
@@ -1,11 +1,17 @@
 package org.geojson;
 
+import java.io.Serializable;
+
 /**
  * Base class.
  *
  * @author Olivier Maury
  */
-public abstract class GeoJsonObject {
+public abstract class GeoJsonObject implements Serializable {
+    /**
+     * UID for Serializable.
+     */
+    private static final long serialVersionUID = 5820780438006585203L;
     /**
      * GeoJSON object type ("Feature", "Polygon", ...).
      */
diff --git a/www-shared/src/main/java/org/geojson/LngLatAlt.java b/www-shared/src/main/java/org/geojson/LngLatAlt.java
index 64f4ac2..1fe9e5b 100644
--- a/www-shared/src/main/java/org/geojson/LngLatAlt.java
+++ b/www-shared/src/main/java/org/geojson/LngLatAlt.java
@@ -1,5 +1,7 @@
 package org.geojson;
 
+import java.io.Serializable;
+
 import org.dominokit.jackson.annotation.JSONMapper;
 
 /**
@@ -10,7 +12,11 @@ import org.dominokit.jackson.annotation.JSONMapper;
  * @author Olivier Maury
  */
 @JSONMapper
-public final class LngLatAlt {
+public final class LngLatAlt implements Serializable {
+    /**
+     * UID for Serializable.
+     */
+    private static final long serialVersionUID = 5820780438006585204L;
 
     /**
      * "X" according to the specification, longitude in a geographic CRS.
diff --git a/www-shared/src/main/java/org/geojson/MultiPolygon.java b/www-shared/src/main/java/org/geojson/MultiPolygon.java
index 5deb3b6..f442384 100644
--- a/www-shared/src/main/java/org/geojson/MultiPolygon.java
+++ b/www-shared/src/main/java/org/geojson/MultiPolygon.java
@@ -12,6 +12,11 @@ import org.dominokit.jackson.annotation.JSONMapper;
  */
 @JSONMapper
 public class MultiPolygon extends GeoJsonObject {
+    /**
+     * UID for Serializable.
+     */
+    private static final long serialVersionUID = 5820780438006585205L;
+
     /**
      * GeoJSON object type.
      */
diff --git a/www-shared/src/main/java/org/geojson/Polygon.java b/www-shared/src/main/java/org/geojson/Polygon.java
index 673f265..e2c73e5 100644
--- a/www-shared/src/main/java/org/geojson/Polygon.java
+++ b/www-shared/src/main/java/org/geojson/Polygon.java
@@ -13,6 +13,11 @@ import org.dominokit.jackson.annotation.JSONMapper;
  */
 @JSONMapper
 public final class Polygon extends GeoJsonObject {
+    /**
+     * UID for Serializable.
+     */
+    private static final long serialVersionUID = 5820780438006585206L;
+
     /**
      * GeoJSON object type.
      */
-- 
GitLab


From a544a37bf126ae3874f8c95c8a8bbaf59bc75cd9 Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Wed, 21 Feb 2024 17:35:52 +0100
Subject: [PATCH 4/5] Tests

---
 www-server/pom.xml                            | 15 ++++++++
 .../www/server/rs/IndicatorResource.java      |  7 ----
 .../dao/SimulationDaoHibernateTest.java       |  9 +++--
 .../www/server/rs/IndicatorResourceTest.java  | 36 +++++++++++++++++++
 4 files changed, 58 insertions(+), 9 deletions(-)

diff --git a/www-server/pom.xml b/www-server/pom.xml
index cfeac5b..1369e52 100644
--- a/www-server/pom.xml
+++ b/www-server/pom.xml
@@ -242,6 +242,21 @@
     </testResources>
     <pluginManagement>
       <plugins>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-surefire-plugin</artifactId>
+          <configuration>
+			  <argLine>
+--add-opens=java.base/java.lang=ALL-UNNAMED
+--add-opens=java.base/java.math=ALL-UNNAMED
+--add-opens=java.base/java.net=ALL-UNNAMED
+--add-opens=java.base/java.text=ALL-UNNAMED
+--add-opens=java.base/java.util=ALL-UNNAMED
+--add-opens=java.base/java.util.concurrent=ALL-UNNAMED
+--add-opens=java.sql/java.sql=ALL-UNNAMED
+			  </argLine>
+		  </configuration>
+        </plugin>
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-war-plugin</artifactId>
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/rs/IndicatorResource.java b/www-server/src/main/java/fr/agrometinfo/www/server/rs/IndicatorResource.java
index 33055df..ef39990 100644
--- a/www-server/src/main/java/fr/agrometinfo/www/server/rs/IndicatorResource.java
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/rs/IndicatorResource.java
@@ -16,7 +16,6 @@ import java.util.TreeMap;
 import org.geojson.Feature;
 import org.geojson.FeatureCollection;
 
-import fr.agrometinfo.www.server.AgroMetInfoConfiguration;
 import fr.agrometinfo.www.server.dao.CellDao;
 import fr.agrometinfo.www.server.dao.IndicatorDao;
 import fr.agrometinfo.www.server.dao.MonthlyValueDao;
@@ -176,12 +175,6 @@ public class IndicatorResource implements IndicatorService {
     @Context
     private HttpHeaders httpHeaders;
 
-    /**
-     * Application configuration.
-     */
-    @Inject
-    private AgroMetInfoConfiguration configuration;
-
     /**
      * Dao for Simulation.
      */
diff --git a/www-server/src/test/java/fr/agrometinfo/www/server/dao/SimulationDaoHibernateTest.java b/www-server/src/test/java/fr/agrometinfo/www/server/dao/SimulationDaoHibernateTest.java
index bca9611..976a082 100644
--- a/www-server/src/test/java/fr/agrometinfo/www/server/dao/SimulationDaoHibernateTest.java
+++ b/www-server/src/test/java/fr/agrometinfo/www/server/dao/SimulationDaoHibernateTest.java
@@ -10,7 +10,12 @@ import org.junit.jupiter.api.Test;
 /**
  * Test SimulationDao Hibernate implementation.
  */
-class SimulationDaoHibernateTest {
+public class SimulationDaoHibernateTest {
+    /**
+     * Last modification set in SQL.
+     */
+    public static final LocalDateTime LAST_MODIFICATION = LocalDateTime.parse("2024-02-20T12:30:00");
+
     /**
      * DAO to test.
      */
@@ -32,7 +37,7 @@ class SimulationDaoHibernateTest {
     @Test
     void findLastSimulationEnd() {
         final var actual = dao.findLastSimulationEnd();
-        final var expected = LocalDateTime.parse("2024-02-20T12:30:00");
+        final var expected = LAST_MODIFICATION;
         assertEquals(expected, actual);
     }
 }
diff --git a/www-server/src/test/java/fr/agrometinfo/www/server/rs/IndicatorResourceTest.java b/www-server/src/test/java/fr/agrometinfo/www/server/rs/IndicatorResourceTest.java
index 34ae4ec..05469d6 100644
--- a/www-server/src/test/java/fr/agrometinfo/www/server/rs/IndicatorResourceTest.java
+++ b/www-server/src/test/java/fr/agrometinfo/www/server/rs/IndicatorResourceTest.java
@@ -3,10 +3,18 @@ package fr.agrometinfo.www.server.rs;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Comparator;
+
 import org.geojson.FeatureCollection;
 import org.glassfish.hk2.utilities.binding.AbstractBinder;
 import org.glassfish.jersey.server.ResourceConfig;
 import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 
 import fr.agrometinfo.www.server.dao.CellDao;
@@ -21,6 +29,10 @@ import fr.agrometinfo.www.server.dao.PraDao;
 import fr.agrometinfo.www.server.dao.PraDaoHibernate;
 import fr.agrometinfo.www.server.dao.RegionDao;
 import fr.agrometinfo.www.server.dao.RegionDaoHibernate;
+import fr.agrometinfo.www.server.dao.SimulationDao;
+import fr.agrometinfo.www.server.dao.SimulationDaoHibernate;
+import fr.agrometinfo.www.server.dao.SimulationDaoHibernateTest;
+import fr.agrometinfo.www.server.service.CacheService;
 import jakarta.ws.rs.core.Application;
 
 /**
@@ -34,6 +46,24 @@ class IndicatorResourceTest extends JerseyTest {
      */
     private static final String SEP = "/";
 
+    /**
+     * Temporary directory for cache.
+     */
+    private static Path cacheDir;
+
+    @BeforeAll
+    static void createCacheDir() throws IOException {
+        cacheDir = Files.createTempDirectory(IndicatorResourceTest.class.getName());
+    }
+
+    @AfterAll
+    static void deleteCacheDir() throws IOException {
+        Files.walk(cacheDir)
+        .sorted(Comparator.reverseOrder())
+        .map(Path::toFile)
+        .forEach(File::delete);
+    }
+
     @Override
     protected final Application configure() {
         final CellDao cellDao = new CellDaoHibernate();
@@ -42,6 +72,10 @@ class IndicatorResourceTest extends JerseyTest {
         final IndicatorDao indicatorDao = new IndicatorDaoHibernate();
         final MonthlyValueDao monthlyValueDao = new MonthlyValueDaoHibernate();
         final RegionDao regionDao = new RegionDaoHibernate();
+        final SimulationDao simulationDao = new SimulationDaoHibernate();
+        final CacheService cacheService = new CacheService();
+        cacheService.setLastModification(SimulationDaoHibernateTest.LAST_MODIFICATION);
+        cacheService.setCacheDirectory(cacheDir.toString());
         return new ResourceConfig(IndicatorResource.class).register(new AbstractBinder() {
             @Override
             public void configure() {
@@ -51,6 +85,8 @@ class IndicatorResourceTest extends JerseyTest {
                 bind(indicatorDao).to(IndicatorDao.class);
                 bind(monthlyValueDao).to(MonthlyValueDao.class);
                 bind(regionDao).to(RegionDao.class);
+                bind(simulationDao).to(SimulationDao.class);
+                bind(cacheService).to(CacheService.class);
             }
         });
     }
-- 
GitLab


From 7d56604ac340f65cda91e95d3d52e0fe4cf66ef0 Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Wed, 21 Feb 2024 17:47:34 +0100
Subject: [PATCH 5/5] Documentation

---
 src/site/markdown/development.md  |  3 +++
 src/site/markdown/installation.md | 12 ++++++++++++
 2 files changed, 15 insertions(+)

diff --git a/src/site/markdown/development.md b/src/site/markdown/development.md
index e243a0c..aecd8c1 100644
--- a/src/site/markdown/development.md
+++ b/src/site/markdown/development.md
@@ -60,9 +60,12 @@ To package sources, you must have:
 Ensure JVM args contains in the server launch configuration:
 
 ```
+--add-opens=java.base/java.lang=ALL-UNNAMED
 --add-opens=java.base/java.math=ALL-UNNAMED
 --add-opens=java.base/java.net=ALL-UNNAMED
 --add-opens=java.base/java.text=ALL-UNNAMED
+--add-opens=java.base/java.util=ALL-UNNAMED
+--add-opens=java.base/java.util.concurrent=ALL-UNNAMED
 --add-opens=java.sql/java.sql=ALL-UNNAMED
 ```
 
diff --git a/src/site/markdown/installation.md b/src/site/markdown/installation.md
index 8cf50a6..2f772c7 100644
--- a/src/site/markdown/installation.md
+++ b/src/site/markdown/installation.md
@@ -64,3 +64,15 @@ Define credentials to deploy, change `conf/tomcat-users.xml` with
 <role rolename="manager-script"/>
 <user username="tomcat-password" password="tomcat" roles="tomcat,manager-gui,manager-script"/>
 ```
+
+Ensure JVM args contains in the server launch configuration (in variable `JAVA_OPTS`) :
+
+```
+--add-opens=java.base/java.lang=ALL-UNNAMED
+--add-opens=java.base/java.math=ALL-UNNAMED
+--add-opens=java.base/java.net=ALL-UNNAMED
+--add-opens=java.base/java.text=ALL-UNNAMED
+--add-opens=java.base/java.util=ALL-UNNAMED
+--add-opens=java.base/java.util.concurrent=ALL-UNNAMED
+--add-opens=java.sql/java.sql=ALL-UNNAMED
+```
\ No newline at end of file
-- 
GitLab