diff --git a/sql/init_data.h2.sql b/sql/init_data.h2.sql index 68699968eaa6a1c9a05a9144c728960e8dfe1a8a..6f76bd4597bb79fddab8ed69b8bb7eaa9fb55468 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 761d96da1832cf06ae29483359f738450159297c..9406e3adea6a51245d528f9b096731d1cbd9797e 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 1c1fdd9bfe6fe2b568fef3b17b0de6a90a3ca1bf..ee92d5e62c86580e29827c9fb000932c4317e194 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/src/site/markdown/development.md b/src/site/markdown/development.md index f9fc082f4ed71eca9c308644e886b35c189bd297..aecd8c167fbd26b15f3bbe4fefa9dace476b837b 100644 --- a/src/site/markdown/development.md +++ b/src/site/markdown/development.md @@ -57,6 +57,18 @@ To package sources, you must have: validationQuery="select 1" /> ``` +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 +``` + If CodeServer fails to launch in Eclipse, use the script ``` diff --git a/src/site/markdown/installation.md b/src/site/markdown/installation.md index 8cf50a6256b3210a064f0f59961c4950b53cdde4..2f772c7cede0b4574bc76afedaa99dafd8d8af03 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 diff --git a/www-server/pom.xml b/www-server/pom.xml index e622b46ad822c12e747a4df037efcb1cabc7831f..1369e52c7df453f60d08566b4dfd3c6f34dfd78b 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> @@ -230,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/AgroMetInfoConfiguration.java b/www-server/src/main/java/fr/agrometinfo/www/server/AgroMetInfoConfiguration.java index 217c016261013af6222f7ac7c8cd0698f419121b..78b496fd3892e260d19091f9622a7c4ec269569f 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/SimulationDao.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/SimulationDao.java new file mode 100644 index 0000000000000000000000000000000000000000..b1dfc6cf11aac91183c1f7fe3397b0b150589f9c --- /dev/null +++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/SimulationDao.java @@ -0,0 +1,23 @@ +package fr.agrometinfo.www.server.dao; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +import fr.agrometinfo.www.server.model.Simulation; + +/** + * DAO for {@link Simulation}. + * + * @author Olivier Maury + */ +public interface SimulationDao { + /** + * @return the last simulated date + */ + LocalDate findLastSimulatedDate(); + + /** + * @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 0000000000000000000000000000000000000000..d9d748b4ff16ba522752b6e859d1752c6b28d13c --- /dev/null +++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/SimulationDaoHibernate.java @@ -0,0 +1,40 @@ +package fr.agrometinfo.www.server.dao; + +import java.time.LocalDate; +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}. + * + * @author Olivier Maury + */ +@ApplicationScoped +public class SimulationDaoHibernate extends DaoHibernate<Simulation> implements SimulationDao { + + /** + * Constructor. + */ + public SimulationDaoHibernate() { + super(Simulation.class); + } + + @Override + 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"; + 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 650b655c9ff1453e2c8b8babdf11a380ea9e57ec..3f8cf9c1aef406af5314cad7535cdb7c2c9cc80a 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 0000000000000000000000000000000000000000..f3cb4e01a88724cbb65ba7e15b1897d00cf18cf1 --- /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/main/java/fr/agrometinfo/www/server/rs/IndicatorResource.java b/www-server/src/main/java/fr/agrometinfo/www/server/rs/IndicatorResource.java index 706665d2a6496fbc45e43fb923509432aa91518c..ef39990f9dac35a089f276e48ab8e7ae0d66b7f3 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 @@ -22,11 +22,13 @@ 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 +39,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 +48,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 +115,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 +163,24 @@ public class IndicatorResource implements IndicatorService { @Inject private RegionDao regionDao; + /** + * JAX-RS request. + */ + @Context + private Request request; + + /** + * HTTP headers for response. + */ + @Context + private HttpHeaders httpHeaders; + + /** + * Dao for Simulation. + */ + @Inject + private SimulationDao simulationDao; + /** * Ensure the value of query parameter is not null and not blank. * @@ -164,7 +191,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 +203,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 +243,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 +283,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 +386,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 +401,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 +438,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 0000000000000000000000000000000000000000..8c038d9f4ff838bcd75b4a6f8b0e589d519b326e --- /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 8c0d7729e40aa4fe0ab312701085e4228fb2426a..816337036532ab7fb27f0fad5bb8a400ec5fe819 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 0125add0eb0816392f8009aa47c0fd9ad67f8627..197593d809e7121ac017bb17a6b1a607ab8d0080 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-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 0000000000000000000000000000000000000000..976a0826662e494ae506b4ff5607ef16cc0be791 --- /dev/null +++ b/www-server/src/test/java/fr/agrometinfo/www/server/dao/SimulationDaoHibernateTest.java @@ -0,0 +1,43 @@ +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; + +/** + * Test SimulationDao Hibernate implementation. + */ +public class SimulationDaoHibernateTest { + /** + * Last modification set in SQL. + */ + public static final LocalDateTime LAST_MODIFICATION = LocalDateTime.parse("2024-02-20T12:30:00"); + + /** + * DAO to test. + */ + 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. + */ + @Test + void findLastSimulationEnd() { + final var actual = dao.findLastSimulationEnd(); + 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 34ae4ece9aeeb948a5989136de0927068de6e8c4..05469d6ae75f5ae5b546b3b8036c0c33e66952c6 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); } }); } diff --git a/www-server/src/test/resources/META-INF/persistence.xml b/www-server/src/test/resources/META-INF/persistence.xml index 1de816efe743696eb7234330e8e5c387b5d8a358..74353e4ccf49232e47ee293e7d1de0e1c3351f87 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" /> 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 b35993c6ec5882e6c3f94b7edcce15e49bb562e0..b93db590f11ef3c302f94a6063e7366fa6c66b1b 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 ba0502561914b4723eb5262010c6424d170d7493..1fe02e5f57002318b6be72cc93362731d469b88b 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 3f60f352a8c8a5fc2e3343c55cf15541b4721be8..89af772b80ab003f7105b7bc705dcb82e1fc1755 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 0f7d4b37a33dfca5a8303663fa6530a1f73cee2c..054c139ec22fdd495ee80807111ce23c73e5292f 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 e48ed9c463f91bdab521bf6a1ed943e70b164582..ebdb8de76d84d2f257ea54c26a1b2d83e339cfda 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 227de207499889494bd4a73289fc650161bd4ee9..c0b0e3f539c807a7b91aa8c592c404dfc71c07b4 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 702e0d5762fc1141424b0df546c63d7f3d766d94..8527be6fa170ec2d849ef242354d80ebbbf02f37 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 288a6d277e8843d6215ac57a0b5b22c6efe9b2f8..8f4a8d597fc557c34e177f7ef526c79974af1da0 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 64f4ac2ebe0dd762bd5c473bafc70e27c99b207d..1fe9e5b9c8522796c45dfc35f2808195e2771f74 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 5deb3b6d9cc3b7e287e4b7bcaed158f405242acc..f4423845ec45ef1ebe11ac24b82ba588f02d34d6 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 673f26590a99bddee1bd3573fbb2529f863eb57b..e2c73e59b98363948bd1d2f118045113ca637423 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. */