diff --git a/pom.xml b/pom.xml
index 0a36d4fb3c..a58a4b38ac 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
net.sumaris
sumaris-pod
- 2.0.6
+ 2.1.0
pom
SUMARiS
SUMARiS :: Maven parent
diff --git a/sumaris-core-shared/pom.xml b/sumaris-core-shared/pom.xml
index fdc9768d7e..60f98dba4f 100644
--- a/sumaris-core-shared/pom.xml
+++ b/sumaris-core-shared/pom.xml
@@ -4,7 +4,7 @@
net.sumaris
sumaris-pod
- 2.0.6
+ 2.1.0
sumaris-core-shared
diff --git a/sumaris-core-shared/src/main/java/net/sumaris/core/config/SumarisConfiguration.java b/sumaris-core-shared/src/main/java/net/sumaris/core/config/SumarisConfiguration.java
index 9c04767ed1..90e15a9772 100644
--- a/sumaris-core-shared/src/main/java/net/sumaris/core/config/SumarisConfiguration.java
+++ b/sumaris-core-shared/src/main/java/net/sumaris/core/config/SumarisConfiguration.java
@@ -27,9 +27,12 @@
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import net.sumaris.core.dao.technical.Daos;
@@ -53,6 +56,7 @@
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
+import java.util.stream.Collectors;
import static org.nuiton.i18n.I18n.t;
@@ -251,7 +255,11 @@ protected void addAlias(ApplicationConfig applicationConfig) {
applicationConfig.addAlias("-d", "--option", SumarisConfigurationOption.CLI_DAEMONIZE.getKey(), "true");
applicationConfig.addAlias("--output", "--option", SumarisConfigurationOption.CLI_OUTPUT_FILE.getKey());
applicationConfig.addAlias("-f", "--option", SumarisConfigurationOption.CLI_FORCE_OUTPUT.getKey(), "true");
+ applicationConfig.addAlias("--program", "--option", SumarisConfigurationOption.CLI_FILTER_PROGRAM_LABEL.getKey());
applicationConfig.addAlias("--year", "--option", SumarisConfigurationOption.CLI_FILTER_YEAR.getKey());
+ applicationConfig.addAlias("--trip", "--option", SumarisConfigurationOption.CLI_FILTER_TRIP_IDS.getKey());
+ applicationConfig.addAlias("--operation", "--option", SumarisConfigurationOption.CLI_FILTER_OPERATION_IDS.getKey());
+ // TODO : add --sale, --observed-location --landing
}
@@ -906,9 +914,17 @@ public Integer getCliFilterYear() {
return year == -1 ? null : year;
}
- public Integer getCliFilterTripId() {
- int tripId = applicationConfig.getOptionAsInt(SumarisConfigurationOption.CLI_FILTER_TRIP_ID.getKey());
- return tripId == -1 ? null : tripId;
+ public String getCliFilterProgramLabel() {
+ String programLabel = applicationConfig.getOption(SumarisConfigurationOption.CLI_FILTER_PROGRAM_LABEL.getKey());
+ return StringUtils.isBlank(programLabel) ? null : programLabel;
+ }
+
+ public List getCliFilterTripIds() {
+ return getConfigurationOptionAsNumbers(SumarisConfigurationOption.CLI_FILTER_TRIP_IDS.getKey());
+ }
+
+ public List getCliFilterOperationIds() {
+ return getConfigurationOptionAsNumbers(SumarisConfigurationOption.CLI_FILTER_OPERATION_IDS.getKey());
}
/**
@@ -1090,4 +1106,39 @@ public String getColumnDefaultValue(String tableName, String columnName) {
return applicationConfig.getOption("sumaris." + tableName.toUpperCase() + "." + columnName.toUpperCase() + ".defaultValue");
}
+
+ public List getConfigurationOptionAsNumbers(String optionKey) {
+ List result = (List) complexOptionsCache.getIfPresent(optionKey);
+
+ // Not exists in cache
+ if (result == null) {
+ String ids = applicationConfig.getOption(optionKey);
+ if (StringUtils.isBlank(ids)) {
+ result = ImmutableList.of();
+ } else {
+ final List invalidIds = Lists.newArrayList();
+ result = Splitter.on(",").omitEmptyStrings().trimResults()
+ .splitToList(ids)
+ .stream()
+ .map(id -> {
+ try {
+ return Integer.parseInt(id);
+ } catch (Exception e) {
+ invalidIds.add(id);
+ return null;
+ }
+ })
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+
+ if (CollectionUtils.isNotEmpty(invalidIds)) {
+ log.error("Skipping invalid values found in configuration option '{}': {}", optionKey, invalidIds);
+ }
+ }
+
+ // Add to cache
+ complexOptionsCache.put(optionKey, result);
+ }
+ return result;
+ }
}
diff --git a/sumaris-core-shared/src/main/java/net/sumaris/core/config/SumarisConfigurationOption.java b/sumaris-core-shared/src/main/java/net/sumaris/core/config/SumarisConfigurationOption.java
index 3c394f6b37..58b94e9418 100644
--- a/sumaris-core-shared/src/main/java/net/sumaris/core/config/SumarisConfigurationOption.java
+++ b/sumaris-core-shared/src/main/java/net/sumaris/core/config/SumarisConfigurationOption.java
@@ -528,13 +528,24 @@ public enum SumarisConfigurationOption implements ConfigOptionDef {
Integer.class,
false),
- CLI_FILTER_TRIP_ID(
- "sumaris.cli.filter.tripId",
- n("sumaris.config.option.cli.filter.tripId.description"),
- "-1",
+ CLI_FILTER_PROGRAM_LABEL(
+ "sumaris.cli.filter.programLabel",
+ n("sumaris.config.option.cli.filter.programLabel.description"),
+ "",
+ String.class,
+ false),
+ CLI_FILTER_TRIP_IDS(
+ "sumaris.cli.filter.tripIds",
+ n("sumaris.config.option.cli.filter.tripIds.description"),
+ "",
+ Integer.class,
+ false),
+ CLI_FILTER_OPERATION_IDS(
+ "sumaris.cli.filter.operationIds",
+ n("sumaris.config.option.cli.filter.operationIds.description"),
+ "",
Integer.class,
false),
-
CSV_SEPARATOR(
"sumaris.csv.separator",
n("sumaris.config.option.csv.separator.description"),
@@ -663,26 +674,6 @@ public enum SumarisConfigurationOption implements ConfigOptionDef {
Boolean.class,
false),
- ENABLE_BATCH_TAXON_NAME(
- "sumaris.trip.operation.batch.taxonName.enable",
- n("sumaris.config.option.trip.operation.batch.taxonName.enable.description"),
- Boolean.TRUE.toString(),
- Boolean.class,
- false),
-
- ENABLE_BATCH_TAXON_GROUP(
- "sumaris.trip.operation.batch.taxonGroup.enable",
- n("sumaris.config.option.trip.operation.batch.taxonGroup.enable.description"),
- Boolean.TRUE.toString(),
- Boolean.class,
- false),
-
- BATCH_TAXON_GROUP_LABELS_NO_WEIGHT(
- "sumaris.trip.operation.batch.taxonGroups.noWeight",
- n("sumaris.config.option.trip.operation.batch.taxonGroups.noWeight.description"),
- "",
- String.class,
- false),
DB_ADAGIO_SCHEMA(
"sumaris.persistence.adagio.schema",
diff --git a/sumaris-core-shared/src/main/java/net/sumaris/core/dao/data/IPosition.java b/sumaris-core-shared/src/main/java/net/sumaris/core/dao/data/IPosition.java
new file mode 100644
index 0000000000..3186ea9435
--- /dev/null
+++ b/sumaris-core-shared/src/main/java/net/sumaris/core/dao/data/IPosition.java
@@ -0,0 +1,36 @@
+package net.sumaris.core.dao.data;
+
+/*-
+ * #%L
+ * SUMARiS:: Core
+ * %%
+ * Copyright (C) 2018 SUMARiS Consortium
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program. If not, see
+ * .
+ * #L%
+ */
+
+public interface IPosition {
+
+
+ Double getLatitude();
+
+ void setLatitude(Double latitude);
+
+ Double getLongitude();
+
+ void setLongitude(Double longitude);
+
+}
diff --git a/sumaris-core-shared/src/main/java/net/sumaris/core/dao/data/Positions.java b/sumaris-core-shared/src/main/java/net/sumaris/core/dao/data/Positions.java
new file mode 100644
index 0000000000..dae5f37dde
--- /dev/null
+++ b/sumaris-core-shared/src/main/java/net/sumaris/core/dao/data/Positions.java
@@ -0,0 +1,48 @@
+/*
+ * #%L
+ * SUMARiS
+ * %%
+ * Copyright (C) 2019 SUMARiS Consortium
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program. If not, see
+ * .
+ * #L%
+ */
+
+package net.sumaris.core.dao.data;
+
+public abstract class Positions {
+
+ protected Positions() {
+ // Helper class
+ }
+
+ public static boolean isNotNullAndValid(IPosition position) {
+
+ if (position == null || position.getLatitude() == null || position.getLongitude() == null) return false;
+
+ // Invalid lat/lon
+ if (position.getLatitude() < -90 || position.getLatitude() > 90
+ || position.getLongitude() < -180 || position.getLongitude() > 180) {
+ return false;
+ }
+
+ // OK: valid
+ return true;
+ }
+
+ public static boolean isNullOrInvalid(IPosition position) {
+ return !isNotNullAndValid(position);
+ }
+}
diff --git a/sumaris-core-shared/src/main/java/net/sumaris/core/dao/referential/location/Locations.java b/sumaris-core-shared/src/main/java/net/sumaris/core/dao/referential/location/Locations.java
index de926d156e..300747a0cf 100644
--- a/sumaris-core-shared/src/main/java/net/sumaris/core/dao/referential/location/Locations.java
+++ b/sumaris-core-shared/src/main/java/net/sumaris/core/dao/referential/location/Locations.java
@@ -220,12 +220,11 @@ public static Geometry getGeometryFromMinuteSquareLabel(String label, int minute
* Compute the statistical rectangle from the 10x10 square.
* (See doc: square_10.md)
* @param squareLabel 10x10 square
+ * @return null if invalid square label
*/
public static String convertMinuteSquareToRectangle(final String squareLabel, final int minute) {
- String calculRectangle = "";
-
if (squareLabel == null || squareLabel.length() != 8) {
- return calculRectangle;
+ return null;
}
int cadran = Integer.parseInt(squareLabel.substring(0, 1));
diff --git a/sumaris-core-shared/src/main/java/net/sumaris/core/dao/schema/DatabaseSchemaDaoImpl.java b/sumaris-core-shared/src/main/java/net/sumaris/core/dao/schema/DatabaseSchemaDaoImpl.java
index 66d0122c8b..68864e509f 100644
--- a/sumaris-core-shared/src/main/java/net/sumaris/core/dao/schema/DatabaseSchemaDaoImpl.java
+++ b/sumaris-core-shared/src/main/java/net/sumaris/core/dao/schema/DatabaseSchemaDaoImpl.java
@@ -51,10 +51,12 @@
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
+import org.hibernate.annotations.common.reflection.MetadataProvider;
import org.hibernate.boot.Metadata;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.cfg.Environment;
+import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.Oracle10gDialect;
import org.hibernate.tool.hbm2ddl.SchemaExport;
import org.hibernate.tool.hbm2ddl.SchemaUpdate;
@@ -70,6 +72,7 @@
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.stereotype.Repository;
+import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import javax.persistence.*;
import javax.persistence.metamodel.Attribute;
@@ -181,17 +184,18 @@ public void generateCreateSchemaFile(String filename) {
/** {@inheritDoc} */
@Override
public void generateCreateSchemaFile(String filename, boolean doExecute, boolean withDrop, boolean withCreate) {
+ Metadata metadata = getMetadata();
new SchemaExport()
.setDelimiter(";")
.setOutputFile(filename)
.execute(EnumSet.of(TargetType.SCRIPT),
withDrop ? SchemaExport.Action.BOTH : SchemaExport.Action.CREATE,
- getMetadata()
+ metadata
);
// Add table and columns comment
try {
- appendRemarks(filename);
+ appendRemarks(filename, metadata);
} catch (SQLException | IOException e) {
throw new SumarisTechnicalException("Error when appending comments on file", e);
}
@@ -671,38 +675,51 @@ public boolean test(String input) {
return result;
}
- protected Metadata getMetadata() {
-
- SumarisConfiguration config = getConfig();
-
- Map sessionSettings;
- SessionFactory session = null;
+ protected Map getSessionSettings(boolean configureHibernateConnectionProvider) {
if (getEntityManager() != null) {
- session = getEntityManager().unwrap(Session.class).getSessionFactory();
+ SessionFactory session = getEntityManager().unwrap(Session.class).getSessionFactory();
+
+ if (session != null) {
+ // Allow Hibernate to get the connection
+ if (configureHibernateConnectionProvider) {
+ HibernateConnectionProvider.setDataSource(getDataSource());
+ }
+ return session.getProperties();
+ }
}
- if (session == null) {
+
+ return getSessionSettings(getConfig().getConnectionProperties(), configureHibernateConnectionProvider);
+ }
+ protected Map getSessionSettings(@Nullable Properties connectionProperties, boolean configureHibernateConnectionProvider) {
+
+
+ // Allow Hibernate to get the connection
+ if (configureHibernateConnectionProvider) {
try {
- // To be able to retrieve connection from datasource
- Connection conn = Daos.createConnection(config.getConnectionProperties());
+ Connection conn = Daos.createConnection(connectionProperties);
HibernateConnectionProvider.setConnection(conn);
} catch (SQLException e) {
- throw new SumarisTechnicalException("Could not open connection: " + config.getJdbcURL());
+ throw new SumarisTechnicalException("Could not open connection: " + connectionProperties.get(Environment.URL));
}
+ }
- sessionSettings = Maps.newHashMap();
- sessionSettings.put(Environment.DIALECT, config.getHibernateDialect());
- sessionSettings.put(Environment.DRIVER, config.getJdbcDriver());
- sessionSettings.put(Environment.URL, config.getJdbcURL());
- sessionSettings.put(Environment.IMPLICIT_NAMING_STRATEGY, HibernateImplicitNamingStrategy.class.getName());
+ Map sessionSettings = Maps.newHashMap();
+ sessionSettings.put(Environment.DIALECT, connectionProperties.get(Environment.DIALECT));
+ sessionSettings.put(Environment.DRIVER, connectionProperties.get(Environment.DRIVER));
+ sessionSettings.put(Environment.URL, connectionProperties.get(Environment.URL));
+ sessionSettings.put(Environment.IMPLICIT_NAMING_STRATEGY, HibernateImplicitNamingStrategy.class.getName());
- sessionSettings.put(Environment.PHYSICAL_NAMING_STRATEGY, HibernatePhysicalNamingStrategy.class.getName());
- //sessionSettings.put(Environment.PHYSICAL_NAMING_STRATEGY, "org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy");
- }
- else {
- // To be able to retrieve connection from datasource
- HibernateConnectionProvider.setDataSource(getDataSource());
- sessionSettings = session.getProperties();
- }
+ sessionSettings.put(Environment.PHYSICAL_NAMING_STRATEGY, HibernatePhysicalNamingStrategy.class.getName());
+ //sessionSettings.put(Environment.PHYSICAL_NAMING_STRATEGY, "org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy");
+
+ return sessionSettings;
+ }
+
+ protected Metadata getMetadata() {
+ return getMetadata(getSessionSettings(true));
+ }
+
+ protected Metadata getMetadata(Map sessionSettings) {
MetadataSources metadata = new MetadataSources(new StandardServiceRegistryBuilder()
.applySettings(sessionSettings)
@@ -754,37 +771,53 @@ private String getTimezoneQuery(Connection connection) {
throw new SumarisTechnicalException("Cannot generate Timezone query : not implemented for this database type");
}
- private void appendRemarks(String filename) throws SQLException, IOException {
+ private void appendRemarks(String filename, Metadata metadata) throws SQLException, IOException {
List linesToAppend = new ArrayList<>();
- String schemaName = dataSource.getConnection().getSchema();
- getEntityManager().getEntityManagerFactory().getMetamodel().getEntities().stream()
- .sorted(Comparator.comparing(EntityType::getName))
- .forEach(entityType -> {
- Table table = entityType.getJavaType().getAnnotation(Table.class);
- Comment tableComment = entityType.getJavaType().getAnnotation(Comment.class);
- if (table != null) {
- if (tableComment != null) {
- Optional.ofNullable(getTableCommentQuery(schemaName, table.name(), tableComment.value())).ifPresent(linesToAppend::add);
- }
- // iterate attributes
- entityType.getAttributes().stream()
- .sorted(Comparator.comparing(Attribute::getName))
- .forEach(attribute -> {
- if (attribute.getJavaMember() instanceof Field) {
- Field field = (Field) attribute.getJavaMember();
- Column column = field.getAnnotation(Column.class);
- JoinColumn joinColumn = field.getAnnotation(JoinColumn.class);
- Comment columnComment = field.getAnnotation(Comment.class);
- String columnName = Optional.ofNullable(column).map(Column::name).orElse(
- Optional.ofNullable(joinColumn).map(JoinColumn::name).orElse(null)
- );
- if (columnName != null && columnComment != null) {
- Optional.ofNullable(getColumnCommentQuery(schemaName, table.name(), columnName, columnComment.value())).ifPresent(linesToAppend::add);
+
+ // Prepare hibernate connection (will be used by buildSessionFactory() )
+ Connection connection;
+ if (dataSource != null) {
+ connection = DataSourceUtils.getConnection(dataSource);
+ HibernateConnectionProvider.setDataSource(dataSource);
+ }
+ else {
+ connection = Daos.createConnection(getConfig().getConnectionProperties());
+ HibernateConnectionProvider.setConnection(connection);
+ }
+
+ try {
+ String schemaName = connection.getSchema();
+ metadata.buildSessionFactory().getMetamodel().getEntities().stream()
+ .sorted(Comparator.comparing(EntityType::getName))
+ .forEach(entityType -> {
+ Table table = entityType.getJavaType().getAnnotation(Table.class);
+ Comment tableComment = entityType.getJavaType().getAnnotation(Comment.class);
+ if (table != null) {
+ if (tableComment != null) {
+ Optional.ofNullable(getTableCommentQuery(schemaName, table.name(), tableComment.value())).ifPresent(linesToAppend::add);
+ }
+ // iterate attributes
+ entityType.getAttributes().stream()
+ .sorted(Comparator.comparing(Attribute::getName))
+ .forEach(attribute -> {
+ if (attribute.getJavaMember() instanceof Field) {
+ Field field = (Field) attribute.getJavaMember();
+ Column column = field.getAnnotation(Column.class);
+ JoinColumn joinColumn = field.getAnnotation(JoinColumn.class);
+ Comment columnComment = field.getAnnotation(Comment.class);
+ String columnName = Optional.ofNullable(column).map(Column::name).orElse(
+ Optional.ofNullable(joinColumn).map(JoinColumn::name).orElse(null)
+ );
+ if (columnName != null && columnComment != null) {
+ Optional.ofNullable(getColumnCommentQuery(schemaName, table.name(), columnName, columnComment.value())).ifPresent(linesToAppend::add);
+ }
}
- }
- });
- }
- });
+ });
+ }
+ });
+ } finally {
+ DataSourceUtils.releaseConnection(connection, dataSource);
+ }
if (!linesToAppend.isEmpty()) {
Files.write(Paths.get(filename), linesToAppend, StandardOpenOption.APPEND);
@@ -792,16 +825,40 @@ private void appendRemarks(String filename) throws SQLException, IOException {
}
private String getTableCommentQuery(String schemaName, String tableName, String comment) {
- if (Daos.getDialect(getEntityManager()) instanceof Oracle10gDialect) {
+ if (isOracleDialect()) {
return String.format("comment on table %s.%s is '%s';", schemaName, tableName, comment.replaceAll("'", "''"));
}
return null;
}
private String getColumnCommentQuery(String schemaName, String tableName, String columnName, String comment) {
- if (Daos.getDialect(getEntityManager()) instanceof Oracle10gDialect) {
+ if (isOracleDialect()) {
return String.format("comment on column %s.%s.%s is '%s';", schemaName, tableName, columnName, comment.replaceAll("'", "''"));
}
return null;
}
+
+ private boolean isOracleDialect() {
+ try {
+ return getHibernateDialect() instanceof Oracle10gDialect;
+ }
+ catch (InstantiationException e) {
+ throw new SumarisTechnicalException(e);
+ }
+ }
+
+ private Dialect getHibernateDialect() throws InstantiationException {
+ EntityManager em = getEntityManager();
+ if (em != null) {
+ return Daos.getDialect(em);
+ }
+ String dialectClassName = getConfig().getHibernateDialect();
+ if (StringUtils.isBlank(dialectClassName)) return null;
+ try {
+ Class dialectClass = Class.forName(getConfig().getHibernateDialect());
+ return ((Dialect) dialectClass.getConstructor().newInstance());
+ } catch (Exception e) {
+ throw new InstantiationException(String.format("Cannot instantiate class %s: %s", dialectClassName, e.getMessage()));
+ }
+ }
}
diff --git a/sumaris-core-shared/src/main/java/net/sumaris/core/dao/technical/cache/CacheTTL.java b/sumaris-core-shared/src/main/java/net/sumaris/core/dao/technical/cache/CacheTTL.java
index 5c959b0a3b..46014c8c6a 100644
--- a/sumaris-core-shared/src/main/java/net/sumaris/core/dao/technical/cache/CacheTTL.java
+++ b/sumaris-core-shared/src/main/java/net/sumaris/core/dao/technical/cache/CacheTTL.java
@@ -47,7 +47,13 @@ public enum CacheTTL {
MEDIUM(60 * 60), // 1 h
LONG(12 * 60 * 60), // 12 h
- ETERNAL(24 * 60 * 60) // 1 day
+ DAY(24 * 60 * 60), // 1 day
+
+ // 2 days ~ almost eternal! :)
+ // - We do not use infinite duration, because some SQL operations can have been done in the DB (SQL scripts, etc.)
+ // - We need more than one day, to avoid cache reload/timeout each morning, when user comme back to the office
+ ETERNAL(24 * 60 * 60 * 2)
+
;
private Duration value;
diff --git a/sumaris-core-shared/src/main/java/net/sumaris/core/model/ITreeNodeEntity.java b/sumaris-core-shared/src/main/java/net/sumaris/core/model/ITreeNodeEntity.java
index 612c65d5d0..3a2554f9c2 100644
--- a/sumaris-core-shared/src/main/java/net/sumaris/core/model/ITreeNodeEntity.java
+++ b/sumaris-core-shared/src/main/java/net/sumaris/core/model/ITreeNodeEntity.java
@@ -50,6 +50,11 @@ default boolean hasChildren() {
return CollectionUtils.isNotEmpty(getChildren());
}
+ @JsonIgnore
+ default boolean isLeaf() {
+ return CollectionUtils.isEmpty(getChildren());
+ }
+
@JsonIgnore
default boolean hasParent() {
return getParent() != null;
diff --git a/sumaris-core-shared/src/main/java/net/sumaris/core/model/annotation/EntityEnums.java b/sumaris-core-shared/src/main/java/net/sumaris/core/model/annotation/EntityEnums.java
index bc73b47b71..d3409f35f0 100644
--- a/sumaris-core-shared/src/main/java/net/sumaris/core/model/annotation/EntityEnums.java
+++ b/sumaris-core-shared/src/main/java/net/sumaris/core/model/annotation/EntityEnums.java
@@ -22,14 +22,18 @@
package net.sumaris.core.model.annotation;
+import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import lombok.AllArgsConstructor;
import lombok.Data;
+import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import net.sumaris.core.config.SumarisConfiguration;
import net.sumaris.core.util.Beans;
import net.sumaris.core.util.StringUtils;
+import org.apache.commons.collections4.CollectionUtils;
import org.nuiton.config.ConfigOptionDef;
+import org.nuiton.i18n.I18n;
import org.reflections.Reflections;
import java.util.List;
@@ -98,6 +102,37 @@ public static ConfigOptionDef[] getEntityEnumAsOptions(SumarisConfiguration conf
return options.toArray(new ConfigOptionDef[options.size()]);
}
+ /**
+ * Check if an entity enumeration (@EntityEnum) has been resolved. Typically, if id!=-1
+ * @param enumerations
+ */
+ public static void checkResolved(String i18nMessageKey, @NonNull IEntityEnum... enumerations) {
+ List invalidEnumerationNames = Beans.getStream(enumerations)
+ .filter(EntityEnums::isUnresolved)
+ .map(EntityEnums::name)
+ .toList();
+
+ if (CollectionUtils.isNotEmpty(invalidEnumerationNames)) {
+ throw new IllegalArgumentException(I18n.t(i18nMessageKey, Joiner.on(",").join(invalidEnumerationNames)));
+ }
+ }
+
+ public static void checkResolved(@NonNull IEntityEnum... enumerations) {
+ checkResolved("sumaris.error.enumeration.unresolved", enumerations);
+ }
+
+ public static String name(IEntityEnum enumeration) {
+ return Beans.getProperty((Object)enumeration, "name").toString();
+ }
+
+ public static boolean isUnresolved(IEntityEnum enumeration) {
+ try {
+ Object id = Beans.getProperty((Object)enumeration, "id");
+ return id == null || ((id instanceof Integer) && ((Integer)id) == UNRESOLVED_ENUMERATION_ID);
+ } catch (Exception e) {
+ return false;
+ }
+ }
@Data
@AllArgsConstructor
diff --git a/sumaris-core-shared/src/main/java/net/sumaris/core/model/annotation/IEntityEnum.java b/sumaris-core-shared/src/main/java/net/sumaris/core/model/annotation/IEntityEnum.java
new file mode 100644
index 0000000000..7efeffa781
--- /dev/null
+++ b/sumaris-core-shared/src/main/java/net/sumaris/core/model/annotation/IEntityEnum.java
@@ -0,0 +1,27 @@
+/*
+ * #%L
+ * SUMARiS
+ * %%
+ * Copyright (C) 2019 SUMARiS Consortium
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program. If not, see
+ * .
+ * #L%
+ */
+
+package net.sumaris.core.model.annotation;
+
+public interface IEntityEnum {
+
+}
\ No newline at end of file
diff --git a/sumaris-core-shared/src/main/java/net/sumaris/core/service/referential/location/LocationByPositionService.java b/sumaris-core-shared/src/main/java/net/sumaris/core/service/referential/location/LocationByPositionService.java
index c36090c2bb..e15a1f0122 100644
--- a/sumaris-core-shared/src/main/java/net/sumaris/core/service/referential/location/LocationByPositionService.java
+++ b/sumaris-core-shared/src/main/java/net/sumaris/core/service/referential/location/LocationByPositionService.java
@@ -26,6 +26,8 @@
import org.springframework.transaction.annotation.Transactional;
+import java.util.Optional;
+
/**
* Service used to access locations
*
@@ -39,9 +41,9 @@ public interface LocationByPositionService {
*
* @param latitude a latitude (in decimal degrees - WG84)
* @param longitude a longitude (in decimal degrees - WG84)
- * @return A location label (corresponding to a statistical rectangle), or null if no statistical rectangle exists for this position
+ * @return A location label (corresponding to a statistical rectangle), or empty if no statistical rectangle exists for this position
*/
- String getLocationLabelByLatLong(Number latitude, Number longitude);
+ Optional getStatisticalRectangleLabelByLatLong(Number latitude, Number longitude);
/**
* Return a location Id, from a longitude and a latitude (in decimal degrees - WG84).
@@ -49,7 +51,7 @@ public interface LocationByPositionService {
*
* @param latitude a latitude (in decimal degrees - WG84)
* @param longitude a longitude (in decimal degrees - WG84)
- * @return A location Id (corresponding to a statistical rectangle), or null if no statistical rectangle exists for this position
+ * @return A location Id (corresponding to a statistical rectangle), or empty if no statistical rectangle exists for this position
*/
- Integer getLocationIdByLatLong(Number latitude, Number longitude);
+ Optional getStatisticalRectangleIdByLatLong(Number latitude, Number longitude);
}
diff --git a/sumaris-core-shared/src/main/java/net/sumaris/core/util/Beans.java b/sumaris-core-shared/src/main/java/net/sumaris/core/util/Beans.java
index d78cfb7316..54de91eb17 100644
--- a/sumaris-core-shared/src/main/java/net/sumaris/core/util/Beans.java
+++ b/sumaris-core-shared/src/main/java/net/sumaris/core/util/Beans.java
@@ -462,14 +462,14 @@ public static Comparator unsortedComparator() {
return (o1, o2) -> 0;
}
- public static T clone(T source, Class sourceClass) {
- T target = newInstance(sourceClass);
+ public static R clone(T source, Class resultClass) {
+ R target = newInstance(resultClass);
copyProperties(source, target);
return target;
}
- public static T clone(T source, Class sourceClass, String... excludedPropertyNames) {
- T target = newInstance(sourceClass);
+ public static R clone(T source, Class resultClass, String... excludedPropertyNames) {
+ R target = newInstance(resultClass);
copyProperties(source, target, excludedPropertyNames);
return target;
}
@@ -509,7 +509,7 @@ public static void copyProperties(S source, T target, String... exceptPro
PropertyDescriptor targetDescriptor = targetProperties.get(pd.getName());
boolean ignored = targetDescriptor == null
|| !targetDescriptor.getPropertyType().isAssignableFrom(pd.getPropertyType())
- || Collection.class.isAssignableFrom(pd.getPropertyType())
+ || Collection.class.isAssignableFrom(pd.getPropertyType()) // Ignore List, Collection, etc
|| targetDescriptor.getWriteMethod() == null;
return ignored;
})
diff --git a/sumaris-core-shared/src/main/java/net/sumaris/core/util/Dates.java b/sumaris-core-shared/src/main/java/net/sumaris/core/util/Dates.java
index f182afe4f4..472866218c 100644
--- a/sumaris-core-shared/src/main/java/net/sumaris/core/util/Dates.java
+++ b/sumaris-core-shared/src/main/java/net/sumaris/core/util/Dates.java
@@ -25,6 +25,7 @@
*/
import com.google.common.base.Preconditions;
+import lombok.NonNull;
import net.sumaris.core.exception.SumarisTechnicalException;
import org.apache.commons.lang3.StringUtils;
import org.nuiton.util.DateUtil;
@@ -56,8 +57,7 @@ public class Dates extends org.apache.commons.lang3.time.DateUtils{
* @param amount the amount to remove, in month
* @return a new date (= the given date - amount in month)
*/
- public static Date removeMonth(Date date, int amount) {
- Preconditions.checkNotNull(date);
+ public static Date removeMonth(@NonNull Date date, int amount) {
Preconditions.checkArgument(amount > 0);
// Compute the start date
@@ -69,6 +69,28 @@ public static Date removeMonth(Date date, int amount) {
return calendar.getTime();
}
+ /**
+ * Extract month of a date
+ * @param date a not null date
+ * @return 0 for january, 11 for december
+ */
+ public static Integer getMonth(@NonNull Date date) {
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTimeInMillis(date.getTime());
+ return calendar.get(Calendar.MONTH);
+ }
+
+ /**
+ * Extract month of a date
+ * @param date a not null date
+ * @return 1 for january, 12 for december
+ */
+ public static Integer getYear(@NonNull Date date) {
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTimeInMillis(date.getTime());
+ return calendar.get(Calendar.YEAR);
+ }
+
/**
* Get the number of days between two dates
*
diff --git a/sumaris-core-shared/src/main/java/net/sumaris/core/util/Geometries.java b/sumaris-core-shared/src/main/java/net/sumaris/core/util/Geometries.java
index f45a71b51f..13d87baf00 100644
--- a/sumaris-core-shared/src/main/java/net/sumaris/core/util/Geometries.java
+++ b/sumaris-core-shared/src/main/java/net/sumaris/core/util/Geometries.java
@@ -261,4 +261,6 @@ public static String getDistanceInMilles(Number distance) {
}
return distanceText;
}
+
+
}
diff --git a/sumaris-core-shared/src/main/java/net/sumaris/core/util/Numbers.java b/sumaris-core-shared/src/main/java/net/sumaris/core/util/Numbers.java
index ef1ec60525..372175ad8b 100644
--- a/sumaris-core-shared/src/main/java/net/sumaris/core/util/Numbers.java
+++ b/sumaris-core-shared/src/main/java/net/sumaris/core/util/Numbers.java
@@ -22,6 +22,9 @@
package net.sumaris.core.util;
+import javax.annotation.Nullable;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
@@ -48,4 +51,31 @@ public static String format(Number value) {
formatter.setDecimalFormatSymbols(new DecimalFormatSymbols(Locale.FRANCE));
return formatter.format(value);
}
+
+ public static BigDecimal firstNotNullAsBigDecimal(Number... values) {
+ for (Number v: values) {
+ if (v != null) {
+ if (v instanceof BigDecimal) return (BigDecimal)v;
+ return new BigDecimal(v.doubleValue());
+ }
+ }
+ return null;
+ }
+
+ public static Double asDouble(@Nullable BigDecimal value) {
+ if (value != null) return value.doubleValue();
+ return null;
+ }
+
+ public static double doubleValue(@Nullable BigDecimal value, double defaultValue) {
+ if (value != null) return value.doubleValue();
+ return defaultValue;
+ }
+
+ public static Double round(BigDecimal value, int scale) {
+ if (value != null) return value
+ .divide(new BigDecimal(1d), scale, RoundingMode.HALF_UP)
+ .doubleValue();
+ return null;
+ }
}
diff --git a/sumaris-core-shared/src/main/java/net/sumaris/core/util/UnicodeChars.java b/sumaris-core-shared/src/main/java/net/sumaris/core/util/UnicodeChars.java
index 09361901ae..f24eaa6698 100644
--- a/sumaris-core-shared/src/main/java/net/sumaris/core/util/UnicodeChars.java
+++ b/sumaris-core-shared/src/main/java/net/sumaris/core/util/UnicodeChars.java
@@ -32,4 +32,6 @@ public class UnicodeChars {
public static final String SUM = "\u2211";
public static final String ARROW_DOWN = "\uA71C";
+
+ public static final String ARROW_UP = "\uA71B";
}
diff --git a/sumaris-core-shared/src/main/java/net/sumaris/core/util/conversion/UnitConversions.java b/sumaris-core-shared/src/main/java/net/sumaris/core/util/conversion/UnitConversions.java
new file mode 100644
index 0000000000..240095ef8a
--- /dev/null
+++ b/sumaris-core-shared/src/main/java/net/sumaris/core/util/conversion/UnitConversions.java
@@ -0,0 +1,53 @@
+/*
+ * #%L
+ * SUMARiS
+ * %%
+ * Copyright (C) 2019 SUMARiS Consortium
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program. If not, see
+ * .
+ * #L%
+ */
+
+package net.sumaris.core.util.conversion;
+
+import lombok.NonNull;
+
+public abstract class UnitConversions {
+ protected UnitConversions() {
+ // helper class
+ }
+
+ public static double weightToKgConversion(@NonNull String unitSymbol) {
+ return switch (unitSymbol) {
+ case "t" -> 1000;
+ case "kg" -> 1;
+ case "g" -> 1d/1000;
+ case "mg" -> 1d/1000/1000;
+ default -> throw new IllegalStateException("Unexpected value: " + unitSymbol);
+ };
+ }
+
+ public static double lengthToMeterConversion(@NonNull String unitSymbol) {
+ return switch (unitSymbol) {
+ case "km" -> 1000;
+ case "m" -> 1;
+ case "dm" -> 1d/10;
+ case "cm" -> 1d/100;
+ case "mm" -> 1d/1000;
+ case "μm" -> 1d/1000/1000;
+ default -> throw new IllegalStateException("Unexpected value: " + unitSymbol);
+ };
+ }
+}
diff --git a/sumaris-core-shared/src/main/java/net/sumaris/core/util/sound/SoundUtils.java b/sumaris-core-shared/src/main/java/net/sumaris/core/util/sound/SoundUtils.java
new file mode 100644
index 0000000000..b658f615ea
--- /dev/null
+++ b/sumaris-core-shared/src/main/java/net/sumaris/core/util/sound/SoundUtils.java
@@ -0,0 +1,152 @@
+/*
+ * #%L
+ * SUMARiS
+ * %%
+ * Copyright (C) 2019 SUMARiS Consortium
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program. If not, see
+ * .
+ * #L%
+ */
+
+package net.sumaris.core.util.sound;
+
+import com.google.common.base.Preconditions;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.sound.sampled.*;
+import java.io.File;
+import java.io.IOException;
+
+@Slf4j
+public class SoundUtils {
+
+ public static float SAMPLE_RATE = 8000f;
+
+ public static void tone(int hz, int msecs)
+ throws LineUnavailableException
+ {
+ tone(hz, msecs, 1.0);
+ }
+
+ public static void tone(int hz, int msecs, double vol)
+ throws LineUnavailableException
+ {
+ byte[] buf = new byte[1];
+ AudioFormat af =
+ new AudioFormat(
+ SAMPLE_RATE, // sampleRate
+ 8, // sampleSizeInBits
+ 1, // channels
+ true, // signed
+ false); // bigEndian
+ SourceDataLine sdl = AudioSystem.getSourceDataLine(af);
+ sdl.open(af);
+ sdl.start();
+ for (int i=0; i < msecs*8; i++) {
+ double angle = i / (SAMPLE_RATE / hz) * 2.0 * Math.PI;
+ buf[0] = (byte)(Math.sin(angle) * 127.0 * vol);
+ sdl.write(buf,0,1);
+ }
+ sdl.drain();
+ sdl.stop();
+ sdl.close();
+ }
+
+ public static void playError(int count) {
+
+ int counter = 0;
+ try {
+ while (counter < count) {
+ SoundUtils.tone(450, 800, 0.7);
+ Thread.sleep(800);
+ SoundUtils.tone(400, 800, 0.7);
+ Thread.sleep(800);
+ SoundUtils.tone(400, 2000);
+ Thread.sleep(3000);
+ counter++;
+ }
+ }
+ catch (Exception e) {
+ log.debug("Cannot play error sound: " + e.getMessage());
+ }
+ }
+
+ public static void playWaiting(int count) {
+ int counter = 0;
+ try {
+ while (counter < count) {
+ SoundUtils.tone(100, 100);
+ Thread.sleep(200);
+ SoundUtils.tone(500, 700);
+ Thread.sleep(800);
+ SoundUtils.tone(500, 1000, 0.4);
+ Thread.sleep(3000);
+ counter++;
+ }
+ }
+ catch (Exception e) {
+ log.debug("Cannot play error sound: " + e.getMessage());
+ }
+ }
+
+ private void playSound(File f) {
+ Preconditions.checkArgument(f.exists());
+
+ Runnable r = new Runnable() {
+ private File f;
+
+ public void run() {
+ playSoundInternal(this.f);
+ }
+
+ public Runnable setFile(File f) {
+ this.f = f;
+ return this;
+ }
+ }.setFile(f);
+
+ new Thread(r).start();
+
+ }
+
+ private void playSoundInternal(File f) {
+
+ try {
+ AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(f);
+ try {
+ Clip clip = AudioSystem.getClip();
+ clip.open(audioInputStream);
+ try {
+ clip.start();
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ clip.drain();
+ } finally {
+ clip.close();
+ }
+ } catch (LineUnavailableException | IOException e) {
+ throw new RuntimeException(e);
+ } finally {
+ audioInputStream.close();
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/sumaris-core-shared/src/main/resources/i18n/sumaris-core-shared_en_GB.properties b/sumaris-core-shared/src/main/resources/i18n/sumaris-core-shared_en_GB.properties
index 726eed6f7e..60a77de3ef 100644
--- a/sumaris-core-shared/src/main/resources/i18n/sumaris-core-shared_en_GB.properties
+++ b/sumaris-core-shared/src/main/resources/i18n/sumaris-core-shared_en_GB.properties
@@ -18,7 +18,10 @@ sumaris.config.option.attribute.separator.description=
sumaris.config.option.basedir.description=
sumaris.config.option.cache.directory.description=
sumaris.config.option.cli.daemon.description=
+sumaris.config.option.cli.filter.operationIds.description=
+sumaris.config.option.cli.filter.programLabel.description=
sumaris.config.option.cli.filter.tripId.description=
+sumaris.config.option.cli.filter.tripIds.description=
sumaris.config.option.cli.filter.year.description=
sumaris.config.option.cli.output.file.description=
sumaris.config.option.cli.output.force.description=
@@ -107,7 +110,8 @@ sumaris.config.option.trip.operation.batch.taxonGroups.noWeight.description=
sumaris.config.option.trip.operation.batch.taxonName.enable.description=
sumaris.config.option.value.separator.description=
sumaris.config.parse.error=
-sumaris.error.account.unauthorized=
+sumaris.error.account.unauthorized=Unauthorized
+sumaris.error.enumeration.unresolved=Enumeration {%s} not resolved. Please define it using configuration option.
sumaris.persistence.bindingQuery.error=
sumaris.persistence.bindingQuery.error.log=
sumaris.persistence.compactDatabase.error=
diff --git a/sumaris-core-shared/src/main/resources/i18n/sumaris-core-shared_fr_FR.properties b/sumaris-core-shared/src/main/resources/i18n/sumaris-core-shared_fr_FR.properties
index f72742313a..2c97ef3a09 100644
--- a/sumaris-core-shared/src/main/resources/i18n/sumaris-core-shared_fr_FR.properties
+++ b/sumaris-core-shared/src/main/resources/i18n/sumaris-core-shared_fr_FR.properties
@@ -18,7 +18,10 @@ sumaris.config.option.attribute.separator.description=
sumaris.config.option.basedir.description=
sumaris.config.option.cache.directory.description=
sumaris.config.option.cli.daemon.description=
+sumaris.config.option.cli.filter.operationIds.description=
+sumaris.config.option.cli.filter.programLabel.description=
sumaris.config.option.cli.filter.tripId.description=
+sumaris.config.option.cli.filter.tripIds.description=
sumaris.config.option.cli.filter.year.description=
sumaris.config.option.cli.output.file.description=
sumaris.config.option.cli.output.force.description=
@@ -139,7 +142,8 @@ sumaris.config.option.trip.operation.batch.taxonGroups.noWeight.description=
sumaris.config.option.trip.operation.batch.taxonName.enable.description=
sumaris.config.option.value.separator.description=
sumaris.config.parse.error=Erreur lors de la lecture de la ligne de commande
-sumaris.error.account.unauthorized=
+sumaris.error.account.unauthorized=Non autorisé
+sumaris.error.enumeration.unresolved=Enumération {%s} non résolue, à partir de la base de données. Veuillez la définir par une option de configuration.
sumaris.persistence.bindingQuery.error=
sumaris.persistence.bindingQuery.error.log=
sumaris.persistence.compactDatabase.error=
diff --git a/sumaris-core-shared/src/test/java/net/sumaris/core/dao/referential/location/LocationsTest.java b/sumaris-core-shared/src/test/java/net/sumaris/core/dao/referential/location/LocationsTest.java
index 1d30fe5d43..c9d31ffc77 100644
--- a/sumaris-core-shared/src/test/java/net/sumaris/core/dao/referential/location/LocationsTest.java
+++ b/sumaris-core-shared/src/test/java/net/sumaris/core/dao/referential/location/LocationsTest.java
@@ -48,6 +48,14 @@ public void getRectangleLabelByLatLong() {
String label = Locations.getRectangleLabelByLatLong(47.6f, -5.05f);
assertEquals("24E4", label);
+ // Check label = 25E5
+ label = Locations.getRectangleLabelByLatLong(48f, -5.01f);
+ assertEquals("25E5", label);
+
+ // Check label = 25E
+ label = Locations.getRectangleLabelByLatLong(48.001f, -5.0547f);
+ assertEquals("25E4", label);
+
// Check label with a position inside the Mediterranean sea
label = Locations.getRectangleLabelByLatLong(42.27f, 5.4f);
assertEquals("M24C2", label);
diff --git a/sumaris-core-shared/src/test/java/net/sumaris/core/util/SoundUtilsTest.java b/sumaris-core-shared/src/test/java/net/sumaris/core/util/SoundUtilsTest.java
new file mode 100644
index 0000000000..f134c3a8ca
--- /dev/null
+++ b/sumaris-core-shared/src/test/java/net/sumaris/core/util/SoundUtilsTest.java
@@ -0,0 +1,36 @@
+/*
+ * #%L
+ * SUMARiS
+ * %%
+ * Copyright (C) 2019 SUMARiS Consortium
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program. If not, see
+ * .
+ * #L%
+ */
+
+package net.sumaris.core.util;
+
+import net.sumaris.core.util.sound.SoundUtils;
+import org.junit.Test;
+
+public class SoundUtilsTest {
+
+
+ @Test
+ public void playError() {
+ SoundUtils.playError(10);
+ }
+
+}
\ No newline at end of file
diff --git a/sumaris-core/pom.xml b/sumaris-core/pom.xml
index 0157d61f8c..c9741dbba5 100644
--- a/sumaris-core/pom.xml
+++ b/sumaris-core/pom.xml
@@ -4,7 +4,7 @@
net.sumaris
sumaris-pod
- 2.0.6
+ 2.1.0
sumaris-core
diff --git a/sumaris-core/src/main/java/net/sumaris/cli/action/data/DenormalizeTripsAction.java b/sumaris-core/src/main/java/net/sumaris/cli/action/data/DenormalizeTripsAction.java
index 0afb113210..ed9b67f9cd 100644
--- a/sumaris-core/src/main/java/net/sumaris/cli/action/data/DenormalizeTripsAction.java
+++ b/sumaris-core/src/main/java/net/sumaris/cli/action/data/DenormalizeTripsAction.java
@@ -27,8 +27,9 @@
import net.sumaris.core.model.IProgressionModel;
import net.sumaris.core.model.ProgressionModel;
import net.sumaris.core.service.ServiceLocator;
-import net.sumaris.core.service.data.denormalize.DenormalizeTripService;
+import net.sumaris.core.service.data.denormalize.DenormalizedTripService;
import net.sumaris.core.util.Dates;
+import net.sumaris.core.util.sound.SoundUtils;
import net.sumaris.core.vo.filter.TripFilterVO;
@Slf4j
@@ -39,11 +40,14 @@ public class DenormalizeTripsAction {
*/
public void run() {
SumarisConfiguration config = SumarisConfiguration.getInstance();
- DenormalizeTripService tripService = ServiceLocator.instance().getService("denormalizeTripService", DenormalizeTripService.class);
+ DenormalizedTripService tripService = ServiceLocator.instance().getService("denormalizeTripService", DenormalizedTripService.class);
// Create filter
TripFilterVO.TripFilterVOBuilder filterBuilder = TripFilterVO.builder()
- .tripId(config.getCliFilterTripId());
+ .includedIds(config.getCliFilterTripIds().toArray(Integer[]::new))
+ .operationIds(config.getCliFilterOperationIds().toArray(Integer[]::new))
+ .programLabel(config.getCliFilterProgramLabel());
+
Integer year = config.getCliFilterYear();
if (year != null && year > 1970) {
filterBuilder.startDate(Dates.getFirstDayOfYear(year))
@@ -55,5 +59,7 @@ public void run() {
progression.addPropertyChangeListener(IProgressionModel.Fields.MESSAGE, (event) -> log.info(progression.getMessage()));
tripService.denormalizeByFilter(filterBuilder.build(), progression);
+ // Play a beep
+ SoundUtils.playWaiting(2);
}
}
diff --git a/sumaris-core/src/main/java/net/sumaris/core/config/CacheConfiguration.java b/sumaris-core/src/main/java/net/sumaris/core/config/CacheConfiguration.java
index 60223faec6..28508b45b7 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/config/CacheConfiguration.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/config/CacheConfiguration.java
@@ -43,7 +43,7 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer;
import org.springframework.cache.annotation.CachingConfigurerSupport;
-import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.cache.interceptor.SimpleKey;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -72,6 +72,8 @@ public interface Names {
// Person
String PERSON_BY_ID = "net.sumaris.core.dao.administration.user.personById";
String PERSON_BY_PUBKEY = "net.sumaris.core.dao.administration.user.personByPubkey";
+ String PERSON_BY_USERNAME = "net.sumaris.core.dao.administration.user.personByUsername";
+
String PERSON_AVATAR_BY_PUBKEY = "net.sumaris.core.dao.administration.user.personAvatarByPubkey";
// Location
@@ -81,12 +83,11 @@ public interface Names {
// Program
String PROGRAM_BY_ID = "net.sumaris.core.dao.administration.programStrategy.programById";
String PROGRAM_BY_LABEL = "net.sumaris.core.dao.administration.programStrategy.programByLabel";
+ String PROGRAM_BY_LABEL_AND_OPTIONS = "net.sumaris.core.dao.administration.programStrategy.programByLabelAndOptions";
String PROGRAM_IDS_BY_USER_ID = "net.sumaris.core.dao.administration.programStrategy.programIdsByUserId";
// Program privilege
String PROGRAM_PRIVILEGE_BY_ID = "net.sumaris.core.dao.administration.programStrategy.programPrivilegeById";
- // Program property
- String PROGRAM_PROPERTY_BY_LABEL = "net.sumaris.core.dao.administration.programStrategy.programByLabel";
// Strategy
String STRATEGY_BY_ID = "net.sumaris.core.dao.administration.programStrategy.strategyById";
@@ -114,6 +115,13 @@ public interface Names {
String TAXONONOMIC_LEVEL_BY_ID = "net.sumaris.core.dao.referential.taxon.taxonomicLevelById";
String REFERENCE_TAXON_ID_BY_TAXON_NAME_ID = "net.sumaris.core.dao.referential.taxon.referenceTaxonIdByTaxonNameId";
+ // Weight length conversion
+ String WEIGHT_LENGTH_CONVERSION_FIRST_BY_FILTER = "net.sumaris.core.service.referential.conversion.weightLengthConversion.findFirstByFilter";
+ String WEIGHT_LENGTH_CONVERSION_IS_LENGTH_PARAMETER_ID = "net.sumaris.core.service.referential.conversion.weightLengthConversion.isLengthParameterId";
+ String WEIGHT_LENGTH_CONVERSION_IS_LENGTH_PMFM_ID = "net.sumaris.core.service.referential.conversion.weightLengthConversion.isLengthPmfmId";
+
+ String ROUND_WEIGHT_CONVERSION_FIRST_BY_FILTER = "net.sumaris.core.service.referential.conversion.roundWeightConversion.findFirstByFilter";
+
// Vessel
String VESSEL_SNAPSHOT_BY_ID_AND_DATE = "net.sumaris.core.service.data.vessel.vesselSnapshotByIdAndDate";
String VESSEL_SNAPSHOTS_BY_FILTER = "net.sumaris.core.service.data.vessel.vesselSnapshotByFilter";
@@ -128,8 +136,10 @@ public interface Names {
String GEAR_BY_ID = "net.sumaris.core.dao.referential.gear.gearById";
String ANALYTIC_REFERENCES_BY_FILTER = "net.sumaris.core.dao.referential.analyticReferenceByFilter";
+
// Data
String MAIN_UNDEFINED_OPERATION_GROUP_BY_TRIP_ID = "net.sumaris.core.dao.data.operation.mainUndefinedOperationGroupId";
+
}
@Bean
@@ -159,6 +169,7 @@ public JCacheManagerCustomizer cacheManagerCustomizer(SumarisConfiguration confi
// Person
Caches.createHeapCache(cacheManager, Names.PERSON_BY_ID, Integer.class, PersonVO.class, CacheTTL.DEFAULT.asDuration(), 600);
Caches.createHeapCache(cacheManager, Names.PERSON_BY_PUBKEY, String.class, PersonVO.class, CacheTTL.DEFAULT.asDuration(), 600);
+ Caches.createHeapCache(cacheManager, Names.PERSON_BY_USERNAME, String.class, PersonVO.class, CacheTTL.DEFAULT.asDuration(), 600);
Caches.createHeapCache(cacheManager, Names.PERSON_AVATAR_BY_PUBKEY, ImageAttachmentVO.class, CacheTTL.DEFAULT.asDuration(), 600);
// Location
@@ -171,6 +182,7 @@ public JCacheManagerCustomizer cacheManagerCustomizer(SumarisConfiguration confi
// Program
Caches.createHeapCache(cacheManager, Names.PROGRAM_BY_ID, Integer.class, ProgramVO.class, CacheTTL.DEFAULT.asDuration(), 100);
Caches.createEternalHeapCache(cacheManager, Names.PROGRAM_BY_LABEL, String.class, ProgramVO.class, 100);
+ Caches.createEternalHeapCache(cacheManager, Names.PROGRAM_BY_LABEL_AND_OPTIONS, SimpleKey.class, ProgramVO.class, 100);
Caches.createEternalHeapCache(cacheManager, Names.PROGRAM_PRIVILEGE_BY_ID, Integer.class, ReferentialVO.class, 10);
Caches.createCollectionHeapCache(cacheManager, Names.PROGRAM_IDS_BY_USER_ID, Integer.class, Integer.class, CacheTTL.MEDIUM.asDuration(), 500);
@@ -208,9 +220,12 @@ public JCacheManagerCustomizer cacheManagerCustomizer(SumarisConfiguration confi
Caches.createEternalCollectionHeapCache(cacheManager, Names.PRODUCTS_BY_FILTER, ExtractionProductVO.class, 100);
Caches.createHeapCache(cacheManager, Names.TABLE_META_BY_NAME, String.class, SumarisTableMetadata.class, CacheTTL.DEFAULT.asDuration(), 500);
-
- // Other entities
+ // Other referential
Caches.createEternalCollectionHeapCache(cacheManager, Names.ANALYTIC_REFERENCES_BY_FILTER, ReferentialVO.class, 100);
+ Caches.createHeapCache(cacheManager, Names.WEIGHT_LENGTH_CONVERSION_FIRST_BY_FILTER, Integer.class, Object.class, CacheTTL.DEFAULT.asDuration(), 200);
+ Caches.createEternalHeapCache(cacheManager, Names.WEIGHT_LENGTH_CONVERSION_IS_LENGTH_PARAMETER_ID, Integer.class, Boolean.class, 1000);
+ Caches.createEternalHeapCache(cacheManager, Names.WEIGHT_LENGTH_CONVERSION_IS_LENGTH_PMFM_ID, Integer.class, Boolean.class, 1000);
+ Caches.createHeapCache(cacheManager, Names.ROUND_WEIGHT_CONVERSION_FIRST_BY_FILTER, Integer.class, Object.class, CacheTTL.DEFAULT.asDuration(), 200);
// Data
Caches.createHeapCache(cacheManager, Names.MAIN_UNDEFINED_OPERATION_GROUP_BY_TRIP_ID, Integer.class, Integer.class, CacheTTL.DATA_DEFAULT.asDuration(), 100);
diff --git a/sumaris-core/src/main/java/net/sumaris/core/dao/administration/programStrategy/PmfmStrategyRepositoryImpl.java b/sumaris-core/src/main/java/net/sumaris/core/dao/administration/programStrategy/PmfmStrategyRepositoryImpl.java
index fc5eba2422..5858f0975a 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/dao/administration/programStrategy/PmfmStrategyRepositoryImpl.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/dao/administration/programStrategy/PmfmStrategyRepositoryImpl.java
@@ -325,7 +325,7 @@ private void loadAcquisitionLevels() {
acquisitionLevelIdByLabel.clear();
// Fill acquisition levels map
- List items = referentialDao.findByFilter(AcquisitionLevel.class.getSimpleName(), new ReferentialFilterVO(), 0, 1000, null, null);
+ List items = referentialDao.findByFilter(AcquisitionLevel.class.getSimpleName(), new ReferentialFilterVO(), 0, 1000, null, null, null);
items.forEach(item -> acquisitionLevelIdByLabel.put(item.getLabel(), item.getId()));
}
diff --git a/sumaris-core/src/main/java/net/sumaris/core/dao/administration/programStrategy/ProgramRepositoryImpl.java b/sumaris-core/src/main/java/net/sumaris/core/dao/administration/programStrategy/ProgramRepositoryImpl.java
index c77157e560..89b9053638 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/dao/administration/programStrategy/ProgramRepositoryImpl.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/dao/administration/programStrategy/ProgramRepositoryImpl.java
@@ -171,13 +171,18 @@ public ProgramVO getByLabel(String label) {
return super.getByLabel(label);
}
+ @Override
+ @Cacheable(cacheNames = CacheConfiguration.Names.PROGRAM_BY_LABEL_AND_OPTIONS)
+ public ProgramVO getByLabel(String label, ProgramFetchOptions fetchOptions) {
+ return super.getByLabel(label, fetchOptions);
+ }
+
@Override
protected Specification toSpecification(ProgramFilterVO filter, ProgramFetchOptions fetchOptions) {
return super.toSpecification(filter, fetchOptions)
.and(newerThan(filter.getMinUpdateDate()))
.and(hasAcquisitionLevelLabels(filter.getAcquisitionLevelLabels()))
- .and(hasProperty(filter.getWithProperty()))
- ;
+ .and(hasProperty(filter.getWithProperty()));
}
@Override
@@ -267,6 +272,7 @@ else if (fetchOptions != null && fetchOptions.isWithLocationClassifications()) {
@Caching(evict = {
@CacheEvict(cacheNames = CacheConfiguration.Names.PROGRAM_BY_ID, allEntries = true),
@CacheEvict(cacheNames = CacheConfiguration.Names.PROGRAM_BY_LABEL, allEntries = true),
+ @CacheEvict(cacheNames = CacheConfiguration.Names.PROGRAM_BY_LABEL_AND_OPTIONS, allEntries = true),
@CacheEvict(cacheNames = CacheConfiguration.Names.PROGRAM_IDS_BY_USER_ID, allEntries = true)
})
public void clearCache() {
@@ -278,6 +284,7 @@ public void clearCache() {
evict = {
@CacheEvict(cacheNames = CacheConfiguration.Names.PROGRAM_BY_ID, key = "#source.id", condition = "#source.id != null"),
@CacheEvict(cacheNames = CacheConfiguration.Names.PROGRAM_BY_LABEL, key = "#source.label", condition = "#source.label != null"),
+ @CacheEvict(cacheNames = CacheConfiguration.Names.PROGRAM_BY_LABEL_AND_OPTIONS, allEntries = true),
@CacheEvict(cacheNames = CacheConfiguration.Names.PROGRAM_IDS_BY_USER_ID, allEntries = true)
},
put = {
@@ -350,6 +357,7 @@ public void toEntity(ProgramVO source, Program target, boolean copyIfNull) {
evict = {
@CacheEvict(cacheNames = CacheConfiguration.Names.PROGRAM_BY_ID, key = "#id", condition = "#id != null"),
@CacheEvict(cacheNames = CacheConfiguration.Names.PROGRAM_BY_LABEL, allEntries = true),
+ @CacheEvict(cacheNames = CacheConfiguration.Names.PROGRAM_BY_LABEL_AND_OPTIONS, allEntries = true),
@CacheEvict(cacheNames = CacheConfiguration.Names.PROGRAM_IDS_BY_USER_ID, allEntries = true)
}
)
@@ -680,7 +688,7 @@ protected void toPersonEntity(@NonNull ProgramPersonVO source,
@Override
public boolean hasPropertyValueByProgramId(@NonNull Integer id, @NonNull ProgramPropertyEnum property, @NonNull String expectedValue) {
String value = findVOById(id)
- .map(program -> program.getProperties().get(property.getLabel()))
+ .map(program -> program.getProperties().get(property.getKey()))
.orElse(property.getDefaultValue());
// If boolean: true = TRUE
@@ -694,7 +702,7 @@ public boolean hasPropertyValueByProgramId(@NonNull Integer id, @NonNull Program
@Override
public boolean hasPropertyValueByProgramLabel(@NonNull String label, @NonNull ProgramPropertyEnum property, @NonNull String expectedValue) {
String value = findByLabel(label)
- .map(program -> program.getProperties().get(property.getLabel()))
+ .map(program -> program.getProperties().get(property.getKey()))
.orElse(property.getDefaultValue());
return expectedValue.equals(value);
@@ -704,7 +712,7 @@ public boolean hasPropertyValueByProgramLabel(@NonNull String label, @NonNull Pr
@Override
public String getPropertyValueByProgramLabel(@NonNull String label, @NonNull ProgramPropertyEnum property) {
return findByLabel(label)
- .map(program -> program.getProperties().get(property.getLabel()))
+ .map(program -> program.getProperties().get(property.getKey()))
.orElse(property.getDefaultValue());
}
}
diff --git a/sumaris-core/src/main/java/net/sumaris/core/dao/administration/user/PersonRepositoryImpl.java b/sumaris-core/src/main/java/net/sumaris/core/dao/administration/user/PersonRepositoryImpl.java
index 3bef6a7006..bd1d586fd8 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/dao/administration/user/PersonRepositoryImpl.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/dao/administration/user/PersonRepositoryImpl.java
@@ -115,9 +115,11 @@ public Optional findByPubkey(@NonNull String pubkey) {
}
@Override
+ @Cacheable(cacheNames = CacheConfiguration.Names.PERSON_BY_USERNAME, key = "#username", unless="#result==null")
public Optional findByUsername(String username) {
- return findAll(hasUsername(username)).stream().filter(p -> StatusEnum.ENABLE.getId().equals(p.getStatus().getId()))
- .findFirst().map(this::toVO);
+ return findAll(hasUsername(username)).stream()
+ .filter(p -> StatusEnum.ENABLE.getId().equals(p.getStatus().getId()))
+ .findFirst().map(this::toVO);
}
@Override
diff --git a/sumaris-core/src/main/java/net/sumaris/core/dao/data/DataSpecifications.java b/sumaris-core/src/main/java/net/sumaris/core/dao/data/DataSpecifications.java
index 714b2dcc13..7576722878 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/dao/data/DataSpecifications.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/dao/data/DataSpecifications.java
@@ -37,7 +37,6 @@
import javax.persistence.criteria.Predicate;
import java.io.Serializable;
import java.util.Arrays;
-import java.util.Collection;
import java.util.Objects;
/**
@@ -73,13 +72,25 @@ default Specification isNotControlled() {
*/
default Specification isControlled() {
return (root, query, cb) ->
- cb.isNotNull(root.get(IDataEntity.Fields.CONTROL_DATE));
+ cb.and(
+ // Control date not null
+ cb.isNotNull(root.get(IDataEntity.Fields.CONTROL_DATE)),
+ // Not validated
+ cb.isNull(root.get(IWithDataQualityEntity.Fields.VALIDATION_DATE))
+ );
}
default Specification isValidated() {
return (root, query, cb) ->
- // Validation date not null
- cb.isNotNull(root.get(IWithDataQualityEntity.Fields.VALIDATION_DATE));
+ cb.and(
+ // Validation date not null
+ cb.isNotNull(root.get(IWithDataQualityEntity.Fields.VALIDATION_DATE)),
+ // Not qualified
+ cb.or(
+ cb.isNull(root.get(IWithDataQualityEntity.Fields.QUALIFICATION_DATE)),
+ cb.equal(cb.coalesce(root.get(IDataEntity.Fields.QUALITY_FLAG).get(QualityFlag.Fields.ID), QualityFlagEnum.NOT_QUALIFIED.getId()), QualityFlagEnum.NOT_QUALIFIED.getId())
+ )
+ );
}
default Specification isQualified() {
diff --git a/sumaris-core/src/main/java/net/sumaris/core/dao/data/MeasurementDaoImpl.java b/sumaris-core/src/main/java/net/sumaris/core/dao/data/MeasurementDaoImpl.java
index 760c4ce632..55eba2c55e 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/dao/data/MeasurementDaoImpl.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/dao/data/MeasurementDaoImpl.java
@@ -1134,7 +1134,10 @@ protected void toEntity(String value, PmfmVO pmfm, IMeasurementEntity target) {
PmfmValueType type = PmfmValueType.fromString(pmfm.getType());
switch (type) {
- case BOOLEAN -> target.setNumericalValue(Boolean.parseBoolean(value) || "1".equals(value) ? 1d : 0d);
+ case BOOLEAN -> {
+ Double boolValue = ("1".equals(value) || Boolean.parseBoolean(value)) ? 1d : 0d;
+ target.setNumericalValue(boolValue);
+ }
case QUALITATIVE_VALUE -> {
// If find a object structure (e.g. ReferentialVO), try to find the id
try {
@@ -1147,9 +1150,8 @@ protected void toEntity(String value, PmfmVO pmfm, IMeasurementEntity target) {
case STRING -> target.setAlphanumericalValue(value);
case DATE -> target.setAlphanumericalValue(Dates.checkISODateTimeString(value));
case INTEGER, DOUBLE -> target.setNumericalValue(Double.parseDouble(value));
- default ->
- // Unknown type
- throw new SumarisTechnicalException(String.format("Unable to set measurement value {%s} for the type {%s}", value, type.name().toLowerCase()));
+ // Unknown type
+ default -> throw new SumarisTechnicalException(String.format("Unable to set measurement value {%s} for the type {%s}", value, type.name().toLowerCase()));
}
}
@@ -1222,7 +1224,9 @@ protected Object getEntityValue(IMeasurementEntity source) {
(source.getNumericalValue() != null) ? (source.getNumericalValue() == 1d ? Boolean.TRUE : Boolean.FALSE) : null;
case QUALITATIVE_VALUE ->
// If find a object structure (e.g. ReferentialVO), try to find the id
- ((source.getQualitativeValue() != null && source.getQualitativeValue().getId() != null) ? source.getQualitativeValue().getId() : null);
+ ((source.getQualitativeValue() != null && source.getQualitativeValue().getId() != null)
+ ? source.getQualitativeValue().getId()
+ : null);
case STRING, DATE -> source.getAlphanumericalValue();
case INTEGER -> (source.getNumericalValue() != null) ? source.getNumericalValue().intValue() : null;
case DOUBLE -> source.getNumericalValue();
diff --git a/sumaris-core/src/main/java/net/sumaris/core/dao/data/batch/BatchRepositoryImpl.java b/sumaris-core/src/main/java/net/sumaris/core/dao/data/batch/BatchRepositoryImpl.java
index 9f2208018c..58710c8deb 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/dao/data/batch/BatchRepositoryImpl.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/dao/data/batch/BatchRepositoryImpl.java
@@ -272,7 +272,7 @@ public BatchVO toTree(List sources) {
// Get root
BatchVO rootBatch = roots.get(0);
- // Fill children
+ // Fill children, and children of children (=recursively)
fillRecursiveChildren(rootBatch, sources);
return rootBatch;
@@ -645,6 +645,7 @@ protected List fillRecursiveChildren(int parentId, List source
List children = sources.stream()
.filter(batch -> Objects.equals(batch.getParentId(), parentId))
+ .sorted(Comparator.comparing(BatchVO::getRankOrder))
.collect(Collectors.toList());
children.forEach(batch -> batch.setChildren(fillRecursiveChildren(batch.getId(), sources)));
return children;
diff --git a/sumaris-core/src/main/java/net/sumaris/core/dao/data/batch/DenormalizedBatchRepositoryImpl.java b/sumaris-core/src/main/java/net/sumaris/core/dao/data/batch/DenormalizedBatchRepositoryImpl.java
index 86d6f63004..4ecb1e8f65 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/dao/data/batch/DenormalizedBatchRepositoryImpl.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/dao/data/batch/DenormalizedBatchRepositoryImpl.java
@@ -31,6 +31,7 @@
import net.sumaris.core.dao.technical.jpa.SumarisJpaRepositoryImpl;
import net.sumaris.core.exception.SumarisTechnicalException;
import net.sumaris.core.model.data.DenormalizedBatch;
+import net.sumaris.core.model.data.DenormalizedBatchSortingValue;
import net.sumaris.core.model.data.Operation;
import net.sumaris.core.model.data.Sale;
import net.sumaris.core.model.referential.QualityFlag;
@@ -59,10 +60,7 @@
import javax.annotation.Nonnull;
import javax.persistence.EntityManager;
-import java.util.Collection;
-import java.util.List;
-import java.util.Objects;
-import java.util.Set;
+import java.util.*;
import java.util.stream.Collectors;
/**
@@ -82,17 +80,21 @@ public class DenormalizedBatchRepositoryImpl
private final ApplicationContext applicationContext;
+ private final DenormalizedBatchSortingValueRepository sortingValueRepository;
+
public DenormalizedBatchRepositoryImpl(EntityManager entityManager,
SumarisConfiguration config,
PmfmRepository pmfmRepository,
ParameterRepository parameterRepository,
TaxonNameRepository taxonNameRepository,
+ DenormalizedBatchSortingValueRepository sortingValueRepository,
ApplicationContext applicationContext) {
super(DenormalizedBatch.class, entityManager);
this.config = config;
this.pmfmRepository = pmfmRepository;
this.parameterRepository = parameterRepository;
this.taxonNameRepository = taxonNameRepository;
+ this.sortingValueRepository = sortingValueRepository;
this.applicationContext = applicationContext;
}
@@ -114,6 +116,11 @@ public void toVO(DenormalizedBatch source, DenormalizedBatchVO target, boolean c
if (copyIfNull || saleId != null) {
target.setSaleId(saleId);
}
+
+ Integer parentId = source.getParent() != null ? source.getParent().getId() : null;
+ if (copyIfNull || parentId != null) {
+ target.setParentId(parentId);
+ }
}
@Override
@@ -207,6 +214,48 @@ public void toEntity(DenormalizedBatchVO source, DenormalizedBatch target, boole
}
}
}
+
+ // Calculated taxon group
+ {
+ Integer calculatedTaxonGroupId = source.getCalculatedTaxonGroup() != null ? source.getCalculatedTaxonGroup().getId() : null;
+ if (copyIfNull || calculatedTaxonGroupId != null) {
+ if (calculatedTaxonGroupId == null) {
+ target.setCalculatedTaxonGroup(null);
+ } else {
+ target.setCalculatedTaxonGroup(getReference(TaxonGroup.class, calculatedTaxonGroupId));
+ }
+ }
+ }
+
+ // Parent name
+ {
+ Integer parentBatchId = source.getParent() != null ? source.getParent().getId() : source.getParentId();
+ if (copyIfNull || parentBatchId != null) {
+ if (parentBatchId == null) {
+ target.setParent(null);
+ } else {
+ target.setParent(getReference(DenormalizedBatch.class, parentBatchId));
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void onAfterSaveEntity(DenormalizedBatchVO vo, DenormalizedBatch savedEntity, boolean isNew) {
+ super.onAfterSaveEntity(vo, savedEntity, isNew);
+
+ List existingSvIds = Beans.collectIds(savedEntity.getSortingValues());
+
+ Beans.getStream(vo.getSortingValues())
+ .forEach(source -> {
+ source.setBatchId(vo.getId());
+ sortingValueRepository.save(source);
+ existingSvIds.remove(source.getId());
+ });
+
+ if (CollectionUtils.isNotEmpty(existingSvIds)) {
+ sortingValueRepository.deleteAllById(existingSvIds);
+ }
}
@Override
@@ -231,7 +280,7 @@ public List saveAllByOperationId(int operationId, @Nonnull
// Set parent link
sources.forEach(b -> b.setOperationId(operationId));
- // Get existing fishing areas
+ // Get existing ids
Set existingIds = getRepository().getAllIdByOperationId(operationId);
// Save
@@ -267,11 +316,17 @@ public List saveAllBySaleId(int saleId, @Nonnull List.
+ * #L%
+ */
+
+package net.sumaris.core.dao.data.batch;
+
+import net.sumaris.core.dao.technical.jpa.SumarisJpaRepository;
+import net.sumaris.core.model.data.DenormalizedBatch;
+import net.sumaris.core.model.data.DenormalizedBatchSortingValue;
+import net.sumaris.core.vo.data.batch.DenormalizedBatchSortingValueVO;
+import net.sumaris.core.vo.data.batch.DenormalizedBatchVO;
+import org.springframework.data.jpa.repository.Query;
+
+import java.util.Set;
+
+public interface DenormalizedBatchSortingValueRepository
+ extends SumarisJpaRepository,
+ DenormalizedBatchSortingValueSpecifications {
+
+
+}
diff --git a/sumaris-core/src/main/java/net/sumaris/core/dao/data/batch/DenormalizedBatchSortingValueRepositoryImpl.java b/sumaris-core/src/main/java/net/sumaris/core/dao/data/batch/DenormalizedBatchSortingValueRepositoryImpl.java
new file mode 100644
index 0000000000..707ebf4ebc
--- /dev/null
+++ b/sumaris-core/src/main/java/net/sumaris/core/dao/data/batch/DenormalizedBatchSortingValueRepositoryImpl.java
@@ -0,0 +1,130 @@
+/*
+ * #%L
+ * SUMARiS
+ * %%
+ * Copyright (C) 2019 SUMARiS Consortium
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program. If not, see
+ * .
+ * #L%
+ */
+
+package net.sumaris.core.dao.data.batch;
+
+import com.google.common.base.Preconditions;
+import lombok.extern.slf4j.Slf4j;
+import net.sumaris.core.config.SumarisConfiguration;
+import net.sumaris.core.dao.referential.pmfm.ParameterRepository;
+import net.sumaris.core.dao.referential.pmfm.PmfmRepository;
+import net.sumaris.core.dao.referential.taxon.TaxonNameRepository;
+import net.sumaris.core.dao.technical.jpa.SumarisJpaRepositoryImpl;
+import net.sumaris.core.exception.SumarisTechnicalException;
+import net.sumaris.core.model.data.DenormalizedBatch;
+import net.sumaris.core.model.data.DenormalizedBatchSortingValue;
+import net.sumaris.core.model.data.Operation;
+import net.sumaris.core.model.data.Sale;
+import net.sumaris.core.model.referential.QualityFlag;
+import net.sumaris.core.model.referential.QualityFlagEnum;
+import net.sumaris.core.model.referential.pmfm.*;
+import net.sumaris.core.model.referential.taxon.ReferenceTaxon;
+import net.sumaris.core.model.referential.taxon.TaxonGroup;
+import net.sumaris.core.util.Beans;
+import net.sumaris.core.util.Numbers;
+import net.sumaris.core.util.StringUtils;
+import net.sumaris.core.vo.data.MeasurementVO;
+import net.sumaris.core.vo.data.QuantificationMeasurementVO;
+import net.sumaris.core.vo.data.batch.BatchVO;
+import net.sumaris.core.vo.data.batch.DenormalizedBatchSortingValueVO;
+import net.sumaris.core.vo.data.batch.DenormalizedBatchVO;
+import net.sumaris.core.vo.referential.ParameterVO;
+import net.sumaris.core.vo.referential.PmfmVO;
+import net.sumaris.core.vo.referential.PmfmValueType;
+import net.sumaris.core.vo.referential.ReferentialVO;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.mutable.MutableInt;
+import org.springframework.context.ApplicationContext;
+import org.springframework.dao.DataRetrievalFailureException;
+
+import javax.annotation.Nonnull;
+import javax.persistence.EntityManager;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author peck7 on 09/06/2020.
+ */
+@Slf4j
+public class DenormalizedBatchSortingValueRepositoryImpl
+ extends SumarisJpaRepositoryImpl
+ implements DenormalizedBatchSortingValueSpecifications {
+
+ public DenormalizedBatchSortingValueRepositoryImpl(EntityManager entityManager) {
+ super(DenormalizedBatchSortingValue.class, entityManager);
+ }
+
+ @Override
+ public Class getVOClass() {
+ return DenormalizedBatchSortingValueVO.class;
+ }
+
+ @Override
+ public void toVO(DenormalizedBatchSortingValue source, DenormalizedBatchSortingValueVO target, boolean copyIfNull) {
+ super.toVO(source, target, copyIfNull);
+
+ }
+
+ @Override
+ public void toEntity(DenormalizedBatchSortingValueVO source, DenormalizedBatchSortingValue target, boolean copyIfNull) {
+ super.toEntity(source, target, copyIfNull);
+
+ // Pmfm
+ Integer pmfmId = source.getPmfmId() != null ? source.getPmfmId() : (source.getPmfm() != null ? source.getPmfm().getId() : null);
+ if (pmfmId != null || copyIfNull) {
+ if (pmfmId != null) target.setPmfm(getReference(Pmfm.class, pmfmId));
+ else target.setPmfm(null);
+ }
+
+ // Parameter
+ Integer parameterId = source.getParameter() != null ? source.getParameter().getId() : null;
+ if (parameterId != null || copyIfNull) {
+ if (parameterId != null) target.setParameter(getReference(Parameter.class, parameterId));
+ else target.setParameter(null);
+ }
+
+ // Qualitative_value
+ Integer qvId = source.getQualitativeValue() != null ? source.getQualitativeValue().getId() : null;
+ if (qvId != null || copyIfNull) {
+ if (qvId != null) target.setQualitativeValue(getReference(QualitativeValue.class, qvId));
+ else target.setQualitativeValue(null);
+ }
+
+ // Unit
+ Integer unitId = source.getUnit() != null ? source.getUnit().getId() : null;
+ if (unitId != null || copyIfNull) {
+ if (unitId != null) target.setUnit(getReference(Unit.class, unitId));
+ else target.setUnit(null);
+ }
+
+ // Link to parent
+ Integer batchId = source.getBatchId() != null ? source.getBatchId() : (source.getBatch() != null ? source.getBatch().getId() : null);
+ if (batchId != null) {
+ target.setBatch(getReference(DenormalizedBatch.class, batchId));
+ }
+ }
+
+ /* -- protected methods -- */
+}
diff --git a/sumaris-core/src/main/java/net/sumaris/core/dao/data/batch/DenormalizedBatchSortingValueSpecifications.java b/sumaris-core/src/main/java/net/sumaris/core/dao/data/batch/DenormalizedBatchSortingValueSpecifications.java
new file mode 100644
index 0000000000..7f21f87cf8
--- /dev/null
+++ b/sumaris-core/src/main/java/net/sumaris/core/dao/data/batch/DenormalizedBatchSortingValueSpecifications.java
@@ -0,0 +1,30 @@
+/*
+ * #%L
+ * SUMARiS
+ * %%
+ * Copyright (C) 2019 SUMARiS Consortium
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program. If not, see
+ * .
+ * #L%
+ */
+
+package net.sumaris.core.dao.data.batch;
+
+import net.sumaris.core.model.data.DenormalizedBatchSortingValue;
+import net.sumaris.core.vo.data.batch.DenormalizedBatchSortingValueVO;
+
+public interface DenormalizedBatchSortingValueSpecifications {
+
+}
diff --git a/sumaris-core/src/main/java/net/sumaris/core/dao/data/operation/OperationRepositoryImpl.java b/sumaris-core/src/main/java/net/sumaris/core/dao/data/operation/OperationRepositoryImpl.java
index 4ced055f7e..213676e199 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/dao/data/operation/OperationRepositoryImpl.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/dao/data/operation/OperationRepositoryImpl.java
@@ -132,29 +132,36 @@ public void toVO(Operation source, OperationVO target, OperationFetchOptions fet
// Load children entities (not loaded by default)
Integer operationId = source.getId();
- if (fetchOptions != null && fetchOptions.isWithChildrenEntities() && operationId != null) {
-
+ if (operationId != null) {
// Positions
- target.setPositions(vesselPositionDao.getAllByOperationId(operationId));
+ if (fetchOptions != null && (fetchOptions.isWithChildrenEntities() || fetchOptions.isWithPositions())) {
+ target.setPositions(vesselPositionDao.getAllByOperationId(operationId));
+ }
// Fishing Areas
- target.setFishingAreas(fishingAreaRepository.findAllVO(fishingAreaRepository.hasOperationId(operationId)));
+ if (fetchOptions != null && (fetchOptions.isWithChildrenEntities() || fetchOptions.isWithFishingAreas())) {
+ target.setFishingAreas(fishingAreaRepository.findAllVO(fishingAreaRepository.hasOperationId(operationId)));
+ }
// Batches
- target.setBatches(batchRepository.findAllVO(batchRepository.hasOperationId(operationId),
+ if (fetchOptions != null && (fetchOptions.isWithChildrenEntities() || fetchOptions.isWithBatches())) {
+ target.setBatches(batchRepository.findAllVO(batchRepository.hasOperationId(operationId),
BatchFetchOptions.builder()
- .withChildrenEntities(false) // Use flat list, not a tree
- .withRecorderDepartment(false)
- .withMeasurementValues(true)
- .build()));
+ .withChildrenEntities(false) // Use flat list, not a tree
+ .withRecorderDepartment(false)
+ .withMeasurementValues(true)
+ .build()));
+ }
// Samples
- target.setSamples(sampleRepository.findAllVO(sampleRepository.hasOperationId(operationId),
+ if (fetchOptions != null && (fetchOptions.isWithChildrenEntities() || fetchOptions.isWithSamples())) {
+ target.setSamples(sampleRepository.findAllVO(sampleRepository.hasOperationId(operationId),
SampleFetchOptions.builder()
- .withChildrenEntities(false) // Use flat list, not a tree
- .withRecorderDepartment(false)
- .withMeasurementValues(true)
- .build()));
+ .withChildrenEntities(false) // Use flat list, not a tree
+ .withRecorderDepartment(false)
+ .withMeasurementValues(true)
+ .build()));
+ }
}
// Measurements
@@ -322,7 +329,9 @@ protected Specification toSpecification(OperationFilterVO filter, Ope
.and(inPhysicalGearIds(filter.getPhysicalGearIds()))
.and(inTaxonGroupLabels(filter.getTaxonGroupLabels()))
.and(hasQualityFlagIds(filter.getQualityFlagIds()))
- .and(inDataQualityStatus(filter.getDataQualityStatus()));
+ .and(inDataQualityStatus(filter.getDataQualityStatus()))
+ .and(needBatchDenormalization(filter.getNeedBatchDenormalization()))
+ ;
}
@Override
diff --git a/sumaris-core/src/main/java/net/sumaris/core/dao/data/operation/OperationSpecifications.java b/sumaris-core/src/main/java/net/sumaris/core/dao/data/operation/OperationSpecifications.java
index 91c4beb962..6851e065db 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/dao/data/operation/OperationSpecifications.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/dao/data/operation/OperationSpecifications.java
@@ -27,10 +27,7 @@
import net.sumaris.core.dao.technical.jpa.BindableSpecification;
import net.sumaris.core.model.IEntity;
import net.sumaris.core.model.administration.programStrategy.Program;
-import net.sumaris.core.model.data.Operation;
-import net.sumaris.core.model.data.PhysicalGear;
-import net.sumaris.core.model.data.Trip;
-import net.sumaris.core.model.data.Vessel;
+import net.sumaris.core.model.data.*;
import net.sumaris.core.model.referential.QualityFlag;
import net.sumaris.core.model.referential.metier.Metier;
import net.sumaris.core.model.referential.taxon.TaxonGroup;
@@ -39,9 +36,7 @@
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.data.jpa.domain.Specification;
-import javax.persistence.criteria.Join;
-import javax.persistence.criteria.JoinType;
-import javax.persistence.criteria.ParameterExpression;
+import javax.persistence.criteria.*;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
@@ -222,6 +217,41 @@ default Specification hasQualityFlagIds(Integer[] qualityFlagIds) {
.addBind(QUALITY_FLAG_ID_PARAM, Arrays.asList(qualityFlagIds));
}
+ default Specification needBatchDenormalization(Boolean needBatchDenormalization) {
+ if (!Boolean.TRUE.equals(needBatchDenormalization)) return null;
+
+ return BindableSpecification.where((root, query, cb) -> {
+
+ Join catchBatch = Daos.composeJoin(root, Operation.Fields.BATCHES, JoinType.INNER);
+
+ // Sub select that return the update to date denormalized catch batch
+ Subquery subQuery = query.subquery(Integer.class);
+ Root denormalizedBatchRoot = subQuery.from(DenormalizedBatch.class);
+ subQuery.select(denormalizedBatchRoot.get(DenormalizedBatch.Fields.ID));
+ subQuery.where(
+ cb.and(
+ // Catch batch
+ cb.isNull(denormalizedBatchRoot.get(DenormalizedBatch.Fields.PARENT)),
+ // Same operation
+ cb.equal(denormalizedBatchRoot.get(DenormalizedBatch.Fields.OPERATION), root),
+ // Same catch batch
+ cb.equal(denormalizedBatchRoot.get(DenormalizedBatch.Fields.ID), catchBatch.get(Batch.Fields.ID)),
+ // Same date
+ cb.equal(denormalizedBatchRoot.get(DenormalizedBatch.Fields.UPDATE_DATE), catchBatch.get(Batch.Fields.UPDATE_DATE))
+ )
+ );
+
+ return cb.and(
+ // Operation with a catch batch
+ cb.isNull(catchBatch.get(Batch.Fields.PARENT)),
+ // And without an update to date denormalization
+ cb.not(cb.exists(subQuery))
+ );
+ });
+ }
+
+
+
// Override the default function, because operation has no validation date
default Specification isValidated() {
return (root, query, cb) ->
diff --git a/sumaris-core/src/main/java/net/sumaris/core/dao/data/trip/TripRepositoryImpl.java b/sumaris-core/src/main/java/net/sumaris/core/dao/data/trip/TripRepositoryImpl.java
index 325a2c5185..d2c8afd280 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/dao/data/trip/TripRepositoryImpl.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/dao/data/trip/TripRepositoryImpl.java
@@ -79,6 +79,7 @@ public Specification toSpecification(TripFilterVO filter, TripFetchOptions
.and(hasObserverPersonIds(filter.getObserverPersonIds()))
.and(hasQualityFlagIds(filter.getQualityFlagIds()))
.and(inDataQualityStatus(filter.getDataQualityStatus()))
+ .and(withOperationIds(filter.getOperationIds()))
;
}
diff --git a/sumaris-core/src/main/java/net/sumaris/core/dao/data/trip/TripSpecifications.java b/sumaris-core/src/main/java/net/sumaris/core/dao/data/trip/TripSpecifications.java
index 2abe85f550..5c65e245cb 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/dao/data/trip/TripSpecifications.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/dao/data/trip/TripSpecifications.java
@@ -27,14 +27,17 @@
import net.sumaris.core.dao.technical.jpa.BindableSpecification;
import net.sumaris.core.model.IEntity;
import net.sumaris.core.model.data.Landing;
+import net.sumaris.core.model.data.Operation;
import net.sumaris.core.model.data.Trip;
import net.sumaris.core.model.referential.QualityFlag;
+import net.sumaris.core.model.referential.conversion.WeightLengthConversion;
+import net.sumaris.core.model.referential.location.Location;
+import net.sumaris.core.model.referential.location.LocationHierarchy;
+import net.sumaris.core.util.StringUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.data.jpa.domain.Specification;
-import javax.persistence.criteria.JoinType;
-import javax.persistence.criteria.ListJoin;
-import javax.persistence.criteria.ParameterExpression;
+import javax.persistence.criteria.*;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
@@ -50,6 +53,7 @@ public interface TripSpecifications extends RootDataSpecifications {
String OBSERVED_LOCATION_ID_PARAM = "observedLocationId";
+ String OPERATION_IDS_PARAM = "operationIds";
default Specification hasLocationId(Integer locationId) {
if (locationId == null) return null;
return BindableSpecification.where((root, query, cb) -> {
@@ -144,4 +148,18 @@ default Specification hasQualityFlagIds(Integer[] qualityFlagIds) {
})
.addBind(QUALITY_FLAG_ID_PARAM, Arrays.asList(qualityFlagIds));
}
+
+
+ default Specification withOperationIds(Integer[] operationIds) {
+ if (ArrayUtils.isEmpty(operationIds)) return null;
+
+ return BindableSpecification.where((root, query, cb) -> {
+ ParameterExpression param = cb.parameter(Collection.class, OPERATION_IDS_PARAM);
+ Subquery subQuery = query.subquery(Operation.class);
+ Root operation = subQuery.from(Operation.class);
+ subQuery.select(operation.get(Operation.Fields.TRIP).get(Operation.Fields.ID));
+ subQuery.where(cb.in(operation.get(Operation.Fields.ID)).value(param));
+ return cb.in(root.get(IEntity.Fields.ID)).value(subQuery);
+ }).addBind(OPERATION_IDS_PARAM, Arrays.asList(operationIds));
+ }
}
diff --git a/sumaris-core/src/main/java/net/sumaris/core/dao/referential/ReferentialDao.java b/sumaris-core/src/main/java/net/sumaris/core/dao/referential/ReferentialDao.java
index cdc3e40b1d..c1f89706d4 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/dao/referential/ReferentialDao.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/dao/referential/ReferentialDao.java
@@ -26,9 +26,11 @@
import net.sumaris.core.model.referential.IReferentialEntity;
import net.sumaris.core.vo.filter.IReferentialFilter;
import net.sumaris.core.vo.referential.IReferentialVO;
+import net.sumaris.core.vo.referential.ReferentialFetchOptions;
import net.sumaris.core.vo.referential.ReferentialTypeVO;
import net.sumaris.core.vo.referential.ReferentialVO;
+import javax.annotation.Nullable;
import java.util.Collection;
import java.util.Date;
import java.util.List;
@@ -57,7 +59,8 @@ List findByFilter(String entityName,
int offset,
int size,
String sortAttribute,
- SortDirection sortDirection);
+ SortDirection sortDirection,
+ @Nullable ReferentialFetchOptions fetchOptions);
Long countByFilter(final String entityName, IReferentialFilter filter);
diff --git a/sumaris-core/src/main/java/net/sumaris/core/dao/referential/ReferentialDaoImpl.java b/sumaris-core/src/main/java/net/sumaris/core/dao/referential/ReferentialDaoImpl.java
index 51d1bfa66e..f0a5953358 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/dao/referential/ReferentialDaoImpl.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/dao/referential/ReferentialDaoImpl.java
@@ -24,6 +24,7 @@
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import lombok.extern.slf4j.Slf4j;
import net.sumaris.core.config.CacheConfiguration;
import net.sumaris.core.dao.technical.Daos;
@@ -34,12 +35,15 @@
import net.sumaris.core.model.IUpdateDateEntity;
import net.sumaris.core.exception.SumarisTechnicalException;
import net.sumaris.core.model.referential.*;
+import net.sumaris.core.model.referential.pmfm.Method;
import net.sumaris.core.util.Beans;
import net.sumaris.core.vo.filter.IReferentialFilter;
import net.sumaris.core.vo.filter.ReferentialFilterVO;
import net.sumaris.core.vo.referential.IReferentialVO;
+import net.sumaris.core.vo.referential.ReferentialFetchOptions;
import net.sumaris.core.vo.referential.ReferentialTypeVO;
import net.sumaris.core.vo.referential.ReferentialVO;
+import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.nuiton.i18n.I18n;
@@ -92,14 +96,15 @@ public List findByFilter(final String entityName,
int offset,
int size,
String sortAttribute,
- SortDirection sortDirection) {
+ SortDirection sortDirection,
+ ReferentialFetchOptions fetchOptions) {
Preconditions.checkNotNull(entityName, "Missing entityName argument");
// Get entity class from entityName
Class extends IReferentialEntity> entityClass = ReferentialEntities.getEntityClass(entityName);
return streamByFilter(entityClass, filter, offset, size, sortAttribute, sortDirection)
- .map(s -> toVO(entityName, s))
+ .map(s -> toVO(entityName, s, fetchOptions))
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
@@ -130,7 +135,7 @@ public Optional findByUniqueLabel(String entityName, String label
} catch (NoResultException e) {
// let result to null
}
- return result == null ? Optional.empty() : Optional.of(toVO(entityName, result));
+ return result == null ? Optional.empty() : Optional.of(toVO(entityName, result, null));
}
@Override
@@ -174,7 +179,7 @@ public List getAllLevels(final String entityName) {
return ReferentialEntities.getLevelProperty(entityName)
.map(levelDescriptor -> {
String levelEntityName = levelDescriptor.getPropertyType().getSimpleName();
- return findByFilter(levelEntityName, ReferentialFilterVO.builder().build(), 0, 1000, IItemReferentialEntity.Fields.NAME, SortDirection.ASC);
+ return findByFilter(levelEntityName, ReferentialFilterVO.builder().build(), 0, 1000, IItemReferentialEntity.Fields.NAME, SortDirection.ASC, null);
})
.orElseGet(ImmutableList::of);
}
@@ -191,7 +196,7 @@ public ReferentialVO getLevelById(String entityName, int levelId) {
throw new DataRetrievalFailureException("Unable to convert class=" + levelClass.getName() + " to a referential bean");
}
- return toVO(levelClass.getSimpleName(), (IReferentialEntity) getById(levelClass, levelId));
+ return toVO(levelClass.getSimpleName(), (IReferentialEntity) getById(levelClass, levelId), null);
}
@Override
@@ -199,7 +204,7 @@ public ReferentialVO toVO(T source) {
if (source == null)
throw new EntityNotFoundException();
- return toVO(getEntityName(source), source);
+ return toVO(getEntityName(source), source, null);
}
@Override
@@ -231,6 +236,7 @@ public void clearCache() {
@CacheEvict(cacheNames = CacheConfiguration.Names.REFERENTIAL_MAX_UPDATE_DATE_BY_TYPE, key = "#entityName"),
@CacheEvict(cacheNames = CacheConfiguration.Names.PERSON_BY_ID, allEntries = true, condition = "#entityName == 'Person'"),
@CacheEvict(cacheNames = CacheConfiguration.Names.PERSON_BY_PUBKEY, allEntries = true, condition = "#entityName == 'Person'"),
+ @CacheEvict(cacheNames = CacheConfiguration.Names.PERSON_BY_USERNAME, allEntries = true, condition = "#entityName == 'Person'"),
@CacheEvict(cacheNames = CacheConfiguration.Names.DEPARTMENT_BY_ID, allEntries = true, condition = "#entityName == 'Department'"),
@CacheEvict(cacheNames = CacheConfiguration.Names.DEPARTMENT_BY_LABEL, allEntries = true, condition = "#entityName == 'Department'"),
@CacheEvict(cacheNames = CacheConfiguration.Names.PMFM_BY_ID, allEntries = true, condition = "#entityName == 'Pmfm'"),
@@ -259,6 +265,7 @@ public void clearCache(String entityName) {
@CacheEvict(cacheNames = CacheConfiguration.Names.REFERENTIAL_MAX_UPDATE_DATE_BY_TYPE, key = "#entityName"),
@CacheEvict(cacheNames = CacheConfiguration.Names.PERSON_BY_ID, key = "#id", condition = "#entityName == 'Person'"),
@CacheEvict(cacheNames = CacheConfiguration.Names.PERSON_BY_PUBKEY, allEntries = true, condition = "#entityName == 'Person'"),
+ @CacheEvict(cacheNames = CacheConfiguration.Names.PERSON_BY_USERNAME, allEntries = true, condition = "#entityName == 'Person'"),
@CacheEvict(cacheNames = CacheConfiguration.Names.DEPARTMENT_BY_ID, key = "#id", condition = "#entityName == 'Department'"),
@CacheEvict(cacheNames = CacheConfiguration.Names.DEPARTMENT_BY_LABEL, allEntries = true, condition = "#entityName == 'Department'"),
@CacheEvict(cacheNames = CacheConfiguration.Names.PMFM_BY_ID, key = "#id", condition = "#entityName == 'Pmfm'"),
@@ -269,6 +276,7 @@ public void clearCache(String entityName) {
@CacheEvict(cacheNames = CacheConfiguration.Names.PMFM_HAS_PARAMETER_GROUP, allEntries = true, condition = "#entityName == 'Pmfm'"),
@CacheEvict(cacheNames = CacheConfiguration.Names.PROGRAM_BY_ID, key = "#id", condition = "#entityName == 'Program'"),
@CacheEvict(cacheNames = CacheConfiguration.Names.PROGRAM_BY_LABEL, allEntries = true, condition = "#entityName == 'Program'"),
+ @CacheEvict(cacheNames = CacheConfiguration.Names.PROGRAM_BY_LABEL_AND_OPTIONS, allEntries = true, condition = "#entityName == 'Program'"),
@CacheEvict(cacheNames = CacheConfiguration.Names.PROGRAM_PRIVILEGE_BY_ID, key = "#id", condition = "#entityName == 'ProgramPrivilege'"),
@CacheEvict(cacheNames = CacheConfiguration.Names.LOCATION_BY_ID, key = "#id", condition = "#entityName == 'Location'"),
@CacheEvict(cacheNames = CacheConfiguration.Names.LOCATION_LEVEL_BY_LABEL, allEntries = true, condition = "#entityName == 'LocationLevel'"),
@@ -468,7 +476,8 @@ protected ReferentialTypeVO getTypeByEntityName(final String entityName) {
return type;
}
- protected ReferentialVO toVO(final String entityName, T source) {
+ protected ReferentialVO toVO(final String entityName, T source,
+ ReferentialFetchOptions fetchOptions) {
Preconditions.checkNotNull(entityName);
Preconditions.checkNotNull(source);
@@ -515,6 +524,10 @@ protected ReferentialVO toVO(final String entityN
target.setParentId(null);
}
+ if (fetchOptions != null && fetchOptions.isWithProperties()) {
+ copyProperties(source, target);
+ }
+
// EntityName (as metadata)
target.setEntityName(entityName);
@@ -777,12 +790,12 @@ protected TypedQuery createFilteredQuery(CriteriaBuilder builder,
private TypedQuery createFindByUniqueLabelQuery(Class entityClass, String label) {
CriteriaBuilder builder = getEntityManager().getCriteriaBuilder();
CriteriaQuery query = builder.createQuery(entityClass);
- Root tripRoot = query.from(entityClass);
- query.select(tripRoot).distinct(true);
+ Root root = query.from(entityClass);
+ query.select(root).distinct(true);
// Filter on text
ParameterExpression labelParam = builder.parameter(String.class);
- query.where(builder.equal(tripRoot.get(IItemReferentialEntity.Fields.LABEL), labelParam));
+ query.where(builder.equal(root.get(IItemReferentialEntity.Fields.LABEL), labelParam));
return getEntityManager().createQuery(query)
.setParameter(labelParam, label);
@@ -816,14 +829,14 @@ protected void toEntity(final ReferentialVO source, IReferentialEntity target, b
}
// Level
- Integer levelID = source.getLevelId();
- if (copyIfNull || levelID != null) {
+ Integer levelId = source.getLevelId() != null ? source.getLevelId() : (source.getLevel() != null ? source.getLevel().getId() : null);
+ if (copyIfNull || levelId != null) {
ReferentialEntities.getLevelProperty(source.getEntityName()).ifPresent(levelDescriptor -> {
try {
- if (levelID == null) {
+ if (levelId == null) {
levelDescriptor.getWriteMethod().invoke(target, new Object[]{null});
} else {
- Object level = getReference(levelDescriptor.getPropertyType().asSubclass(Serializable.class), levelID);
+ Object level = getReference(levelDescriptor.getPropertyType().asSubclass(Serializable.class), levelId);
levelDescriptor.getWriteMethod().invoke(target, level);
}
} catch (Exception e) {
@@ -841,6 +854,57 @@ protected void toEntity(final ReferentialVO source, IReferentialEntity target, b
((IWithValidityStatusEntity, ValidityStatus>) target).setValidityStatus(getReference(ValidityStatus.class, source.getValidityStatusId()));
}
}
+
+ // Parent
+ if (target instanceof ITreeNodeEntity) {
+ Integer parentId = source.getParentId() != null ? source.getParentId()
+ : (source.getParent() != null ? source.getParent().getId() : null);
+ if (parentId != null || copyIfNull) {
+ if (parentId != null) {
+ IReferentialEntity> parent = getReference(target.getClass(), parentId);
+ ((ITreeNodeEntity, IReferentialEntity>>) target).setParent(parent);
+ }
+ else {
+ ((ITreeNodeEntity, ?>) target).setParent(null);
+ }
+ }
+ }
+
+ // Properties
+ if (MapUtils.isNotEmpty(source.getProperties())) {
+ copyProperties(source, target, copyIfNull);
+ }
+
+ }
+
+ protected void copyProperties(final ReferentialVO source, IReferentialEntity target, boolean copyIfNull) {
+ if (MapUtils.isNotEmpty(source.getProperties())) {
+ source.getProperties().entrySet().forEach(entry -> {
+ try {
+ Object value = entry.getValue();
+ if (value != null || copyIfNull) {
+ Beans.setProperty(target, entry.getKey(), value);
+ }
+ } catch (Exception e) {
+ throw new SumarisTechnicalException(String.format("Cannot set %s.%s using value: %s",
+ target.getClass().getSimpleName(),
+ entry.getKey(),
+ entry.getValue()));
+ }
+ });
+ }
}
+ protected void copyProperties(final IReferentialEntity source, ReferentialVO target) {
+
+ // TODO use EntitiesUtils
+ switch (source.getClass().getSimpleName()) {
+ case "Method" -> {
+ target.setProperties(ImmutableMap.of(
+ Method.Fields.IS_CALCULATED, ((Method)source).getIsCalculated(),
+ Method.Fields.IS_ESTIMATED, ((Method)source).getIsEstimated()
+ ));
+ }
+ }
+ }
}
diff --git a/sumaris-core/src/main/java/net/sumaris/core/dao/referential/ReferentialEntities.java b/sumaris-core/src/main/java/net/sumaris/core/dao/referential/ReferentialEntities.java
index 9b69df84c3..958ebc3520 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/dao/referential/ReferentialEntities.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/dao/referential/ReferentialEntities.java
@@ -133,6 +133,7 @@ public class ReferentialEntities {
UserProfile.class,
SaleType.class,
VesselType.class,
+ ObjectType.class,
// Taxon group
TaxonGroupType.class,
TaxonGroup.class,
@@ -235,6 +236,7 @@ protected static final Map createLevelPropertyNameMa
// Other level (not having "level" in id)
result.put(Pmfm.class.getSimpleName(), BeanUtils.getPropertyDescriptor(Pmfm.class, Pmfm.Fields.PARAMETER));
+ result.put(Parameter.class.getSimpleName(), BeanUtils.getPropertyDescriptor(Parameter.class, Parameter.Fields.PARAMETER_GROUP));
result.put(Fraction.class.getSimpleName(), BeanUtils.getPropertyDescriptor(Fraction.class, Fraction.Fields.MATRIX));
result.put(QualitativeValue.class.getSimpleName(), BeanUtils.getPropertyDescriptor(QualitativeValue.class, QualitativeValue.Fields.PARAMETER));
result.put(TaxonGroup.class.getSimpleName(), BeanUtils.getPropertyDescriptor(TaxonGroup.class, TaxonGroup.Fields.TAXON_GROUP_TYPE));
@@ -247,6 +249,10 @@ protected static final Map createLevelPropertyNameMa
result.put(ExtractionProductTable.class.getSimpleName(), BeanUtils.getPropertyDescriptor(ExtractionProductTable.class, ExtractionProductTable.Fields.PRODUCT));
result.put(LocationLevel.class.getSimpleName(), BeanUtils.getPropertyDescriptor(LocationLevel.class, LocationLevel.Fields.LOCATION_CLASSIFICATION));
result.put(Gear.class.getSimpleName(), BeanUtils.getPropertyDescriptor(Gear.class, Gear.Fields.GEAR_CLASSIFICATION));
+ result.put(TranscribingItemType.class.getSimpleName(), BeanUtils.getPropertyDescriptor(TranscribingItemType.class, TranscribingItemType.Fields.OBJECT_TYPE));
+ result.put(TranscribingItem.class.getSimpleName(), BeanUtils.getPropertyDescriptor(TranscribingItem.class, TranscribingItem.Fields.TYPE));
+
+ // TODO remove the following put ? Too many put for the same Program class
result.put(Program.class.getSimpleName(), BeanUtils.getPropertyDescriptor(Program.class, Program.Fields.GEAR_CLASSIFICATION));
result.put(Program.class.getSimpleName(), BeanUtils.getPropertyDescriptor(Program.class, Program.Fields.TAXON_GROUP_TYPE));
diff --git a/sumaris-core/src/main/java/net/sumaris/core/dao/referential/conversion/RoundWeightConversionSpecifications.java b/sumaris-core/src/main/java/net/sumaris/core/dao/referential/conversion/RoundWeightConversionSpecifications.java
index 8251a8910d..1590e95fa0 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/dao/referential/conversion/RoundWeightConversionSpecifications.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/dao/referential/conversion/RoundWeightConversionSpecifications.java
@@ -27,8 +27,8 @@
import net.sumaris.core.dao.technical.DatabaseType;
import net.sumaris.core.dao.technical.Page;
import net.sumaris.core.dao.technical.jpa.BindableSpecification;
-import net.sumaris.core.model.data.VesselFeatures;
import net.sumaris.core.model.referential.conversion.RoundWeightConversion;
+import net.sumaris.core.util.Dates;
import net.sumaris.core.vo.referential.conversion.RoundWeightConversionFetchOptions;
import net.sumaris.core.vo.referential.conversion.RoundWeightConversionFilterVO;
import net.sumaris.core.vo.referential.conversion.RoundWeightConversionVO;
@@ -66,10 +66,10 @@ default Specification atDate(Date aDate) {
return cb.not(
cb.or(
cb.lessThan(Daos.nvlEndDate(root, cb, RoundWeightConversion.Fields.END_DATE, getDatabaseType()), dateParam),
- cb.greaterThan(root.get(VesselFeatures.Fields.START_DATE), dateParam)
+ cb.greaterThan(root.get(RoundWeightConversion.Fields.START_DATE), dateParam)
)
);
- }).addBind(DATE_PARAMETER, aDate);
+ }).addBind(DATE_PARAMETER, Dates.resetTime(aDate));
}
diff --git a/sumaris-core/src/main/java/net/sumaris/core/dao/referential/conversion/WeightLengthConversionRepositoryImpl.java b/sumaris-core/src/main/java/net/sumaris/core/dao/referential/conversion/WeightLengthConversionRepositoryImpl.java
index 2072932791..d448b5ab6c 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/dao/referential/conversion/WeightLengthConversionRepositoryImpl.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/dao/referential/conversion/WeightLengthConversionRepositoryImpl.java
@@ -208,6 +208,7 @@ protected Specification toSpecification(@NonNull WeightL
.and(atYear(filter.getYear()))
.and(hasReferenceTaxonIds(filter.getReferenceTaxonIds()))
.and(hasLocationIds(filter.getLocationIds()))
+ .and(hasChildLocationIds(filter.getChildLocationIds()))
.and(hasRectangleLabels(filter.getRectangleLabels()))
.and(hasLengthParameterIds(filter.getLengthParameterIds()))
.and(hasLengthUnitIds(filter.getLengthUnitIds()))
diff --git a/sumaris-core/src/main/java/net/sumaris/core/dao/referential/conversion/WeightLengthConversionSpecifications.java b/sumaris-core/src/main/java/net/sumaris/core/dao/referential/conversion/WeightLengthConversionSpecifications.java
index ae3c16cc01..c22f363605 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/dao/referential/conversion/WeightLengthConversionSpecifications.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/dao/referential/conversion/WeightLengthConversionSpecifications.java
@@ -48,6 +48,7 @@ public interface WeightLengthConversionSpecifications
extends IEntityWithStatusSpecifications {
String MONTH_PARAMETER = "month";
+ String CHILD_LOCATION_IDS_PARAMETER = "childLocationIds";
String RECTANGLE_LABELS_PARAMETER = "rectangleLabels";
String RECTANGLE_LEVEL_IDS_PARAMETER = "rectangleLevelIds";
String LEANGTH_PMFM_IDS_PARAMETER = "lengthPmfmIds";
@@ -60,6 +61,27 @@ default Specification hasLocationIds(Integer... ids) {
return hasInnerJoinIds(WeightLengthConversion.Fields.LOCATION, ids);
}
+ default Specification hasChildLocationIds(Integer... childLocationIds) {
+ if (ArrayUtils.isEmpty(childLocationIds)) return null;
+
+ return BindableSpecification.where((root, query, cb) -> {
+ ParameterExpression idsParam = cb.parameter(Collection.class, CHILD_LOCATION_IDS_PARAMETER);
+
+ Subquery subQuery = query.subquery(LocationHierarchy.class);
+ Root lh = subQuery.from(LocationHierarchy.class);
+ subQuery.select(lh.get(LocationHierarchy.Fields.PARENT_LOCATION));
+
+ subQuery.where(
+ cb.and(
+ cb.equal(lh.get(LocationHierarchy.Fields.PARENT_LOCATION), Daos.composeJoin(root, WeightLengthConversion.Fields.LOCATION, JoinType.INNER)),
+ Daos.composePath(lh, StringUtils.doting(LocationHierarchy.Fields.CHILD_LOCATION, Location.Fields.ID), JoinType.INNER).in(idsParam)
+ )
+ );
+
+ return cb.exists(subQuery);
+ })
+ .addBind(CHILD_LOCATION_IDS_PARAMETER, Arrays.asList(childLocationIds));
+ }
default Specification hasRectangleLabels(String... rectangleLabels) {
if (ArrayUtils.isEmpty(rectangleLabels)) return null;
Integer[] rectangleLocationLevelIds = LocationLevels.getStatisticalRectangleLevelIds();
diff --git a/sumaris-core/src/main/java/net/sumaris/core/event/data/BatchEventListener.java b/sumaris-core/src/main/java/net/sumaris/core/event/data/BatchEventListener.java
index 16a264ccfb..ce41a0c209 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/event/data/BatchEventListener.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/event/data/BatchEventListener.java
@@ -27,7 +27,7 @@
import net.sumaris.core.event.entity.EntityEventService;
import net.sumaris.core.event.entity.EntityUpdateEvent;
import net.sumaris.core.model.data.Batch;
-import net.sumaris.core.service.data.DenormalizedBatchService;
+import net.sumaris.core.service.data.denormalize.DenormalizedBatchService;
import net.sumaris.core.vo.data.batch.BatchVO;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
diff --git a/sumaris-core/src/main/java/net/sumaris/core/model/administration/programStrategy/ProgramPropertyEnum.java b/sumaris-core/src/main/java/net/sumaris/core/model/administration/programStrategy/ProgramPropertyEnum.java
index c3ba376623..fb1a3e005f 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/model/administration/programStrategy/ProgramPropertyEnum.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/model/administration/programStrategy/ProgramPropertyEnum.java
@@ -22,6 +22,8 @@
* #L%
*/
+import lombok.NonNull;
+
import java.io.Serializable;
import java.util.Arrays;
@@ -32,6 +34,13 @@ public enum ProgramPropertyEnum implements Serializable {
TRIP_BATCH_TAXON_NAME_ENABLE("sumaris.trip.operation.batch.taxonName.enable", Boolean.class, Boolean.TRUE.toString()),
+ TRIP_BATCH_TAXON_GROUP_ENABLE("sumaris.trip.operation.batch.taxonGroup.enable", Boolean.class, Boolean.TRUE.toString()),
+
+ TRIP_BATCH_TAXON_GROUPS_NO_WEIGHT("sumaris.trip.operation.batch.taxonGroups.noWeight", String.class, ""),
+
+ TRIP_BATCH_LENGTH_WEIGHT_CONVERSION_ENABLE("sumaris.trip.operation.batch.lengthWeightConversion.enable", Boolean.class, Boolean.FALSE.toString()),
+ TRIP_BATCH_ROUND_WEIGHT_CONVERSION_COUNTRY_ID("sumaris.trip.operation.batch.roundWeightConversion.country.id", Integer.class, null),
+
TRIP_BATCH_MEASURE_INDIVIDUAL_TAXON_NAME_ENABLE("sumaris.trip.operation.batch.individual.taxonName.enable",
Boolean.class, Boolean.TRUE.toString()),
@@ -41,27 +50,27 @@ public enum ProgramPropertyEnum implements Serializable {
;
- private String label;
+ private String key;
private String defaultValue;
private Class type;
- ProgramPropertyEnum(String label, Class type, String defaultValue) {
- this.label = label;
+ ProgramPropertyEnum(String key, Class type, String defaultValue) {
+ this.key = key;
this.type = type;
this.defaultValue = defaultValue;
}
- public static ProgramPropertyEnum findByLabel(final String label) {
- return Arrays.stream(values()).filter(item -> item.name().equalsIgnoreCase(label)).findFirst()
- .orElseThrow(() -> new IllegalArgumentException("Unknown ProgramPropertyEnum label: " + label));
+ public static ProgramPropertyEnum findByKey(@NonNull final String key) {
+ return Arrays.stream(values()).filter(item -> key.equalsIgnoreCase(item.getKey())).findFirst()
+ .orElseThrow(() -> new IllegalArgumentException("Unknown ProgramPropertyEnum with key: " + key));
}
- public String getLabel() {
- return label;
+ public String getKey() {
+ return key;
}
- public void setLabel(String label) {
- this.label = label;
+ public void setKey(String key) {
+ this.key = key;
}
diff --git a/sumaris-core/src/main/java/net/sumaris/core/model/data/DenormalizedBatch.java b/sumaris-core/src/main/java/net/sumaris/core/model/data/DenormalizedBatch.java
index 36143c0678..0dc3129ea5 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/model/data/DenormalizedBatch.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/model/data/DenormalizedBatch.java
@@ -94,6 +94,37 @@ public class DenormalizedBatch implements IEntity {
@Column(name = "elevate_individual_count")
private Integer elevateIndividualCount;
+ /**
+ * Nombre d'individus élevé à l'échelle du groupe de taxon, ou par défaut du taxon (cf mantis
+ * #37645)
+ * Calculé par le traitement de dénormalisation.
+ * @return this.taxonElevateIndivCount Integer
+ */
+ @Column(name = "taxon_elevate_indiv_count")
+ private Double taxonElevateIndividualCount;
+
+ /**
+ * Poids contextuel élevé à l'échelle du groupe de taxon, ou par défaut du taxon (cf mantis
+ * #37645).
+ * Calculé par le traitement de dénormalisation.
+ */
+ @Column(name = "taxon_elevate_context_weight")
+ private Double taxonElevateContextWeight;
+
+ /**
+ * Poids vif sans élévation reconstitué à partir du poids RTP (cf mantis #30088).
+ * Calculé par le traitement de dénormalisation.
+ */
+ @Column(name = "indirect_rtp_weight")
+ private Double indirectRtpWeight;
+
+ /**
+ * Poids vif élevé et reconstitué à partir du poids RTP (cf mantis #30088).
+ * Calculé par le traitement de dénormalisation.
+ */
+ @Column(name = "elevate_rtp_weight")
+ private Double elevateRtpWeight;
+
@Column(name = "sampling_ratio")
private Double samplingRatio;
@@ -137,6 +168,20 @@ public class DenormalizedBatch implements IEntity {
@JoinColumn(name = "inherited_taxon_group_fk")
private TaxonGroup inheritedTaxonGroup;
+ /**
+ * L'espèce commerciale déterminée à partir de l'espèce scientifique.
+ * Calculé par le traitement de dénormalisation.
+ * Dans le cas où le lot ni aucun de ses lots pères indique une espèce commerciale, le
+ * traitement détermine la plus forte probalité d'appartenance de l'espèce scientifique à une
+ * espèce ciommerciale.
+ * Pour cela, les correspondances existantes entre TAXON_GROUP et REFERENCE_TAXON sont
+ * exploitées (cf table TAXON_GROUP_HISTORICAL_RECORD), ou le cas échéant les correspodnances
+ * trouvées dans l'arbre d'achantillonnage courant (typiquement dans la "partie retenue" PR, ou
+ * les deux types d'espèces une chance d'avoir été déjà saisis).
+ * Ce champ sert à afficher le nom commerciale probable, à côté de chaque informations relatifs
+ * à une espèce scientifique. C'est le cas par exemple dans les rapports de restitution OBSMER
+ * aux professionnels.
+ **/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "calculated_taxon_group_fk")
private TaxonGroup calculatedTaxonGroup;
diff --git a/sumaris-core/src/main/java/net/sumaris/core/model/data/VesselPosition.java b/sumaris-core/src/main/java/net/sumaris/core/model/data/VesselPosition.java
index b22754c777..c762bdf495 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/model/data/VesselPosition.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/model/data/VesselPosition.java
@@ -25,6 +25,7 @@
import lombok.*;
import lombok.ToString;
import lombok.experimental.FieldNameConstants;
+import net.sumaris.core.dao.data.IPosition;
import net.sumaris.core.model.administration.user.Department;
import net.sumaris.core.model.referential.QualityFlag;
@@ -37,7 +38,7 @@
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@FieldNameConstants
@Entity(name = "vessel_position")
-public class VesselPosition implements IDataEntity {
+public class VesselPosition implements IDataEntity, IPosition {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "VESSEL_POSITION_SEQ")
diff --git a/sumaris-core/src/main/java/net/sumaris/core/model/referential/QualityFlags.java b/sumaris-core/src/main/java/net/sumaris/core/model/referential/QualityFlags.java
new file mode 100644
index 0000000000..2946a645b1
--- /dev/null
+++ b/sumaris-core/src/main/java/net/sumaris/core/model/referential/QualityFlags.java
@@ -0,0 +1,77 @@
+/*
+ * #%L
+ * SUMARiS
+ * %%
+ * Copyright (C) 2019 SUMARiS Consortium
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program. If not, see
+ * .
+ * #L%
+ */
+
+package net.sumaris.core.model.referential;
+
+import lombok.NonNull;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Objects;
+
+public abstract class QualityFlags {
+
+ protected QualityFlags(){
+ // helper class
+ }
+
+ public static boolean isInvalid(int qualityFlagId) {
+ return isInvalid(QualityFlagEnum.valueOf(qualityFlagId));
+ }
+
+ public static boolean isInvalid(@NonNull QualityFlagEnum qualityFlag) {
+ switch (qualityFlag) {
+ case BAD:
+ case MISSING:
+ case NOT_COMPLETED:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ public static boolean isValid(int qualityFlagId) {
+ return isValid(QualityFlagEnum.valueOf(qualityFlagId));
+ }
+
+ public static boolean isValid(@NonNull QualityFlagEnum qualityFlag) {
+ return !isInvalid(qualityFlag);
+ }
+
+ public static Integer worst(Integer... qualityFlags) {
+ return Arrays.stream(qualityFlags)
+ .filter(Objects::nonNull)
+ // Sort (invalid first, with a negative id)
+ .sorted(Comparator.comparingInt(qualityFlagId -> isInvalid(qualityFlagId) ? -1 * qualityFlagId : (10 - qualityFlagId)))
+ .findFirst()
+ .orElse(null);
+ }
+
+ public static QualityFlagEnum worst(QualityFlagEnum... qualityFlags) {
+ return Arrays.stream(qualityFlags)
+ .filter(Objects::nonNull)
+ // Sort (invalid first, with a negative id)
+ .sorted(Comparator.comparingInt(qualityFlag -> isInvalid(qualityFlag) ? -1 * qualityFlag.getId() : (10 - qualityFlag.getId())))
+ .findFirst()
+ .orElse(null);
+ }
+}
diff --git a/sumaris-core/src/main/java/net/sumaris/core/model/referential/pmfm/ParameterEnum.java b/sumaris-core/src/main/java/net/sumaris/core/model/referential/pmfm/ParameterEnum.java
index a1c4bab05f..6dca7a63f2 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/model/referential/pmfm/ParameterEnum.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/model/referential/pmfm/ParameterEnum.java
@@ -24,14 +24,17 @@
import net.sumaris.core.model.IEntity;
import net.sumaris.core.model.annotation.EntityEnum;
+import net.sumaris.core.model.annotation.IEntityEnum;
import java.io.Serializable;
import java.util.Arrays;
@EntityEnum(entity = Parameter.class, joinAttributes = {ParameterGroup.Fields.LABEL, IEntity.Fields.ID})
-public enum ParameterEnum implements Serializable {
+public enum ParameterEnum implements Serializable, IEntityEnum {
- HULL_MATERIAL(420, "HULL_MATERIAL")
+ HULL_MATERIAL(420, "HULL_MATERIAL"),
+
+ SEX(80, "SEX")
;
diff --git a/sumaris-core/src/main/java/net/sumaris/core/model/referential/pmfm/PmfmEnum.java b/sumaris-core/src/main/java/net/sumaris/core/model/referential/pmfm/PmfmEnum.java
index 3e3b98e497..2c93a7c187 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/model/referential/pmfm/PmfmEnum.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/model/referential/pmfm/PmfmEnum.java
@@ -23,12 +23,13 @@
*/
import net.sumaris.core.model.annotation.EntityEnum;
+import net.sumaris.core.model.annotation.IEntityEnum;
import java.io.Serializable;
import java.util.Arrays;
@EntityEnum(entity = Pmfm.class, joinAttributes = Pmfm.Fields.LABEL, required = false)
-public enum PmfmEnum implements Serializable {
+public enum PmfmEnum implements IEntityEnum, Serializable {
SMALLER_MESH_GAUGE_MM(3, "SMALLER_MESH_GAUGE_MM"),
HEADLINE_CUMULATIVE_LENGTH(12, "HEADLINE_CUMULATIVE_LENGTH"),
diff --git a/sumaris-core/src/main/java/net/sumaris/core/model/referential/pmfm/QualitativeValueEnum.java b/sumaris-core/src/main/java/net/sumaris/core/model/referential/pmfm/QualitativeValueEnum.java
index 94d188de69..e6d3b7a846 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/model/referential/pmfm/QualitativeValueEnum.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/model/referential/pmfm/QualitativeValueEnum.java
@@ -22,18 +22,21 @@
* #L%
*/
+import net.sumaris.core.model.administration.programStrategy.Program;
import net.sumaris.core.model.annotation.EntityEnum;
+import net.sumaris.core.model.annotation.IEntityEnum;
import java.io.Serializable;
import java.util.Arrays;
-@EntityEnum(entity = QualitativeValue.class)
-public enum QualitativeValueEnum implements Serializable {
+@EntityEnum(entity = QualitativeValue.class, joinAttributes = {QualitativeValue.Fields.LABEL, QualitativeValue.Fields.ID})
+public enum QualitativeValueEnum implements Serializable, IEntityEnum {
SORTING_BULK(390, "VRAC"), // Adagio => 311
SORTING_NON_BULK(391, "H-VRAC"), // Adagio => 310
SORTING_UNSORTED(392, "NONE"), // Adagio => 2146
- DRESSING_WHOLE(381, "WHL"),
+ DRESSING_WHOLE(381, "WHL"), // Entier - Adagio => 139
+ DRESSING_GUTTED(381, "GUT"), // Eviscéré - Adagio => 120
PRESERVATION_FRESH(332, "FRE"),
SIZE_CATEGORY_NONE(435, "UNS"),
@@ -47,6 +50,9 @@ public enum QualitativeValueEnum implements Serializable {
// LANDING_OR_DISCARD
LANDING(190, "LAN"),
DISCARD(191, "DIS"),
+
+ // SEX
+ SEX_UNSEXED(188, "NS"), // Adagio => 302
;
public static QualitativeValueEnum valueOf(final int id) {
diff --git a/sumaris-core/src/main/java/net/sumaris/core/model/referential/pmfm/UnitEnum.java b/sumaris-core/src/main/java/net/sumaris/core/model/referential/pmfm/UnitEnum.java
index 9b5d0b3f71..ef5461cf85 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/model/referential/pmfm/UnitEnum.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/model/referential/pmfm/UnitEnum.java
@@ -22,18 +22,20 @@
package net.sumaris.core.model.referential.pmfm;
+import net.sumaris.core.model.administration.programStrategy.Program;
import net.sumaris.core.model.annotation.EntityEnum;
import java.io.Serializable;
import java.util.Arrays;
-@EntityEnum(entity = Unit.class)
+@EntityEnum(entity = Unit.class, joinAttributes = {Unit.Fields.LABEL})
public enum UnitEnum implements Serializable {
NONE(0, "None"),
- MM(1, "mm"),
KG(3, "kg"),
- CM(12, "cm");
+ MM(1, "mm"),
+ CM(12, "cm")
+ ;
public static UnitEnum valueOf(final int id) {
return Arrays.stream(values())
diff --git a/sumaris-core/src/main/java/net/sumaris/core/model/technical/extraction/rdb/ProductRdbLanding.java b/sumaris-core/src/main/java/net/sumaris/core/model/technical/extraction/rdb/ProductRdbLanding.java
index 2d2d8b9934..b239373077 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/model/technical/extraction/rdb/ProductRdbLanding.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/model/technical/extraction/rdb/ProductRdbLanding.java
@@ -33,7 +33,6 @@
@Getter
@Setter
-
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@FieldNameConstants
@Entity
@@ -73,7 +72,7 @@ public class ProductRdbLanding implements Serializable, IEntity {
private Integer id;
@Column(nullable = false, length = 2, name = COLUMN_RECORD_TYPE)
- @ColumnDefault(SHEET_NAME)
+ @ColumnDefault("'" + SHEET_NAME + "'")
private String recordType;
@Column(nullable = false, length = 3, name = COLUMN_LANDING_COUNTRY)
diff --git a/sumaris-core/src/main/java/net/sumaris/core/model/technical/extraction/rdb/ProductRdbSpeciesLength.java b/sumaris-core/src/main/java/net/sumaris/core/model/technical/extraction/rdb/ProductRdbSpeciesLength.java
index fb1303515d..06bd888607 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/model/technical/extraction/rdb/ProductRdbSpeciesLength.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/model/technical/extraction/rdb/ProductRdbSpeciesLength.java
@@ -70,7 +70,7 @@ public class ProductRdbSpeciesLength implements Serializable, IEntity {
private Integer id;
@Column(nullable = false, length = 2, name = COLUMN_RECORD_TYPE)
- @ColumnDefault(SHEET_NAME)
+ @ColumnDefault("'" + SHEET_NAME + "'")
private String recordType;
@Column(nullable = false, length = 2, name = COLUMN_SAMPLING_TYPE)
diff --git a/sumaris-core/src/main/java/net/sumaris/core/model/technical/extraction/rdb/ProductRdbSpeciesList.java b/sumaris-core/src/main/java/net/sumaris/core/model/technical/extraction/rdb/ProductRdbSpeciesList.java
index 5e714b17a3..fdeb39aa8a 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/model/technical/extraction/rdb/ProductRdbSpeciesList.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/model/technical/extraction/rdb/ProductRdbSpeciesList.java
@@ -70,7 +70,7 @@ public class ProductRdbSpeciesList implements Serializable, IEntity {
private Integer id;
@Column(nullable = false, length = 2, name = COLUMN_RECORD_TYPE)
- @ColumnDefault(SHEET_NAME)
+ @ColumnDefault("'" + SHEET_NAME + "'")
private String recordType;
@Column(nullable = false, length = 2, name = COLUMN_SAMPLING_TYPE)
diff --git a/sumaris-core/src/main/java/net/sumaris/core/model/technical/extraction/rdb/ProductRdbStation.java b/sumaris-core/src/main/java/net/sumaris/core/model/technical/extraction/rdb/ProductRdbStation.java
index 487e04921b..e274473c63 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/model/technical/extraction/rdb/ProductRdbStation.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/model/technical/extraction/rdb/ProductRdbStation.java
@@ -83,7 +83,7 @@ public class ProductRdbStation implements Serializable, IEntity {
private Integer id;
@Column(nullable = false, length = 2, name = COLUMN_RECORD_TYPE)
- @ColumnDefault(SHEET_NAME)
+ @ColumnDefault("'" + SHEET_NAME + "'")
private String recordType;
@Column(nullable = false, length = 2, name = COLUMN_SAMPLING_TYPE)
diff --git a/sumaris-core/src/main/java/net/sumaris/core/model/technical/extraction/rdb/ProductRdbTrip.java b/sumaris-core/src/main/java/net/sumaris/core/model/technical/extraction/rdb/ProductRdbTrip.java
index 65ec14eecd..4fcf463ce3 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/model/technical/extraction/rdb/ProductRdbTrip.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/model/technical/extraction/rdb/ProductRdbTrip.java
@@ -67,7 +67,7 @@ public class ProductRdbTrip implements Serializable, IEntity {
private Integer id;
@Column(nullable = false, length = 2, name = COLUMN_RECORD_TYPE)
- @ColumnDefault(SHEET_NAME)
+ @ColumnDefault("'" + SHEET_NAME + "'")
private String recordType;
@Column(nullable = false, length = 2, name = COLUMN_SAMPLING_TYPE)
diff --git a/sumaris-core/src/main/java/net/sumaris/core/service/administration/programStrategy/ProgramService.java b/sumaris-core/src/main/java/net/sumaris/core/service/administration/programStrategy/ProgramService.java
index a3f88fd7e7..8b46ac9846 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/service/administration/programStrategy/ProgramService.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/service/administration/programStrategy/ProgramService.java
@@ -56,6 +56,7 @@ public interface ProgramService {
@Transactional(readOnly = true)
ProgramVO getByLabel(String label);
+ @Transactional(readOnly = true)
ProgramVO getByLabel(String label, ProgramFetchOptions fetchOptions);
@Transactional(readOnly = true)
diff --git a/sumaris-core/src/main/java/net/sumaris/core/service/administration/programStrategy/ProgramServiceImpl.java b/sumaris-core/src/main/java/net/sumaris/core/service/administration/programStrategy/ProgramServiceImpl.java
index e7e9247198..72933d928d 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/service/administration/programStrategy/ProgramServiceImpl.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/service/administration/programStrategy/ProgramServiceImpl.java
@@ -25,7 +25,9 @@
import com.google.common.base.Preconditions;
import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import net.sumaris.core.config.CacheConfiguration;
import net.sumaris.core.dao.administration.programStrategy.AcquisitionLevelRepository;
import net.sumaris.core.dao.administration.programStrategy.ProgramRepository;
import net.sumaris.core.dao.technical.Page;
@@ -41,6 +43,7 @@
import net.sumaris.core.vo.referential.ReferentialVO;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import javax.annotation.Nullable;
@@ -51,14 +54,13 @@
import java.util.stream.Stream;
@Service("programService")
+@RequiredArgsConstructor
@Slf4j
public class ProgramServiceImpl implements ProgramService {
- @Autowired
- protected ProgramRepository programRepository;
+ protected final ProgramRepository programRepository;
- @Autowired
- protected StrategyService strategyService;
+ protected final StrategyService strategyService;
@Override
public List getAll() {
diff --git a/sumaris-core/src/main/java/net/sumaris/core/service/data/DenormalizedBatchServiceImpl.java b/sumaris-core/src/main/java/net/sumaris/core/service/data/DenormalizedBatchServiceImpl.java
deleted file mode 100644
index 1d4b84d1fd..0000000000
--- a/sumaris-core/src/main/java/net/sumaris/core/service/data/DenormalizedBatchServiceImpl.java
+++ /dev/null
@@ -1,648 +0,0 @@
-package net.sumaris.core.service.data;
-
-/*-
- * #%L
- * SUMARiS:: Core
- * %%
- * Copyright (C) 2018 SUMARiS Consortium
- * %%
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public
- * License along with this program. If not, see
- * .
- * #L%
- */
-
-
-import com.google.common.base.Preconditions;
-import lombok.NonNull;
-import lombok.extern.slf4j.Slf4j;
-import net.sumaris.core.config.SumarisConfigurationOption;
-import net.sumaris.core.dao.data.batch.BatchRepository;
-import net.sumaris.core.dao.data.batch.DenormalizedBatchRepository;
-import net.sumaris.core.dao.data.batch.InvalidSamplingBatchException;
-import net.sumaris.core.model.TreeNodeEntities;
-import net.sumaris.core.exception.SumarisTechnicalException;
-import net.sumaris.core.model.referential.QualityFlagEnum;
-import net.sumaris.core.model.referential.StatusEnum;
-import net.sumaris.core.model.referential.taxon.TaxonGroupTypeEnum;
-import net.sumaris.core.service.administration.programStrategy.ProgramService;
-import net.sumaris.core.service.referential.taxon.TaxonGroupService;
-import net.sumaris.core.util.Beans;
-import net.sumaris.core.util.StringUtils;
-import net.sumaris.core.util.TimeUtils;
-import net.sumaris.core.vo.administration.programStrategy.ProgramFetchOptions;
-import net.sumaris.core.vo.administration.programStrategy.ProgramVO;
-import net.sumaris.core.vo.administration.programStrategy.Programs;
-import net.sumaris.core.vo.data.batch.*;
-import net.sumaris.core.vo.filter.ReferentialFilterVO;
-import net.sumaris.core.vo.referential.TaxonGroupVO;
-import org.apache.commons.collections4.CollectionUtils;
-import org.apache.commons.lang3.mutable.MutableInt;
-import org.apache.commons.lang3.mutable.MutableShort;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-
-import javax.annotation.Nullable;
-import java.math.BigDecimal;
-import java.math.RoundingMode;
-import java.util.*;
-import java.util.stream.Collectors;
-
-@Service("denormalizedBatchService")
-@Slf4j
-public class DenormalizedBatchServiceImpl implements DenormalizedBatchService {
-
- @Autowired
- protected DenormalizedBatchRepository denormalizedBatchRepository;
-
- @Autowired
- protected BatchRepository batchRepository;
-
- @Autowired
- protected ProgramService programService;
-
- @Autowired
- protected OperationService operationService;
-
- @Autowired
- protected SaleService saleService;
-
- @Autowired
- protected TaxonGroupService taxonGroupService;
-
- @Override
- public List denormalize(@NonNull BatchVO catchBatch, @NonNull final DenormalizedBatchOptions options) {
-
- boolean trace = log.isTraceEnabled();
- long startTime = System.currentTimeMillis();
- final MutableShort flatRankOrder = new MutableShort(0);
-
- List result = TreeNodeEntities.streamAllAndMap(catchBatch, (source, parent) -> {
- TempDenormalizedBatchVO target = createTempVO(source);
-
- // Add to parent's children
- if (parent != null) parent.addChildren(target);
-
- // Depth level
- if (parent == null) {
- target.setTreeLevel((short)1); // First level
- if (target.getIsLanding() == null) target.setIsLanding(false);
- if (target.getIsDiscard() == null) target.setIsDiscard(false);
- }
- else {
- target.setTreeLevel((short)(parent.getTreeLevel() + 1));
- // Inherit taxon group
- if (target.getInheritedTaxonGroup() == null && parent.getInheritedTaxonGroup() != null) {
- target.setInheritedTaxonGroup(parent.getInheritedTaxonGroup());
- }
- // Inherit taxon name
- if (target.getInheritedTaxonName() == null && parent.getInheritedTaxonName() != null) {
- target.setInheritedTaxonName(parent.getInheritedTaxonName());
- }
- // Exhaustive inventory
- if (target.getExhaustiveInventory() == null) {
- // Always true, when:
- // - taxon name is defined
- // - taxon group is defined and taxon Name disable (in options)
- if (target.getInheritedTaxonName() != null) {
- target.setExhaustiveInventory(Boolean.TRUE);
- }
- else if (target.getInheritedTaxonGroup() != null && !options.isEnableTaxonName()) {
- target.setExhaustiveInventory(Boolean.TRUE);
- }
- else if (parent.getExhaustiveInventory() != null) {
- target.setExhaustiveInventory(parent.getExhaustiveInventory());
- }
- }
- // Inherit location
- if (parent.getLocationId() != null) {
- target.setLocationId(parent.getLocationId());
- }
- // Inherit landing / discard
- if (target.getIsLanding() == null) {
- target.setIsLanding(parent.getIsLanding());
- }
- if (target.getIsDiscard() == null) {
- target.setIsDiscard(parent.getIsDiscard());
- }
-
- // Inherit quality flag (keep the worst value)
- if (parent.getQualityFlagId() > target.getQualityFlagId()) {
- target.setQualityFlagId(parent.getQualityFlagId());
- }
-
- // If current quality is out of stats
- if (target.getQualityFlagId() >= QualityFlagEnum.OUT_STATS.getId()) {
- // Force both parent and current parent exhaustive inventory to FALSE
- parent.setExhaustiveInventory(Boolean.FALSE);
- target.setExhaustiveInventory(Boolean.FALSE);
- }
-
- // Inherit sorting values
- Beans.getStream(parent.getSortingValues()).forEach(svSource -> {
- DenormalizedBatchSortingValueVO svTarget = new DenormalizedBatchSortingValueVO();
- Beans.copyProperties(svSource, svTarget);
- svTarget.setIsInherited(true);
- svTarget.setRankOrder(svSource.getRankOrder() / 10);
- target.addSortingValue(svTarget);
- });
-
- }
-
- return target;
- })
-
- // Sort
- .sorted(Comparator.comparing(DenormalizedBatches::computeFlatOrder))
-
- .map(target -> {
- // Compute flat rank order
- flatRankOrder.increment();
- target.setFlatRankOrder(flatRankOrder.getValue());
-
- // Compute tree indent (run once, on the root batch)
- if (target.getParent() == null) computeTreeIndent(target);
-
- return target;
- })
- .collect(Collectors.toList());
-
- if (CollectionUtils.size(result) == 1) {
- DenormalizedBatchVO target = result.get(0);
- target.setElevateWeight(target.getWeight());
- }
- else {
- // Compute indirect values
- computeIndirectValues(result);
-
- // Elevate weight
- computeElevatedValues(result);
- }
-
- // Log
- if (trace) {
- log.trace("Successfully denormalized batches, in {}:\n{}",
- TimeUtils.printDurationFrom(startTime),
- DenormalizedBatches.dumpAsString(result, true, true));
- }
- else {
- log.debug("Successfully denormalized batches, in {}", TimeUtils.printDurationFrom(startTime));
- }
-
- return result;
- }
-
- @Override
- public List denormalizeAndSaveByOperationId(int operationId, @Nullable DenormalizedBatchOptions options) {
- BatchVO catchBatch = batchRepository.getCatchBatchByOperationId(operationId, BatchFetchOptions.builder()
- .withChildrenEntities(true)
- .withMeasurementValues(true)
- .withRecorderDepartment(false)
- .build());
- if (catchBatch == null) return null;
-
- long startTime = System.currentTimeMillis();
- log.debug("Denormalize batches of operation {id: {}}...", operationId);
-
- // Compute options, for the operation's program
- if (options == null) {
- int programId = operationService.getProgramIdById(operationId);
- options = createOptionsByProgramId(programId);
- }
-
- // Denormalize batches
- List batches = denormalize(catchBatch, options);
-
- // Save denormalized batches
- batches = denormalizedBatchRepository.saveAllByOperationId(operationId, batches);
-
- log.debug("Denormalize batches of operation {id: {}} [OK] in {}", operationId, TimeUtils.printDurationFrom(startTime));
- return batches;
- }
-
- @Override
- public List denormalizeAndSaveBySaleId(int saleId, @Nullable DenormalizedBatchOptions options) {
- BatchVO catchBatch = batchRepository.getCatchBatchBySaleId(saleId, BatchFetchOptions.builder()
- .withChildrenEntities(true)
- .withMeasurementValues(true)
- .withRecorderDepartment(false)
- .build());
- if (catchBatch == null) return null;
-
- long startTime = System.currentTimeMillis();
- log.debug("Denormalize batches of sale {id: {}}...", saleId);
-
- // Compute options, for the sale's program
- if (options == null) {
- int programId = saleService.getProgramIdById(saleId);
- options = createOptionsByProgramId(programId);
- }
-
- // Denormalize batches
- List denormalizedBatches = denormalize(catchBatch, options);
-
- // Save denormalized batches
- List result = denormalizedBatchRepository.saveAllBySaleId(saleId, denormalizedBatches);
-
- log.debug("Denormalize batches of sale {id: {}} [OK] in {}", saleId, TimeUtils.printDurationFrom(startTime));
- return result;
- }
-
- @Override
- public DenormalizedBatchOptions createOptionsByProgramId(int programId) {
-
- ProgramVO program = programService.get(programId, ProgramFetchOptions.builder()
- .withProperties(true)
- .withLocations(false)
- .withStrategies(false)
- .build());
-
- String taxonGroupsNoWeight = Programs.getProperty(program, SumarisConfigurationOption.BATCH_TAXON_GROUP_LABELS_NO_WEIGHT);
- List taxonGroupIdsNoWeight = Arrays.stream(taxonGroupsNoWeight.split(","))
- .map(String::trim)
- .map(label -> taxonGroupService.findAllByFilter(ReferentialFilterVO.builder()
- .label(label)
- .levelIds(new Integer[]{TaxonGroupTypeEnum.FAO.getId()})
- .statusIds(new Integer[]{ StatusEnum.ENABLE.getId() })
- .build()).stream().findFirst().orElse(null))
- .filter(Objects::nonNull)
- .map(TaxonGroupVO::getId)
- .collect(Collectors.toList());
-
- return DenormalizedBatchOptions.builder()
- .enableTaxonName(Programs.getPropertyAsBoolean(program, SumarisConfigurationOption.ENABLE_BATCH_TAXON_NAME))
- .enableTaxonGroup(Programs.getPropertyAsBoolean(program, SumarisConfigurationOption.ENABLE_BATCH_TAXON_GROUP))
- .taxonGroupIdsNoWeight(taxonGroupIdsNoWeight)
- .build();
- }
-
- /* -- protected methods -- */
-
- protected void computeIndirectValues(List batches) {
-
- List revertBatches = batches.stream()
- .map(target -> (TempDenormalizedBatchVO)target)
- // Reverse order (start from leaf)
- .sorted(Collections.reverseOrder(Comparator.comparing(DenormalizedBatchVO::getFlatRankOrder)))
- .collect(Collectors.toList());
-
- // Compute indirect values (from children to parent)
- MutableInt changesCount = new MutableInt(0);
- MutableInt loopCounter = new MutableInt(0);
- do {
- changesCount.setValue(0);
- loopCounter.increment();
- log.debug("Computing indirect values (pass #{}) ...", loopCounter);
-
- revertBatches.forEach(batch -> {
- boolean changed = false;
-
- // Indirect weight
- Double indirectWeight = computeIndirectWeight(batch);
- changed = changed || !Objects.equals(indirectWeight, batch.getIndirectWeight());
- batch.setIndirectWeight(indirectWeight);
-
- // Indirect individual count
- Integer indirectIndividualCount = computeIndirectIndividualCount(batch);
- changed = changed || !Objects.equals(indirectIndividualCount, batch.getIndirectIndividualCount());
- batch.setIndirectIndividualCount(indirectIndividualCount);
-
-
- // Contextual weight
- //Double contextWeight = computeContextWeight(target);
- //changed = changed || !Objects.equals(contextWeight, target.getContextWeight());
- //target.setContextWeight(contextWeight);
-
- // Compute Round weight weight
- //Double sumChildRoundWeight = computeSumChildRoundWeight(target);
- //changed = changed || !Objects.equals(sumChildRoundWeight, target.getSumChildRoundWeight());
-
- // Compute RTP weight
- //Double sumChildRtpWeight = computeSumChildRTPWeight(target);
- //changed = changed || !Objects.equals(sumChildRtpWeight, target.getSumChildRTPWeight());
-
- if (changed) changesCount.increment();
- });
-
- log.trace("Computing indirect values (pass #{}) [OK] - {} changes", loopCounter, changesCount);
- }
-
- // Continue while changes has been applied on tree
- while (changesCount.intValue() > 0);
- }
-
- /**
- * Compute elevated values
- */
- protected void computeElevatedValues(List batches) {
- MutableInt changesCount = new MutableInt(0);
- MutableInt loopCounter = new MutableInt(0);
-
- do {
- changesCount.setValue(0);
- loopCounter.increment();
- log.debug("Computing elevated values (pass #{}) ...", loopCounter);
-
- batches.stream()
- .map(target -> (TempDenormalizedBatchVO) target)
- .forEach(target -> {
- boolean changed = false;
-
- log.trace("{} {}", target.getTreeIndent(), target.getLabel());
- BigDecimal elevateFactor = target.getElevateFactor();
- if (elevateFactor == null) {
- elevateFactor = new BigDecimal(1);
- if (target.getParent() != null) {
- elevateFactor = elevateFactor.multiply(((TempDenormalizedBatchVO) target.getParent()).getElevateFactor());
- }
- }
- // Remember it, for children
- target.setElevateFactor(elevateFactor);
-
- Double weight = target.getWeight() != null ? target.getWeight() : target.getIndirectWeight();
- if (weight != null) {
- Double elevateWeight = elevateFactor.multiply(new BigDecimal(weight)).doubleValue();
- changed = changed || !Objects.equals(elevateWeight, target.getElevateWeight());
- target.setElevateWeight(elevateWeight);
- }
-
- Integer individualCount = target.getIndividualCount() != null ? target.getIndividualCount() : target.getIndirectIndividualCount();
- if (individualCount != null) {
- Integer elevateIndividualCount = new BigDecimal(individualCount).multiply(elevateFactor).intValue();
- changed = changed || !Objects.equals(elevateIndividualCount, target.getElevateIndividualCount());
- target.setElevateIndividualCount(elevateIndividualCount);
- }
-
- if (changed) changesCount.increment();
- });
-
- log.trace("Computing elevated values (pass #{}) [OK] - {} changes", loopCounter, changesCount);
- } while (changesCount.intValue() > 0);
-
- }
-
- protected Double computeIndirectWeight(TempDenormalizedBatchVO batch) {
- // Already computed: skip
- if (batch.getIndirectWeight() != null) return batch.getIndirectWeight();
-
- // Sampling batch
- if (DenormalizedBatches.isSamplingBatch(batch)) {
- try {
- Double samplingWeight = computeSamplingWeightAndRatio(batch, false);
- if (samplingWeight != null) return samplingWeight;
- }
- catch (InvalidSamplingBatchException e) {
- // May be not a sampling batch ? (e.g. a species batch)
- Double indirectWeight = computeSumChildrenWeight(batch);
- if (indirectWeight != null) return indirectWeight;
- throw e;
- }
- // Invalid sampling batch: Continue if not set
- }
-
- // Child batch is a sampling batch
- if (DenormalizedBatches.isParentOfSamplingBatch(batch)) {
- return computeParentSamplingWeight(batch, false);
- }
-
- Double indirectWeight = computeSumChildrenWeight(batch);
- return indirectWeight;
- }
-
-
- protected Double computeSamplingWeightAndRatio(TempDenormalizedBatchVO batch, boolean checkArgument) {
- if (checkArgument)
- Preconditions.checkArgument(DenormalizedBatches.isSamplingBatch(batch));
-
- DenormalizedBatchVO parent = batch.getParent();
- boolean parentExhaustiveInventory = DenormalizedBatches.isExhaustiveInventory(parent);
- Double samplingWeight = null;
- Double samplingRatio = null;
- BigDecimal elevateFactor = null;
-
- if (batch.getSamplingRatio() != null) {
- samplingRatio = batch.getSamplingRatio();
- elevateFactor = new BigDecimal(1).divide(new BigDecimal(samplingRatio), RoundingMode.HALF_UP);
-
- // Try to use the sampling ratio text (more accuracy)
- if (StringUtils.isNotBlank(batch.getSamplingRatioText()) && batch.getSamplingRatioText().contains("/")) {
- String[] parts = batch.getSamplingRatioText().split("/", 2);
- try {
- double d0 = Double.parseDouble(parts[0]);
- double d1 = Double.parseDouble(parts[1]);
- samplingRatio = d0 / d1;
- elevateFactor = new BigDecimal(d1).divide(new BigDecimal(d0), RoundingMode.HALF_UP);
- } catch (Exception e) {
- log.warn("Cannot parse samplingRatioText on batch {id: {}}, label: '{}', saplingRatioText: '{}'} : {}",
- batch.getId(),
- batch.getLabel(),
- batch.getSamplingRatioText(),
- e.getMessage());
- }
- }
- }
- else if (parentExhaustiveInventory && parent.getWeight() != null && batch.getWeight() != null) {
- samplingRatio = batch.getWeight() / parent.getWeight();
- elevateFactor = new BigDecimal(parent.getWeight()).divide(new BigDecimal(batch.getWeight()), RoundingMode.HALF_UP);
- }
-
- else if (parentExhaustiveInventory && parent.getWeight() != null && batch.hasChildren()) {
- samplingWeight = computeSumChildrenWeight(batch);
- if (samplingWeight != null) {
- samplingRatio = samplingWeight / parent.getWeight();
- elevateFactor = new BigDecimal(parent.getWeight()).divide(new BigDecimal(samplingWeight), RoundingMode.HALF_UP);
- }
- }
-
- else if ((!parentExhaustiveInventory || parent.getWeight() == null) && batch.hasChildren()) {
- samplingWeight = computeSumChildrenWeight(batch);
- if (samplingWeight != null) {
- samplingRatio = 1d;
- elevateFactor = new BigDecimal(1);
- }
- }
-
- if (samplingRatio == null || elevateFactor == null) {
- // Use default value (samplingRatio=1) if:
- // - batch has no children
- // - batch is parent of a sampling batch
- if (CollectionUtils.isEmpty(batch.getChildren())) {
- samplingRatio = 1d;
- elevateFactor = new BigDecimal(1);
- }
- else {
- throw new InvalidSamplingBatchException(String.format("Invalid sampling batch {id: %s, label: '%s'}: cannot get or compute the sampling ratio",
- batch.getId(), batch.getLabel()));
- }
- }
-
- // Remember values
- batch.setSamplingRatio(samplingRatio);
- batch.setElevateFactor(elevateFactor);
-
- if (samplingWeight == null) {
- if (batch.getWeight() != null) {
- samplingWeight = batch.getWeight();
- } else if (parentExhaustiveInventory && parent.getWeight() != null) {
- samplingWeight = parent.getWeight() * samplingRatio;
- }
- }
-
- return samplingWeight;
- }
-
-
- protected Double computeParentSamplingWeight(TempDenormalizedBatchVO parent, boolean checkArgument) {
- if (checkArgument) Preconditions.checkArgument(DenormalizedBatches.isParentOfSamplingBatch(parent));
-
- // Use reference weight, if any
- if (parent.getWeight() != null) {
- return parent.getWeight();
- }
-
- TempDenormalizedBatchVO batch = (TempDenormalizedBatchVO)CollectionUtils.extractSingleton(parent.getChildren());
-
- Double parentWeight = null;
- Double samplingRatio = null;
- Double elevateFactor = null;
- if (batch.getSamplingRatio() != null) {
- samplingRatio = batch.getSamplingRatio();
- elevateFactor = 1 / samplingRatio;
-
- // Try to use the sampling ratio text (more accuracy)
- if (StringUtils.isNotBlank(batch.getSamplingRatioText()) && batch.getSamplingRatioText().contains("/")) {
- String[] parts = batch.getSamplingRatioText().split("/", 2);
- try {
- Double shouldBeSamplingWeight = Double.parseDouble(parts[0]);
- Double shouldBeParentWeight = Double.parseDouble(parts[1]);
- // If ratio text use the sampling weight, we have the parent weight
- if (Objects.equals(shouldBeSamplingWeight, batch.getWeight())
- || Objects.equals(shouldBeSamplingWeight, batch.getIndirectWeight())) {
- parentWeight = shouldBeParentWeight;
- }
- samplingRatio = shouldBeSamplingWeight / shouldBeParentWeight;
- elevateFactor = shouldBeParentWeight / shouldBeSamplingWeight;
- } catch (Exception e) {
- log.warn(String.format("Cannot parse samplingRatioText on batch {id: %s, label: '%s', saplingRatioText: '%s'} : %s",
- batch.getId(),
- batch.getLabel(),
- batch.getSamplingRatioText(),
- e.getMessage()));
- }
- }
- }
-
- if (samplingRatio == null)
- throw new SumarisTechnicalException(String.format("Invalid fraction batch {id: %s, label: '%s'}: cannot get or compute the sampling ratio",
- batch.getId(), batch.getLabel()));
-
- if (parentWeight == null) {
- if (batch.getWeight() != null) {
- parentWeight = batch.getWeight() * elevateFactor;
- } else if (batch.getIndirectWeight() != null) {
- parentWeight = batch.getIndirectWeight() * elevateFactor;
- }
- }
-
- return parentWeight;
- }
-
- protected Double computeSumChildrenWeight(DenormalizedBatchVO batch) {
- // Cannot compute children sum, when:
- // - Not exhaustive inventory
- // - No children
- if (!DenormalizedBatches.isExhaustiveInventory(batch)
- || !batch.hasChildren()) {
- return null;
- }
-
- try {
- return Beans.getStream(batch.getChildren())
- .map(child -> (TempDenormalizedBatchVO)child)
- .mapToDouble(child -> {
- if (child.getWeight() != null) return child.getWeight();
- if (child.getIndirectWeight() != null) return child.getIndirectWeight();
- if (child.hasChildren()) {
- Double indirectWeight = computeIndirectWeight(child);
- if (indirectWeight != null) {
- return indirectWeight;
- }
- }
- throw new SumarisTechnicalException(String.format("Cannot compute indirect weight,"
- + " because some child batch has no weight {id: %s, label: '%s'}", child.getId(), child.getLabel()));
- }).sum();
- }
- catch(SumarisTechnicalException e) {
- log.trace(e.getMessage());
- return null;
- }
- }
-
- protected Integer computeIndirectIndividualCount(TempDenormalizedBatchVO batch) {
- // Already computed: skip
- if (batch.getIndirectIndividualCount() != null) return batch.getIndirectIndividualCount();
-
- // Cannot compute when:
- // - Not exhaustive inventory
- // - No children
- if (!DenormalizedBatches.isExhaustiveInventory(batch)
- || !batch.hasChildren()) {
- return null;
- }
-
- try {
- return Beans.getStream(batch.getChildren())
- .map(child -> (TempDenormalizedBatchVO) child)
- .mapToInt(child -> {
- if (child.getIndividualCount() != null) return child.getIndividualCount();
- if (child.hasChildren()) {
- Integer indirectIndividualCount = computeIndirectIndividualCount(child);
- if (indirectIndividualCount != null) {
- return indirectIndividualCount;
- }
- }
- throw new SumarisTechnicalException(String.format("Cannot compute indirect individual count,"
- + " because some child batch has no individual count {id: %s, label: '%s'}", child.getId(), child.getLabel()));
- }).sum();
- } catch (SumarisTechnicalException e) {
- log.trace(e.getMessage());
- return null;
- }
- }
-
- protected void computeTreeIndent(DenormalizedBatchVO target) {
- computeTreeIndent(target, "", true);
- }
-
- protected void computeTreeIndent(DenormalizedBatchVO target, String inheritedTreeIndent, boolean isLast) {
- if (target.getParent() == null) {
- target.setTreeIndent("-");
- } else {
- target.setTreeIndent(inheritedTreeIndent + (isLast? "|_" : "|-"));
- }
-
- List children = target.getChildren();
- if (CollectionUtils.isNotEmpty(children)) {
- String childrenTreeIndent = inheritedTreeIndent + (isLast? " " : "| ");
- for (int i = 0; i < children.size(); i++) {
- computeTreeIndent(children.get(i), childrenTreeIndent, i == children.size() - 1);
- }
- }
- }
-
- protected TempDenormalizedBatchVO createTempVO(BatchVO source) {
- TempDenormalizedBatchVO target = new TempDenormalizedBatchVO();
- denormalizedBatchRepository.copy(source, target, true);
- return target;
- }
-}
diff --git a/sumaris-core/src/main/java/net/sumaris/core/service/data/OperationService.java b/sumaris-core/src/main/java/net/sumaris/core/service/data/OperationService.java
index eb470a10f7..066d71a4e8 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/service/data/OperationService.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/service/data/OperationService.java
@@ -23,6 +23,7 @@
*/
+import lombok.NonNull;
import net.sumaris.core.dao.technical.SortDirection;
import net.sumaris.core.vo.data.OperationFetchOptions;
import net.sumaris.core.vo.data.OperationVO;
@@ -47,6 +48,9 @@ public interface OperationService {
@Transactional(readOnly = true)
List findAllByTripId(int tripId, int offset, int size, String sortAttribute, SortDirection sortDirection, OperationFetchOptions fetchOptions);
+ @Transactional(readOnly = true)
+ List findAllByFilter(@NonNull OperationFilterVO filter, @NonNull OperationFetchOptions fetchOptions);
+
@Transactional(readOnly = true)
List findAllByFilter(OperationFilterVO filter, int offset, int size, String sortAttribute, SortDirection sortDirection, OperationFetchOptions fetchOptions);
diff --git a/sumaris-core/src/main/java/net/sumaris/core/service/data/OperationServiceImpl.java b/sumaris-core/src/main/java/net/sumaris/core/service/data/OperationServiceImpl.java
index e2b10ade6b..aca7c28f31 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/service/data/OperationServiceImpl.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/service/data/OperationServiceImpl.java
@@ -99,9 +99,15 @@ public List findAllByTripId(int tripId, @NonNull OperationFetchOpti
public List findAllByTripId(int tripId,
int offset, int size, String sortAttribute, SortDirection sortDirection,
@NonNull OperationFetchOptions fetchOptions) {
- return operationRepository.findAllVO(operationRepository.hasTripId(tripId),
- Pageables.create(offset, size, sortAttribute, sortDirection),
- fetchOptions).getContent();
+ return operationRepository.findAll(OperationFilterVO.builder().tripId(tripId).build(),
+ offset, size, sortAttribute, sortDirection,
+ fetchOptions);
+ }
+
+ @Override
+ public List findAllByFilter(@NonNull OperationFilterVO filter,
+ @NonNull OperationFetchOptions fetchOptions) {
+ return operationRepository.findAll(filter, fetchOptions);
}
@Override
diff --git a/sumaris-core/src/main/java/net/sumaris/core/service/data/PacketServiceImpl.java b/sumaris-core/src/main/java/net/sumaris/core/service/data/PacketServiceImpl.java
index b0f1a56bd6..cf24352f6a 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/service/data/PacketServiceImpl.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/service/data/PacketServiceImpl.java
@@ -33,6 +33,7 @@
import net.sumaris.core.event.config.ConfigurationReadyEvent;
import net.sumaris.core.event.config.ConfigurationUpdatedEvent;
import net.sumaris.core.exception.SumarisTechnicalException;
+import net.sumaris.core.model.annotation.EntityEnums;
import net.sumaris.core.model.data.BatchQuantificationMeasurement;
import net.sumaris.core.model.data.BatchSortingMeasurement;
import net.sumaris.core.model.data.IMeasurementEntity;
@@ -82,6 +83,19 @@ public void onConfigurationReady(ConfigurationEvent event) {
this.measuredWeightPmfmId = PmfmEnum.BATCH_MEASURED_WEIGHT.getId();
this.estimatedRatioPmfmId = PmfmEnum.BATCH_ESTIMATED_RATIO.getId();
this.sortingPmfmId = PmfmEnum.BATCH_SORTING.getId();
+
+ try {
+
+ EntityEnums.checkResolved(
+ // Check pmfm enums
+ PmfmEnum.BATCH_CALCULATED_WEIGHT, PmfmEnum.BATCH_MEASURED_WEIGHT, PmfmEnum.BATCH_ESTIMATED_RATIO, PmfmEnum.BATCH_SORTING,
+ // Check QV enums
+ QualitativeValueEnum.SORTING_BULK
+ );
+ }
+ catch (Exception e) {
+ log.error(e.getMessage());
+ }
}
@Override
diff --git a/sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizeOperationServiceImpl.java b/sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizeOperationServiceImpl.java
new file mode 100644
index 0000000000..c46c797070
--- /dev/null
+++ b/sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizeOperationServiceImpl.java
@@ -0,0 +1,222 @@
+package net.sumaris.core.service.data.denormalize;
+
+/*-
+ * #%L
+ * SUMARiS:: Core
+ * %%
+ * Copyright (C) 2018 SUMARiS Consortium
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program. If not, see
+ * .
+ * #L%
+ */
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.Lists;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import net.sumaris.core.dao.data.Positions;
+import net.sumaris.core.dao.technical.SortDirection;
+import net.sumaris.core.exception.SumarisBusinessException;
+import net.sumaris.core.service.data.OperationService;
+import net.sumaris.core.service.referential.LocationService;
+import net.sumaris.core.util.Beans;
+import net.sumaris.core.util.Dates;
+import net.sumaris.core.vo.data.*;
+import net.sumaris.core.vo.data.batch.DenormalizedBatchOptions;
+import net.sumaris.core.vo.filter.OperationFilterVO;
+import net.sumaris.core.vo.referential.LocationVO;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.mutable.MutableInt;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+@Service("denormalizeOperationService")
+@RequiredArgsConstructor
+@Slf4j
+public class DenormalizeOperationServiceImpl implements DenormalizedOperationService {
+
+ private final DenormalizedBatchService denormalizedBatchService;
+
+ private final OperationService operationService;
+
+ private final LocationService locationService;
+
+ // Create a cache for denormalized options, by programId
+ private LoadingCache optionsByProgramIdCache;
+
+ private LoadingCache optionsByProgramLabelCache;
+
+ @PostConstruct
+ protected void init() {
+ // Create a cache for denormalized options, by programId
+ optionsByProgramIdCache = CacheBuilder.newBuilder()
+ .expireAfterWrite(5, TimeUnit.MINUTES) // 5 min (if job is very long, the options will be reload)
+ .build(CacheLoader.from(denormalizedBatchService::createOptionsByProgramId));
+
+ // Create a cache for denormalized options, by programLabel
+ optionsByProgramLabelCache = CacheBuilder.newBuilder()
+ .expireAfterWrite(5, TimeUnit.MINUTES) // 5 min (if job is very long, the options will be reload)
+ .build(CacheLoader.from(denormalizedBatchService::createOptionsByProgramLabel));
+
+ }
+ @Override
+ public DenormalizedBatchOptions createOptionsByProgramId(int programId) {
+ return optionsByProgramIdCache.getUnchecked(programId);
+ }
+ @Override
+ public DenormalizedBatchOptions createOptionsByProgramLabel(String programLabel) {
+ return optionsByProgramLabelCache.getUnchecked(programLabel);
+ }
+
+ @Override
+ public DenormalizedTripResultVO denormalizeByFilter(@NonNull OperationFilterVO operationFilter,
+ @NonNull DenormalizedBatchOptions baseOptions) {
+ long startTime = System.currentTimeMillis();
+
+ operationFilter = operationFilter.clone();
+
+ // Make sure to exclude parent operation, because should not have batches
+ // (see "filage" operation in ACOST program)
+ operationFilter.setHasNoChildOperation(true);
+
+ // Select only operation that should be update (if not force)
+ operationFilter.setNeedBatchDenormalization(!baseOptions.isForce());
+
+ long operationTotal = operationService.countByFilter(operationFilter);
+
+ boolean hasMoreData;
+ int offset = 0;
+ int pageSize = 10;
+ int operationCount = 0;
+ MutableInt batchesCount = new MutableInt(0);
+ MutableInt errorCount = new MutableInt(0);
+ List messages = Lists.newArrayList();
+
+ if (operationTotal > 0) {
+ do {
+ // Fetch some operations
+ List operations = operationService.findAllByFilter(operationFilter,
+ offset, pageSize, // Page
+ OperationVO.Fields.ID, SortDirection.ASC, // Sort by id, to keep continuity between pages
+ OperationFetchOptions.builder()
+ .withChildrenEntities(false)
+ .withMeasurementValues(false)
+ // Fetch position and fishing area, to be able to compute fishing area id, need by conversion
+ .withPositions(true)
+ .withFishingAreas(true)
+ .build());
+
+ operations.forEach(operation -> {
+ try {
+ // Prepare options (add fishing area, date, etc.)
+ DenormalizedBatchOptions options = createOptionsByOperation(operation, baseOptions);
+
+ List> batches = denormalizedBatchService.denormalizeAndSaveByOperationId(operation.getId(), options);
+ batchesCount.add(CollectionUtils.size(batches));
+ } catch (SumarisBusinessException be) {
+ log.error(be.getMessage());
+ messages.add(be.getMessage());
+ errorCount.increment();
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ messages.add(e.getMessage());
+ errorCount.increment();
+ }
+ });
+
+ offset += pageSize;
+ operationCount += operations.size();
+ hasMoreData = operations.size() >= pageSize;
+ if (operationCount > operationTotal) {
+ operationTotal = operationCount;
+ }
+ } while (hasMoreData);
+ }
+
+ return DenormalizedTripResultVO.builder()
+ .operationCount(operationCount)
+ .batchCount(batchesCount.intValue())
+ .invalidBatchCount(errorCount.intValue())
+ .message(CollectionUtils.isNotEmpty(messages) ? String.join("\n", messages) : null)
+ .executionTime(System.currentTimeMillis() - startTime)
+ .build();
+ }
+
+ public DenormalizedBatchOptions createOptionsByOperation(@NonNull OperationVO operation,
+ @Nullable DenormalizedBatchOptions inheritedOptions) {
+
+ if (inheritedOptions == null) {
+ int programId = operationService.getProgramIdById(operation.getId());
+ inheritedOptions = denormalizedBatchService.createOptionsByProgramId(programId);
+ }
+
+ Optional fishingAreaLocationIds = getOperationFishingAreaIds(operation);
+ if (fishingAreaLocationIds.isEmpty()) {
+ log.warn("Cannot found the statistical rectangle for Operation #{}, neither in positions nor in fishing areas", operation.getId());
+ }
+
+ DenormalizedBatchOptions options = inheritedOptions.clone(); // Copy, to keep original options unchanged
+ options.setFishingAreaLocationIds(fishingAreaLocationIds.orElse(null));
+ options.setDateTime(Dates.resetTime(getFishingStartDateTime(operation)));
+
+ return options;
+ }
+
+ public Optional getOperationFishingAreaIds(@NonNull OperationVO operation) {
+ // Get the fishing area: first, search from last position
+ Integer[] result = Beans.getStream(operation.getPositions())
+ .filter(Positions::isNotNullAndValid)
+ .map(position -> locationService.getStatisticalRectangleIdByLatLong(position.getLatitude(), position.getLongitude()))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .distinct()
+ .toArray(Integer[]::new);
+ if (ArrayUtils.isNotEmpty(result)) return Optional.of(result);
+
+ // Try to get location from fishing areas
+ result = Beans.getStream(operation.getFishingAreas())
+ .filter(fa -> fa.getLocation() != null)
+ .map(FishingAreaVO::getLocation)
+ .map(LocationVO::getId)
+ .filter(Objects::nonNull)
+ .distinct()
+ .toArray(Integer[]::new);
+ if (ArrayUtils.isNotEmpty(result)) return Optional.of(result);
+
+ return Optional.empty(); // Not found
+ }
+
+ /**
+ * Get the start fishing date (or the start date if no found)
+ * @param operation
+ * @return
+ */
+ public Date getFishingStartDateTime(@NonNull OperationVO operation) {
+ return operation.getFishingStartDateTime() != null
+ ? operation.getFishingStartDateTime()
+ : operation.getStartDateTime();
+ }
+}
diff --git a/sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizeTripServiceImpl.java b/sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizeTripServiceImpl.java
deleted file mode 100644
index 4afbf1e080..0000000000
--- a/sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizeTripServiceImpl.java
+++ /dev/null
@@ -1,201 +0,0 @@
-package net.sumaris.core.service.data.denormalize;
-
-/*-
- * #%L
- * SUMARiS:: Core
- * %%
- * Copyright (C) 2018 SUMARiS Consortium
- * %%
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public
- * License along with this program. If not, see
- * .
- * #L%
- */
-
-import lombok.NonNull;
-import lombok.extern.slf4j.Slf4j;
-import net.sumaris.core.config.SumarisConfiguration;
-import net.sumaris.core.dao.technical.SortDirection;
-import net.sumaris.core.exception.SumarisBusinessException;
-import net.sumaris.core.model.IProgressionModel;
-import net.sumaris.core.model.ProgressionModel;
-import net.sumaris.core.service.data.DenormalizedBatchService;
-import net.sumaris.core.service.data.OperationService;
-import net.sumaris.core.service.data.TripService;
-import net.sumaris.core.util.TimeUtils;
-import net.sumaris.core.vo.data.*;
-import net.sumaris.core.vo.data.batch.DenormalizedBatchOptions;
-import net.sumaris.core.vo.filter.TripFilterVO;
-import org.apache.commons.collections4.CollectionUtils;
-import org.apache.commons.lang3.mutable.MutableInt;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.core.task.TaskExecutor;
-import org.springframework.stereotype.Service;
-
-import java.util.List;
-
-@Service("denormalizeTripService")
-@Slf4j
-public class DenormalizeTripServiceImpl implements DenormalizeTripService {
-
- @Autowired
- private SumarisConfiguration configuration;
-
- @Autowired
- private TripService tripService;
-
- @Autowired
- private DenormalizedBatchService denormalizedBatchService;
-
- @Autowired
- private OperationService operationService;
-
- @Autowired(required = false)
- private TaskExecutor taskExecutor;
-
- @Override
- public DenormalizeTripResultVO denormalizeByFilter(@NonNull TripFilterVO filter) {
- return denormalizeByFilter(filter, new ProgressionModel());
- }
-
- @Override
- public DenormalizeTripResultVO denormalizeByFilter(@NonNull TripFilterVO filter, @NonNull IProgressionModel progression) {
- long startTime = System.currentTimeMillis();
-
- progression.setCurrent(0);
- progression.setMessage(String.format("Starting trips denormalization... filter: %s", filter));
- log.debug(progression.getMessage());
-
- TripFetchOptions tripFetchOptions = TripFetchOptions.builder()
- .withChildrenEntities(false)
- .withMeasurementValues(false)
- .withRecorderPerson(false)
- .build();
-
- long tripTotal = tripService.countByFilter(filter);
- progression.setTotal(tripTotal);
-
- boolean hasMoreData;
- int offset = 0;
- int pageSize = 10;
- int tripCount = 0;
- MutableInt operationCount = new MutableInt(0);
- MutableInt batchCount = new MutableInt(0);
- MutableInt invalidBatchCount = new MutableInt(0);
- do {
- // Fetch some trips
- List trips = tripService.findAll(filter,
- offset, pageSize, // Page
- TripVO.Fields.ID, SortDirection.ASC, // Sort by id, to keep continuity between pages
- tripFetchOptions);
-
- if (offset > 0 && offset % (pageSize * 2) == 0) {
- progression.setCurrent(offset);
- progression.setMessage(String.format("Processing trips denormalization... %s/%s", offset, tripTotal));
- log.debug(progression.getMessage());
- }
-
- // Denormalize each trip
- trips.stream()
- .map(TripVO::getId)
- .map(this::denormalizeById)
- .forEach(result -> {
- operationCount.add(result.getOperationCount());
- batchCount.add(result.getBatchCount());
- invalidBatchCount.add(result.getInvalidBatchCount());
- });
-
- offset += pageSize;
- tripCount += trips.size();
- hasMoreData = trips.size() >= pageSize;
- if (tripCount > tripTotal) {
- tripTotal = tripCount;
- progression.adaptTotal(tripTotal);
- }
- } while (hasMoreData);
-
- // Success log
- progression.setCurrent(tripCount);
- progression.setMessage(String.format("Trips denormalization finished, in %s - %s trips, %s operations, %s batches - %s invalid batch tree (skipped)",
- TimeUtils.printDurationFrom(startTime),
- tripCount,
- operationCount,
- batchCount,
- invalidBatchCount));
- log.debug(progression.getMessage());
-
- return DenormalizeTripResultVO.builder()
- .tripCount(tripCount)
- .operationCount(operationCount.intValue())
- .batchCount(batchCount.intValue())
- .invalidBatchCount(invalidBatchCount.intValue())
- .executionTime(System.currentTimeMillis() - startTime)
- .build();
- }
-
- @Override
- public DenormalizeTripResultVO denormalizeById(int tripId) {
- long startTime = System.currentTimeMillis();
- long operationTotal = operationService.countByTripId(tripId);
-
- // Load denormalized options
- int programId = tripService.getProgramIdById(tripId);
- DenormalizedBatchOptions options = denormalizedBatchService.createOptionsByProgramId(programId);
-
- boolean hasMoreData;
- int offset = 0;
- int pageSize = 10;
- int operationCount = 0;
- MutableInt batchesCount = new MutableInt(0);
- MutableInt errorCount = new MutableInt(0);
- do {
- // Fetch some operations
- List operations = operationService.findAllByTripId(tripId,
- offset, pageSize, // Page
- OperationVO.Fields.ID, SortDirection.ASC, // Sort by id, to keep continuity between pages
- OperationFetchOptions.builder()
- .withChildrenEntities(false)
- .withMeasurementValues(false)
- .build());
-
- operations.forEach(operation -> {
- try {
- List> batches = denormalizedBatchService.denormalizeAndSaveByOperationId(operation.getId(), options);
- batchesCount.add(CollectionUtils.size(batches));
- } catch (SumarisBusinessException be) {
- log.error(be.getMessage());
- errorCount.increment();
- }
- catch (Exception e) {
- log.error(e.getMessage(), e);
- errorCount.increment();
- }
- });
-
- offset += pageSize;
- operationCount += operations.size();
- hasMoreData = operations.size() >= pageSize;
- if (operationCount > operationTotal) {
- operationTotal = operationCount;
- }
- } while (hasMoreData);
-
- return DenormalizeTripResultVO.builder()
- .tripCount(1)
- .operationCount(operationCount)
- .batchCount(batchesCount.intValue())
- .invalidBatchCount(errorCount.intValue())
- .executionTime(System.currentTimeMillis() - startTime)
- .build();
- }
-}
diff --git a/sumaris-core/src/main/java/net/sumaris/core/service/data/DenormalizedBatchService.java b/sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizedBatchService.java
similarity index 85%
rename from sumaris-core/src/main/java/net/sumaris/core/service/data/DenormalizedBatchService.java
rename to sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizedBatchService.java
index d52f779ddc..08979f7491 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/service/data/DenormalizedBatchService.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizedBatchService.java
@@ -1,27 +1,26 @@
-package net.sumaris.core.service.data;
-
-/*-
+/*
* #%L
- * SUMARiS:: Core
+ * SUMARiS
* %%
- * Copyright (C) 2018 SUMARiS Consortium
+ * Copyright (C) 2019 SUMARiS Consortium
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
- *
+ *
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
- *
+ *
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* .
* #L%
*/
+package net.sumaris.core.service.data.denormalize;
import net.sumaris.core.vo.data.batch.BatchVO;
import net.sumaris.core.vo.data.batch.DenormalizedBatchOptions;
@@ -46,5 +45,9 @@ public interface DenormalizedBatchService {
List denormalizeAndSaveBySaleId(int saleId, @Nullable DenormalizedBatchOptions options);
+ @Transactional(readOnly = true)
DenormalizedBatchOptions createOptionsByProgramId(int programId);
+
+ @Transactional(readOnly = true)
+ DenormalizedBatchOptions createOptionsByProgramLabel(String programLabel);
}
diff --git a/sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizedBatchServiceImpl.java b/sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizedBatchServiceImpl.java
new file mode 100644
index 0000000000..90ac8d729a
--- /dev/null
+++ b/sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizedBatchServiceImpl.java
@@ -0,0 +1,1348 @@
+/*
+ * #%L
+ * SUMARiS
+ * %%
+ * Copyright (C) 2019 SUMARiS Consortium
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program. If not, see
+ * .
+ * #L%
+ */
+
+package net.sumaris.core.service.data.denormalize;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import net.sumaris.core.dao.data.batch.BatchRepository;
+import net.sumaris.core.dao.data.batch.DenormalizedBatchRepository;
+import net.sumaris.core.dao.data.batch.InvalidSamplingBatchException;
+import net.sumaris.core.event.config.ConfigurationEvent;
+import net.sumaris.core.event.config.ConfigurationReadyEvent;
+import net.sumaris.core.event.config.ConfigurationUpdatedEvent;
+import net.sumaris.core.exception.SumarisTechnicalException;
+import net.sumaris.core.model.IEntity;
+import net.sumaris.core.model.TreeNodeEntities;
+import net.sumaris.core.model.administration.programStrategy.ProgramPropertyEnum;
+import net.sumaris.core.model.annotation.EntityEnums;
+import net.sumaris.core.model.referential.QualityFlags;
+import net.sumaris.core.model.referential.StatusEnum;
+import net.sumaris.core.model.referential.location.LocationLevels;
+import net.sumaris.core.model.referential.pmfm.MethodEnum;
+import net.sumaris.core.model.referential.pmfm.ParameterEnum;
+import net.sumaris.core.model.referential.pmfm.QualitativeValueEnum;
+import net.sumaris.core.model.referential.pmfm.UnitEnum;
+import net.sumaris.core.model.referential.taxon.TaxonGroupTypeEnum;
+import net.sumaris.core.service.administration.programStrategy.ProgramService;
+import net.sumaris.core.service.data.OperationService;
+import net.sumaris.core.service.data.SaleService;
+import net.sumaris.core.service.referential.conversion.RoundWeightConversionService;
+import net.sumaris.core.service.referential.conversion.WeightLengthConversionService;
+import net.sumaris.core.service.referential.taxon.TaxonGroupService;
+import net.sumaris.core.util.Beans;
+import net.sumaris.core.util.Numbers;
+import net.sumaris.core.util.StringUtils;
+import net.sumaris.core.util.TimeUtils;
+import net.sumaris.core.vo.administration.programStrategy.ProgramFetchOptions;
+import net.sumaris.core.vo.administration.programStrategy.ProgramVO;
+import net.sumaris.core.vo.administration.programStrategy.Programs;
+import net.sumaris.core.vo.data.batch.*;
+import net.sumaris.core.vo.filter.ReferentialFilterVO;
+import net.sumaris.core.vo.referential.TaxonGroupVO;
+import net.sumaris.core.vo.referential.conversion.RoundWeightConversionFilterVO;
+import net.sumaris.core.vo.referential.conversion.RoundWeightConversionVO;
+import net.sumaris.core.vo.referential.conversion.WeightLengthConversionFilterVO;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.mutable.MutableDouble;
+import org.apache.commons.lang3.mutable.MutableInt;
+import org.apache.commons.lang3.mutable.MutableShort;
+import org.nuiton.i18n.I18n;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Nullable;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.*;
+import java.util.function.Function;
+
+@Service("denormalizedBatchService")
+@RequiredArgsConstructor
+@Slf4j
+public class DenormalizedBatchServiceImpl implements DenormalizedBatchService {
+
+ public static final int INTERMEDIATE_DECIMAL_SCALE = 24; // intermediate scale can be maximized
+ public static final int WEIGHT_DECIMAL_SCALE = 6; // grams precision
+
+ protected final DenormalizedBatchRepository denormalizedBatchRepository;
+
+ protected final BatchRepository batchRepository;
+
+ protected final ProgramService programService;
+
+ protected final OperationService operationService;
+
+ protected final SaleService saleService;
+
+ protected final TaxonGroupService taxonGroupService;
+
+ protected final WeightLengthConversionService weightLengthConversionService;
+
+ protected final RoundWeightConversionService roundWeightConversionService;
+
+ private boolean canEnableRtpWeight = true;
+
+ @EventListener({ConfigurationReadyEvent.class, ConfigurationUpdatedEvent.class})
+ public void onConfigurationReady(ConfigurationEvent event) {
+ // Check useful enumerations
+ try {
+ checkBaseEnumerations();
+ } catch (Exception e) {
+ log.warn(e.getMessage());
+ }
+
+ // Check is can enable RTP
+ {
+ boolean enableRtp = true;
+ List errorMessages = Lists.newArrayList();
+
+ // Check enumerations used for RTP
+ try {
+ checkRtpEnumerations();
+ } catch (Exception e) {
+ enableRtp = false;
+ errorMessages.add(e.getMessage());
+ }
+
+ // Check statistical rectangle level ids
+ if (Beans.getStream(LocationLevels.getStatisticalRectangleLevelIds()).anyMatch(id -> id < 0)) {
+ enableRtp = false;
+ errorMessages.add(I18n.t("sumaris.error.missingSomeRectangleLocationLevel"));
+
+ }
+ if (this.canEnableRtpWeight != enableRtp) {
+ this.canEnableRtpWeight = enableRtp;
+ if (!enableRtp) log.warn(I18n.t("sumaris.error.denormalization.batch.cannotEnableRtpWeight", "\n" + Joiner.on("\n\t- ").join(errorMessages)));
+ }
+ }
+ }
+
+ @Override
+ public List denormalize(@NonNull BatchVO catchBatch, @NonNull final DenormalizedBatchOptions options) {
+ if (options.isEnableRtpWeight()) {
+ Preconditions.checkArgument(canEnableRtpWeight, I18n.t("sumaris.error.denormalization.batch.cannotEnableRtpWeight", "See startup error"));
+
+ // Check options
+ Preconditions.checkNotNull(options.getDateTime(), "Required options.dateTime when RTP weight enabled");
+ Preconditions.checkNotNull(options.getRoundWeightCountryLocationId(), "Required options.roundWeightCountryLocationId when RTP weight enabled");
+ Preconditions.checkNotNull(options.getDefaultLandingDressingId(), "Required options.defaultLandingDressingId when RTP weight enabled");
+ Preconditions.checkNotNull(options.getDefaultDiscardDressingId(), "Required options.defaultDiscardDressingId when RTP weight enabled");
+ Preconditions.checkNotNull(options.getDefaultLandingPreservationId(), "Required options.defaultLandingPreservationId when RTP weight enabled");
+ Preconditions.checkNotNull(options.getDefaultDiscardDressingId(), "Required options.defaultDiscardDressingId when RTP weight enabled");
+
+ }
+
+ long startTime = System.currentTimeMillis();
+ final MutableShort flatRankOrder = new MutableShort(0);
+
+ List result = TreeNodeEntities.streamAllAndMap(catchBatch, (source, p) -> {
+ TempDenormalizedBatchVO target = createTempVO(source);
+ TempDenormalizedBatchVO parent = p != null ? (TempDenormalizedBatchVO)p : null;
+ boolean isLeaf = source.isLeaf(); // Do not use 'target', because children are added later
+
+ // Add to parent's children
+ if (parent != null) parent.addChildren(target);
+
+ // Depth level
+ if (parent == null) {
+ target.setTreeLevel((short) 1); // First level
+ if (target.getIsLanding() == null) target.setIsLanding(false);
+ if (target.getIsDiscard() == null) target.setIsDiscard(false);
+ } else {
+ target.setTreeLevel((short) (parent.getTreeLevel() + 1));
+ // Inherit taxon group
+ if (target.getInheritedTaxonGroup() == null && parent.getInheritedTaxonGroup() != null) {
+ target.setInheritedTaxonGroup(parent.getInheritedTaxonGroup());
+ }
+ // Inherit taxon name
+ if (target.getInheritedTaxonName() == null && parent.getInheritedTaxonName() != null) {
+ target.setInheritedTaxonName(parent.getInheritedTaxonName());
+ }
+ // Exhaustive inventory
+ if (target.getExhaustiveInventory() == null) {
+ // Always true, when:
+ // - taxon name is defined
+ // - taxon group is defined and taxon Name disable (in options)
+ if (target.getInheritedTaxonName() != null) {
+ target.setExhaustiveInventory(Boolean.TRUE);
+ } else if (target.getInheritedTaxonGroup() != null && !options.isEnableTaxonName()) {
+ target.setExhaustiveInventory(Boolean.TRUE);
+ } else if (parent.getExhaustiveInventory() != null) {
+ target.setExhaustiveInventory(parent.getExhaustiveInventory());
+ }
+ }
+ // Inherit location
+ if (parent.getLocationId() != null) {
+ target.setLocationId(parent.getLocationId());
+ }
+ // Inherit landing / discard
+ if (target.getIsLanding() == null) {
+ target.setIsLanding(parent.getIsLanding());
+ }
+ if (target.getIsDiscard() == null) {
+ target.setIsDiscard(parent.getIsDiscard());
+ }
+
+ // Inherit quality flag
+ if (parent.getQualityFlagId() != null) {
+ if (target.getQualityFlagId() == null) {
+ target.setQualityFlagId(parent.getQualityFlagId());
+ }
+ // Keep the worst value, if current has a value
+ else {
+ target.setQualityFlagId(QualityFlags.worst(parent.getQualityFlagId(), target.getQualityFlagId()));
+ }
+ }
+
+ // If current quality is invalid
+ if (QualityFlags.isInvalid(target.getQualityFlagId())) {
+ // Force both parent and current parent exhaustive inventory to FALSE
+ // NOTE Allegro:
+ // mantis Allegro #12951 - remontée des poids selon le niveau de qualité
+ // Si un des lots fils (direct ou indirect) est invalide
+ // (c'est à dire si le code du niveau de qualité appartient à la liste des niveaux invalides)
+ // alors il faut considérer que l'inventaire exhaustif est non.
+ // Le but est de stopper la remontée des poids calculés
+ // s'il y a au moins un lot invalide parmi les fils, isExhaustive = false
+ parent.setExhaustiveInventory(Boolean.FALSE);
+ target.setExhaustiveInventory(Boolean.FALSE);
+ }
+
+ // Inherit sorting values
+ Beans.getStream(parent.getSortingValues())
+ .forEach(svSource -> {
+ // Make sure sorting value not already exists
+ Beans.getStream(target.getSortingValues())
+ .filter(svTarget -> Objects.equals(svTarget.getPmfmId(), svSource.getPmfmId()))
+ .findFirst()
+ .ifPresentOrElse(svTarget -> {
+ Beans.copyProperties(svSource, svTarget, IEntity.Fields.ID);
+ svTarget.setIsInherited(true);
+ svTarget.setRankOrder(svSource.getRankOrder() / 10);
+ },
+ () -> {
+ DenormalizedBatchSortingValueVO svTarget = new DenormalizedBatchSortingValueVO();
+ Beans.copyProperties(svSource, svTarget, IEntity.Fields.ID);
+ svTarget.setIsInherited(true);
+ svTarget.setRankOrder(svSource.getRankOrder() / 10);
+ target.addSortingValue(svTarget);
+ });
+ });
+
+ // Compute Alive weight, on leaf batch
+ // (to be able to compute indirect alive weight later)
+ if (isLeaf) {
+ computeAliveWeightFactor(target, options, true)
+ .ifPresent(target::setAliveWeightFactor);
+ }
+
+ }
+
+ return target;
+ })
+
+ // Sort
+ .sorted(Comparator.comparing(DenormalizedBatches::computeFlatOrder))
+
+ .map(target -> {
+ // Compute flat rank order
+ flatRankOrder.increment();
+ target.setFlatRankOrder(flatRankOrder.getValue());
+
+ // Compute tree indent (run once, on the root batch)
+ if (target.getParent() == null) computeTreeIndent(target);
+
+ return target;
+ })
+ .toList();
+
+ // If only the catch batch
+ if (CollectionUtils.size(result) == 1) {
+ DenormalizedBatchVO target = result.get(0);
+ target.setElevateWeight(target.getWeight());
+ } else {
+
+ // Compute RTP weight from length (if enabled)
+ if (options.isEnableRtpWeight()) {
+ computeRtpWeights(result, options);
+ }
+
+ // Compute indirect values
+ computeIndirectValues(result, options);
+
+ // compute elevate factors
+ computeElevateFactor(result, options);
+
+ // Elevate weight
+ computeElevatedValues(result, options);
+
+ // Indirect elevated values
+ computeIndirectElevatedValues(result, options);
+
+ }
+
+ // Log
+ if (log.isDebugEnabled()) {
+ log.debug("Batches denormalization succeed, in {}:\n{}",
+ TimeUtils.printDurationFrom(startTime),
+ DenormalizedBatches.dumpAsString(result, true, true));
+ //log.debug("Batches denormalization succeed, in {}", TimeUtils.printDurationFrom(startTime));
+ }
+
+ return result;
+ }
+
+ @Override
+ public List denormalizeAndSaveByOperationId(int operationId, @Nullable DenormalizedBatchOptions options) {
+ BatchVO catchBatch = batchRepository.getCatchBatchByOperationId(operationId, BatchFetchOptions.builder()
+ .withChildrenEntities(true)
+ .withMeasurementValues(true)
+ .withRecorderDepartment(false)
+ .build());
+ if (catchBatch == null) return null;
+
+ long startTime = System.currentTimeMillis();
+ log.debug("Batches denormalization of operation {id: {}}...", operationId);
+
+ // Compute options, for the operation's program
+ if (options == null) {
+ int programId = operationService.getProgramIdById(operationId);
+ options = createOptionsByProgramId(programId);
+ }
+
+ // Denormalize batches
+ List batches = denormalize(catchBatch, options);
+
+ // Save denormalized batches
+ batches = denormalizedBatchRepository.saveAllByOperationId(operationId, batches);
+
+ log.debug("Batches denormalization of operation {id: {}} [OK] in {}", operationId, TimeUtils.printDurationFrom(startTime));
+ return batches;
+ }
+
+ @Override
+ public List denormalizeAndSaveBySaleId(int saleId, @Nullable DenormalizedBatchOptions options) {
+ BatchVO catchBatch = batchRepository.getCatchBatchBySaleId(saleId, BatchFetchOptions.builder()
+ .withChildrenEntities(true)
+ .withMeasurementValues(true)
+ .withRecorderDepartment(false)
+ .build());
+ if (catchBatch == null) return null;
+
+ long startTime = System.currentTimeMillis();
+ log.debug("Denormalize batches of sale {id: {}}...", saleId);
+
+ // Compute options, for the sale's program
+ if (options == null) {
+ int programId = saleService.getProgramIdById(saleId);
+ options = createOptionsByProgramId(programId);
+ }
+
+ // Denormalize batches
+ List denormalizedBatches = denormalize(catchBatch, options);
+
+ // Save denormalized batches
+ List result = denormalizedBatchRepository.saveAllBySaleId(saleId, denormalizedBatches);
+
+ log.debug("Denormalize batches of sale {id: {}} [OK] in {}", saleId, TimeUtils.printDurationFrom(startTime));
+ return result;
+ }
+
+ @Override
+ public DenormalizedBatchOptions createOptionsByProgramId(int programId) {
+
+ ProgramVO program = programService.get(programId, ProgramFetchOptions.builder()
+ .withProperties(true)
+ .withLocations(false)
+ .withStrategies(false)
+ .build());
+
+ return createOptionsByProgram(program);
+ }
+
+ @Override
+ public DenormalizedBatchOptions createOptionsByProgramLabel(String programLabel) {
+
+ ProgramVO program = programService.getByLabel(programLabel, ProgramFetchOptions.builder()
+ .withProperties(true)
+ .withLocations(false)
+ .withStrategies(false)
+ .build());
+
+ return createOptionsByProgram(program);
+ }
+
+ /* -- protected methods -- */
+
+ protected DenormalizedBatchOptions createOptionsByProgram(@NonNull ProgramVO program) {
+ Preconditions.checkNotNull(program.getProperties());
+
+ // Get ids of taxon group without weight
+ String taxonGroupsNoWeight = Optional.ofNullable(Programs.getProperty(program, ProgramPropertyEnum.TRIP_BATCH_TAXON_GROUPS_NO_WEIGHT)).orElse("");
+ Integer[] taxonGroupIdsNoWeight = Arrays.stream(taxonGroupsNoWeight.split(","))
+ .map(String::trim)
+ .map(label -> taxonGroupService.findAllByFilter(ReferentialFilterVO.builder()
+ .label(label)
+ .levelIds(new Integer[]{TaxonGroupTypeEnum.FAO.getId()})
+ .statusIds(new Integer[]{StatusEnum.ENABLE.getId()})
+ .build()).stream().findFirst())
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .map(TaxonGroupVO::getId)
+ .toArray(Integer[]::new);
+
+ Integer roundWeightConversionCountryId = Programs.getPropertyAsInteger(program, ProgramPropertyEnum.TRIP_BATCH_ROUND_WEIGHT_CONVERSION_COUNTRY_ID);
+
+ if (roundWeightConversionCountryId == null || roundWeightConversionCountryId < 0) {
+ log.warn("Missing or invalid value for program property '{}'. Will not be able to compute round weight, in batch denormalization!", ProgramPropertyEnum.TRIP_BATCH_ROUND_WEIGHT_CONVERSION_COUNTRY_ID.getKey());
+ }
+
+ return DenormalizedBatchOptions.builder()
+ .taxonGroupIdsNoWeight(taxonGroupIdsNoWeight)
+ .enableTaxonName(Programs.getPropertyAsBoolean(program, ProgramPropertyEnum.TRIP_BATCH_TAXON_NAME_ENABLE))
+ .enableTaxonGroup(Programs.getPropertyAsBoolean(program, ProgramPropertyEnum.TRIP_BATCH_TAXON_GROUP_ENABLE))
+ .enableRtpWeight(canEnableRtpWeight && Programs.getPropertyAsBoolean(program, ProgramPropertyEnum.TRIP_BATCH_LENGTH_WEIGHT_CONVERSION_ENABLE))
+ .roundWeightCountryLocationId(roundWeightConversionCountryId)
+ .build();
+ }
+
+ protected void computeRtpWeights(List batches, DenormalizedBatchOptions options) {
+ // Select leafs
+ List leafBatches = batches.stream()
+ .map(target -> (TempDenormalizedBatchVO) target)
+ .filter(target -> !target.hasChildren())
+ .toList();
+
+ int maxRtpWeightDiffPct = options.getMaxRtpWeightDiffPct();
+ log.debug("Computing RTP weights on leafs...");
+
+ // For each leaf, try to compute a RTP weight
+ leafBatches.forEach(batch -> {
+ log.trace("- {}", batch.getLabel());
+
+ computeRtpContextWeight(batch, options)
+ .ifPresent(rtpContextWeight -> {
+ // Apply to the batch
+ batch.setRtpContextWeight(rtpContextWeight);
+
+ // Check diff with existing weight
+ if (batch.getWeight() != null && maxRtpWeightDiffPct > 0) {
+
+ // Compute diff between two weights
+ double errorPct = DenormalizedBatches.computeWeightDiffPercent(rtpContextWeight, batch.getWeight());
+
+ // If delta > max % => warn
+ if (errorPct > maxRtpWeightDiffPct) {
+
+ // Replace weight, if it was a RTP (should be wrong computation in the App ?)
+ if (Objects.equals(batch.getWeightMethodId(), MethodEnum.CALCULATED_WEIGHT_LENGTH.getId())) {
+ log.warn("Batch {} has a invalid RTP weight (computed: {}, actual: {}, delta: {}%). Fixing the RTP weight using the computed value.",
+ batch.getLabel(),
+ batch.getRtpContextWeight(),
+ batch.getWeight(),
+ errorPct
+ );
+ batch.setWeight(rtpContextWeight);
+ } else {
+ log.warn("Batch {} has a invalid weight (RTP: {}, actual: {} => delta: {}%)",
+ batch.getLabel(),
+ batch.getRtpContextWeight(),
+ batch.getWeight(),
+ errorPct
+ );
+ }
+ }
+ }
+ });
+ }
+ );
+ }
+ protected void computeIndirectValues(List batches, DenormalizedBatchOptions options) {
+
+ List revertBatches = batches.stream()
+ .map(target -> (TempDenormalizedBatchVO) target)
+ // Reverse order (start from leaf)
+ .sorted(Collections.reverseOrder(Comparator.comparing(DenormalizedBatchVO::getFlatRankOrder, Short::compareTo)))
+ .toList();
+
+ MutableInt changesCount = new MutableInt(0);
+ MutableInt loopCounter = new MutableInt(0);
+ do {
+ changesCount.setValue(0);
+ loopCounter.increment();
+ log.debug("Computing indirect values (pass #{}) ...", loopCounter);
+
+ // For each (leaf -> root)
+ revertBatches.forEach(batch -> {
+ boolean changed = false;
+
+ log.trace("- {}", batch.getLabel());
+
+ // Indirect context weight
+ Double indirectContextWeight = computeIndirectContextWeight(batch, options);
+ changed = changed || !Objects.equals(indirectContextWeight, batch.getIndirectContextWeight());
+ batch.setIndirectContextWeight(indirectContextWeight);
+
+ // Indirect RTP weight from length (if enabled)
+ if (options.isEnableRtpWeight()) {
+ Double indirectRtpContextWeight = computeIndirectRtpContextWeight(batch, options);
+ changed = changed || !Objects.equals(indirectRtpContextWeight, batch.getIndirectRtpContextWeight());
+ batch.setIndirectRtpContextWeight(indirectRtpContextWeight);
+ }
+
+ // Indirect individual count
+ BigDecimal indirectIndividualCount = computeIndirectIndividualCount(batch);
+ changed = changed || !Objects.equals(indirectIndividualCount, batch.getIndirectIndividualCountDecimal());
+ batch.setIndirectIndividualCountDecimal(indirectIndividualCount);
+
+ // Compute alive weight factor
+ if (batch.isLeaf()) {
+ Double aliveWeightFactor = computeAliveWeightFactor(batch, options, batch.isLeaf()).orElse(null);
+ changed = changed || !Objects.equals(aliveWeightFactor, batch.getAliveWeightFactor());
+ batch.setAliveWeightFactor(aliveWeightFactor);
+ }
+ else {
+ Double aliveWeightFactor = computeIndirectAliveWeightFactor(batch, options);
+ changed = changed || !Objects.equals(aliveWeightFactor, batch.getAliveWeightFactor());
+ batch.setAliveWeightFactor(aliveWeightFactor);
+ }
+
+ if (changed) changesCount.increment();
+ });
+
+ log.trace("Computing indirect values (pass #{}) [OK] - {} changes", loopCounter, changesCount);
+ }
+
+ // Continue while changes has been applied on tree
+ while (changesCount.intValue() > 0);
+ }
+
+ /**
+ * Compute elevation factors
+ */
+ protected void computeElevateFactor(List batches, DenormalizedBatchOptions options) {
+ log.debug("Computing elevation factors...");
+
+ // For each (root -> leaf)
+ batches.stream()
+ .map(target -> (TempDenormalizedBatchVO) target)
+ .forEach(target -> {
+ TempDenormalizedBatchVO parent = (TempDenormalizedBatchVO) target.getParent();
+
+ log.trace("{} {}", target.getTreeIndent(), target.getLabel());
+
+ BigDecimal samplingFactor = target.getSamplingFactor() != null ? target.getSamplingFactor() : new BigDecimal(1);
+
+ // Elevate context factor (=samplingFactor x parent value)
+ BigDecimal elevateContextFactor = samplingFactor;
+ if (parent != null) {
+ elevateContextFactor = elevateContextFactor.multiply(parent.getElevateContextFactor());
+ }
+ target.setElevateContextFactor(elevateContextFactor);
+
+ // Taxon elevation factor (=samplingFactor x parent value - BUT not if parent has no taxonGroup/taxonName)
+ if (target.hasTaxonGroup() || target.hasTaxonName()) {
+ BigDecimal taxonElevateFactor = samplingFactor;
+ // Apply parent factor (only if has taxonGroup)
+ if (parent != null && (parent.hasTaxonName() || parent.hasTaxonName())) {
+ taxonElevateFactor = taxonElevateFactor.multiply(parent.getTaxonElevateFactor());
+ }
+ target.setTaxonElevateFactor(taxonElevateFactor);
+ }
+
+ // Elevation factor (alive weight) = elevateContextFactor x aliveWeightFactor
+ if (target.getAliveWeightFactor() != null) {
+ BigDecimal elevateFactor = elevateContextFactor.multiply(new BigDecimal(target.getAliveWeightFactor()));
+ target.setElevateFactor(elevateFactor);
+ }
+ else {
+ target.setElevateFactor(elevateContextFactor);
+ }
+ });
+ }
+
+ /**
+ * Compute elevated values
+ */
+ protected void computeElevatedValues(List batches, DenormalizedBatchOptions options) {
+ MutableInt changesCount = new MutableInt(0);
+ MutableInt loopCounter = new MutableInt(0);
+
+ do {
+ changesCount.setValue(0);
+ loopCounter.increment();
+ log.debug("Computing elevated values (pass #{}) ...", loopCounter);
+
+ // For each (root -> leaf)
+ batches.stream()
+ .map(batch -> (TempDenormalizedBatchVO) batch)
+ .forEach(batch -> {
+ boolean changed = false;
+ log.trace("{} {}", batch.getTreeIndent(), batch.getLabel());
+
+ // Base weight
+ BigDecimal contextWeight = Numbers.firstNotNullAsBigDecimal(batch.getWeight(), batch.getIndirectContextWeight());
+
+ if (contextWeight != null) {
+ // Elevate contextual weight
+ {
+ Double elevateContextWeight = contextWeight.multiply(batch.getElevateContextFactor())
+ .divide(new BigDecimal(1), WEIGHT_DECIMAL_SCALE, RoundingMode.HALF_UP)
+ .doubleValue();
+ changed = changed || !Objects.equals(elevateContextWeight, batch.getElevateContextWeight());
+ batch.setElevateContextWeight(elevateContextWeight);
+ }
+
+ // Taxon elevate context weight
+ if (batch.getTaxonElevateFactor() != null) {
+ Double taxonElevateContextWeight = contextWeight.multiply(batch.getTaxonElevateFactor()).doubleValue();
+ changed = changed || !Objects.equals(taxonElevateContextWeight, batch.getTaxonElevateContextWeight());
+ batch.setTaxonElevateContextWeight(taxonElevateContextWeight);
+ }
+
+ // Elevate weight (alive weight)
+ {
+ Double elevateWeight = contextWeight.multiply(batch.getElevateFactor())
+ .divide(new BigDecimal(1), WEIGHT_DECIMAL_SCALE, RoundingMode.HALF_UP)
+ .doubleValue();
+ changed = changed || !Objects.equals(elevateWeight, batch.getElevateWeight());
+ batch.setElevateWeight(elevateWeight);
+ }
+ }
+
+ if (options.isEnableRtpWeight()) {
+
+ BigDecimal rtpContextWeight = Numbers.firstNotNullAsBigDecimal(batch.getRtpContextWeight(), batch.getIndirectRtpContextWeight());
+ if (rtpContextWeight != null) {
+ // Elevate RTP context weight
+ {
+ Double elevateRtpContextWeight = rtpContextWeight.multiply(batch.getElevateContextFactor())
+ .divide(new BigDecimal(1), WEIGHT_DECIMAL_SCALE, RoundingMode.HALF_UP)
+ .doubleValue();
+ changed = changed || !Objects.equals(elevateRtpContextWeight, batch.getElevateRtpContextWeight());
+ batch.setElevateRtpContextWeight(elevateRtpContextWeight);
+ }
+
+ // Indirect RTP weight (from indirect RTP context weight, converted to alive)
+ {
+ BigDecimal aliveWeightFactor = Numbers.firstNotNullAsBigDecimal(batch.getAliveWeightFactor(), new BigDecimal(1));
+ Double indirectRtpWeight = rtpContextWeight.multiply(aliveWeightFactor)
+ .divide(new BigDecimal(1), WEIGHT_DECIMAL_SCALE, RoundingMode.HALF_UP)
+ .doubleValue();
+ changed = changed || !Objects.equals(indirectRtpWeight, batch.getIndirectRtpWeight());
+ batch.setIndirectRtpWeight(indirectRtpWeight);
+ }
+
+ // Elevate RTP weight
+ {
+ Double elevateRtpWeight = rtpContextWeight.multiply(batch.getElevateFactor())
+ .divide(new BigDecimal(1), WEIGHT_DECIMAL_SCALE, RoundingMode.HALF_UP)
+ .doubleValue();
+ changed = changed || !Objects.equals(elevateRtpWeight, batch.getElevateRtpWeight());
+ batch.setElevateRtpWeight(elevateRtpWeight);
+ }
+ }
+ }
+
+ BigDecimal individualCount = batch.getIndividualCount() != null ? new BigDecimal(batch.getIndividualCount()) : batch.getIndirectIndividualCountDecimal();
+ if (individualCount != null) {
+ // Taxon elevate individual count
+ if (batch.getTaxonElevateContextWeight() != null && batch.getTaxonElevateFactor() != null) {
+ Integer taxonElevateIndividualCount = individualCount.multiply(batch.getTaxonElevateFactor())
+ .divide(new BigDecimal(1), 0, RoundingMode.HALF_UP) // Round to half up
+ .intValue();
+ changed = changed || !Objects.equals(taxonElevateIndividualCount, batch.getTaxonElevateIndividualCount());
+ batch.setTaxonElevateIndividualCount(taxonElevateIndividualCount);
+ }
+
+ // Elevate individual count
+ Integer elevateIndividualCount = individualCount.multiply(batch.getElevateFactor())
+ .divide(new BigDecimal(1), 0, RoundingMode.HALF_UP) // Round to half up
+ .intValue();
+ changed = changed || !Objects.equals(elevateIndividualCount, batch.getElevateIndividualCount());
+ batch.setElevateIndividualCount(elevateIndividualCount);
+
+ // Set indirect individualCount
+ if (batch.getIndirectIndividualCountDecimal() != null) {
+ batch.setIndirectIndividualCount(batch.getIndirectIndividualCountDecimal()
+ .divide(new BigDecimal(1), 0, RoundingMode.HALF_UP) // Round to half up
+ .intValue());
+ }
+ }
+
+ if (changed) {
+ //log.trace("{} {} - changes!", target.getTreeIndent(), target.getLabel());
+ changesCount.increment();
+ }
+ });
+
+ log.trace("Computing elevated values (pass #{}) [OK] - {} changes", loopCounter, changesCount);
+ } while (changesCount.intValue() > 0);
+ }
+
+ protected void computeIndirectElevatedValues(List batches, DenormalizedBatchOptions options) {
+
+ List revertBatches = batches.stream()
+ .map(target -> (TempDenormalizedBatchVO) target)
+ // Reverse order (start from leaf)
+ .sorted(Collections.reverseOrder(Comparator.comparing(DenormalizedBatchVO::getFlatRankOrder, Short::compareTo)))
+ .toList();
+
+ MutableInt changesCount = new MutableInt(0);
+ MutableInt loopCounter = new MutableInt(0);
+ do {
+ changesCount.setValue(0);
+ loopCounter.increment();
+ log.debug("Computing indirect elevated values (pass #{}) ...", loopCounter);
+
+ // For each (leaf -> root)
+ revertBatches
+ .forEach(batch -> {
+ boolean changed = false;
+
+ log.trace("- {}", batch.getLabel());
+
+ if (batch.getElevateWeight() == null) {
+ // No context weight to elevate: so use children elevate weight
+ Double indirectElevateWeight = computeIndirectElevateWeight(batch, options);
+ changed = changed || !Objects.equals(indirectElevateWeight, batch.getIndirectElevateWeight())
+ || !Objects.equals(indirectElevateWeight, batch.getElevateWeight());
+ batch.setIndirectElevateWeight(indirectElevateWeight);
+ batch.setElevateWeight(indirectElevateWeight);
+ }
+
+ if (options.isEnableRtpWeight() && batch.getElevateRtpWeight() == null) {
+ Double indirectElevateRtpWeight = computeIndirectElevateRtpWeight(batch, options);
+ changed = changed || !Objects.equals(indirectElevateRtpWeight, batch.getIndirectRtpElevateWeight())
+ || !Objects.equals(indirectElevateRtpWeight, batch.getElevateRtpWeight());
+ batch.setIndirectRtpElevateWeight(indirectElevateRtpWeight);
+ batch.setElevateRtpWeight(indirectElevateRtpWeight);
+ }
+
+ // Check weight = 0 AND individual
+ boolean zeroWeightWithIndividual = batch.getElevateWeight() != null && batch.getElevateWeight() == 0d
+ && batch.getElevateIndividualCount() != null && batch.getElevateIndividualCount() > 0;
+ if (zeroWeightWithIndividual) {
+ String message = String.format("Invalid batch {id: %s, label: '%s'}: elevateWeight=0 but elevateIndividualCount > 0",
+ batch.getId(), batch.getLabel());
+ if (options.isAllowZeroWeightWithIndividual()) log.warn(message);
+ else throw new InvalidSamplingBatchException(message);
+ }
+
+ if (changed) {
+ //log.trace("{} {} - changes!", target.getTreeIndent(), target.getLabel());
+ changesCount.increment();
+ }
+ });
+
+ log.trace("Computing indirect elevated values (pass #{}) [OK] - {} changes", loopCounter, changesCount);
+ } while (changesCount.intValue() > 0);
+ }
+
+ protected Optional computeRtpContextWeight(TempDenormalizedBatchVO batch, DenormalizedBatchOptions options) {
+ // Already computed: skip
+ if (batch.getRtpContextWeight() != null) return Optional.of(batch.getRtpContextWeight());
+
+ // No individual count: skip
+ // TODO: should we use '1' as default individual count ?
+ if (batch.getIndividualCount() == null) return Optional.empty();
+
+ // No taxon: skip
+ Integer referenceTaxonId = batch.getTaxonName() != null
+ ? batch.getTaxonName().getReferenceTaxonId()
+ : (batch.getInheritedTaxonName() != null ? batch.getInheritedTaxonName().getId() : null);
+ if (referenceTaxonId == null) return Optional.empty();
+
+ return Beans.getStream(batch.getSortingValues())
+ // Filter on length measure
+ .filter(sv -> sv.getNumericalValue() != null
+ && sv.getParameter() != null && sv.getParameter().getId() != null
+ && weightLengthConversionService.isWeightLengthParameter(sv.getParameter().getId())
+ )
+ .map(measure -> {
+ // Get the individual's sex (or not sexed as default)
+ Integer sexId = DenormalizedBatches.getSexId(batch).orElse(QualitativeValueEnum.SEX_UNSEXED.getId());
+ // Get length precision (default = 1 - see Allegro mantis #8330)
+ Double lengthPrecision = measure.getPmfm().getPrecision() != null ? measure.getPmfm().getPrecision() : 1d;
+
+ return weightLengthConversionService.loadFirstByFilter(WeightLengthConversionFilterVO.builder()
+ .month(options.getMonth())
+ .year(options.getYear())
+ .referenceTaxonIds(new Integer[]{referenceTaxonId})
+ .childLocationIds(options.getFishingAreaLocationIds())
+ .lengthPmfmIds(new Integer[]{measure.getPmfmId()})
+ .sexIds(sexId >= 0 ? new Integer[]{sexId} : null)
+ .build())
+ .map(conversion -> weightLengthConversionService.computedWeight(conversion,
+ measure.getNumericalValue(), // Length
+ measure.getUnit().getLabel(),
+ lengthPrecision,
+ batch.getIndividualCount(),
+ UnitEnum.KG.getLabel(),
+ 6 // = mg precision
+ )
+ );
+ })
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .findFirst()
+ // Convert alive RTP weight into dressing/preservation weight
+ .flatMap(rtpAliveWeight -> convertAliveWeightToContext(batch, options, rtpAliveWeight))
+ .map(BigDecimal::doubleValue);
+ }
+
+ protected Double computeIndirectContextWeight(TempDenormalizedBatchVO batch,
+ DenormalizedBatchOptions options) {
+ return computeIndirectWeight(batch, options,
+ DenormalizedBatchVO::getWeight,
+ DenormalizedBatchVO::getIndirectContextWeight,
+ true,
+ TempDenormalizedBatchVO::getAliveWeightFactor
+ );
+ }
+
+ protected Double computeIndirectRtpContextWeight(TempDenormalizedBatchVO batch,
+ DenormalizedBatchOptions options) {
+ return computeIndirectWeight(batch, options,
+ TempDenormalizedBatchVO::getRtpContextWeight,
+ TempDenormalizedBatchVO::getIndirectRtpContextWeight,
+ true,
+ TempDenormalizedBatchVO::getAliveWeightFactor
+ );
+ }
+
+ protected Double computeIndirectElevateWeight(TempDenormalizedBatchVO batch,
+ DenormalizedBatchOptions options) {
+ return computeIndirectWeight(batch, options,
+ TempDenormalizedBatchVO::getElevateWeight,
+ TempDenormalizedBatchVO::getIndirectElevateWeight,
+ false,
+ null // Skip control on same dressing/preservation
+ );
+ }
+
+ protected Double computeIndirectElevateRtpWeight(TempDenormalizedBatchVO batch,
+ DenormalizedBatchOptions options) {
+ return computeIndirectWeight(batch, options,
+ TempDenormalizedBatchVO::getElevateRtpWeight,
+ TempDenormalizedBatchVO::getIndirectRtpElevateWeight,
+ false,
+ null // Skip control on same dressing/preservation
+ );
+ }
+
+ protected Double computeIndirectWeight(TempDenormalizedBatchVO batch,
+ DenormalizedBatchOptions options,
+ Function weightGetter,
+ Function indirectWeightGetter,
+ boolean applySamplingRatio,
+ @Nullable Function aliveWeightFactorGetter) {
+ // Already computed: skip
+ Double indirectWeight = indirectWeightGetter.apply(batch);
+ if (indirectWeight != null) return indirectWeight;
+
+ if (applySamplingRatio) {
+ // Sampling batch
+ if (DenormalizedBatches.isSamplingBatch(batch)) {
+ try {
+ Double samplingWeight = computeSamplingWeightAndRatio(batch, false,
+ options, weightGetter, indirectWeightGetter, aliveWeightFactorGetter);
+ if (samplingWeight != null) return samplingWeight;
+ } catch (InvalidSamplingBatchException e) {
+ // May be not a sampling batch ? (e.g. a species batch)
+ indirectWeight = computeSumChildrenWeight(batch, options,
+ weightGetter, indirectWeightGetter, true, aliveWeightFactorGetter);
+ if (indirectWeight != null) return indirectWeight;
+ throw e;
+ }
+ // Invalid sampling batch: Continue if not set
+ }
+
+ // Child batch is a sampling batch
+ if (DenormalizedBatches.isParentOfSamplingBatch(batch)) {
+ return computeParentSamplingWeight(batch, false, weightGetter, indirectWeightGetter);
+ }
+ }
+
+ // Has children (not a leaf batch)
+ if (batch.hasChildren()) {
+ // Compute sum from children's weight
+ return computeSumChildrenWeight(batch, options, weightGetter, indirectWeightGetter, applySamplingRatio, aliveWeightFactorGetter);
+ }
+ // Leaf batch: use the current weight as default
+ else {
+ return weightGetter.apply(batch);
+ }
+ }
+
+ protected Double computeSamplingWeightAndRatio(TempDenormalizedBatchVO batch,
+ boolean checkArgument,
+ DenormalizedBatchOptions options,
+ Function weightGetter,
+ Function indirectWeightGetter,
+ Function aliveWeightFactorGetter) {
+ if (checkArgument) Preconditions.checkArgument(DenormalizedBatches.isSamplingBatch(batch));
+
+ TempDenormalizedBatchVO parent = (TempDenormalizedBatchVO) batch.getParent();
+ boolean parentExhaustiveInventory = DenormalizedBatches.isExhaustiveInventory(parent);
+ Double parentWeight = weightGetter.apply(parent);
+ Double weight = weightGetter.apply(batch);
+ Double samplingWeight = null;
+ Double samplingRatio = batch.getSamplingRatio();
+ BigDecimal samplingFactor = null;
+ final int scale = INTERMEDIATE_DECIMAL_SCALE;
+
+ // Ignore invalid value
+ if (samplingRatio != null && (Double.isNaN(samplingRatio) || Double.isInfinite(batch.getSamplingRatio()))) {
+ samplingRatio = null;
+ }
+
+ if (samplingRatio != null) {
+ samplingFactor = samplingRatio <= 0
+ ? new BigDecimal(0)
+ : new BigDecimal(1).divide(new BigDecimal(samplingRatio), scale, RoundingMode.HALF_UP);
+
+ // Try to restore sampling ratio from text (more accuracy)
+ if (samplingRatio > 0 && StringUtils.isNotBlank(batch.getSamplingRatioText()) && batch.getSamplingRatioText().contains("/")) {
+ String[] parts = batch.getSamplingRatioText().split("/", 2);
+ try {
+ BigDecimal shouldBeSamplingWeight = new BigDecimal(parts[0]);
+ BigDecimal shouldBeParentWeight = new BigDecimal(parts[1]);
+ samplingRatio = shouldBeParentWeight.doubleValue() <= 0
+ ? 0d
+ : shouldBeSamplingWeight.divide(shouldBeParentWeight, scale, RoundingMode.HALF_UP).doubleValue();
+ samplingFactor = shouldBeSamplingWeight.doubleValue() <= 0
+ ? new BigDecimal(0)
+ : shouldBeParentWeight.divide(shouldBeSamplingWeight, scale, RoundingMode.HALF_UP);
+ } catch (Exception e) {
+ log.warn("Cannot parse samplingRatioText on batch {id: {}, label: '{}', saplingRatioText: '{}'} : {}",
+ batch.getId(),
+ batch.getLabel(),
+ batch.getSamplingRatioText(),
+ e.getMessage());
+ }
+ }
+ } else if (parentExhaustiveInventory && parentWeight != null && weight != null) {
+ if (weight > parentWeight) {
+ throw new InvalidSamplingBatchException(String.format("Invalid batch weight {id: %s, label: '%s', weight: %s}. Should be <= %s kg (parent weight)",
+ batch.getId(), batch.getLabel(), weight,
+ parentWeight));
+ }
+ if (parentWeight <= 0) {
+ samplingRatio = 0d;
+ samplingFactor = new BigDecimal(0);
+ }
+ else {
+ samplingRatio = new BigDecimal(weight)
+ .divide(new BigDecimal(parentWeight), scale, RoundingMode.HALF_UP)
+ .doubleValue();
+ samplingFactor = weight == 0d
+ ? new BigDecimal(0)
+ : new BigDecimal(parentWeight).divide(new BigDecimal(weight), scale, RoundingMode.HALF_UP);
+ }
+ } else if (parentExhaustiveInventory && parentWeight != null && batch.hasChildren()) {
+ samplingWeight = computeSumChildrenWeight(batch, options, weightGetter, indirectWeightGetter, true, aliveWeightFactorGetter);
+ if (samplingWeight != null) {
+ if (parentWeight <= 0d || samplingWeight <= 0d) {
+ samplingRatio = 0d;
+ samplingFactor = new BigDecimal(0);
+ }
+ else {
+ samplingRatio = new BigDecimal(samplingWeight).divide(new BigDecimal(parentWeight), scale, RoundingMode.HALF_UP).doubleValue();
+ samplingFactor = new BigDecimal(parentWeight).divide(new BigDecimal(samplingWeight), scale, RoundingMode.HALF_UP);
+ }
+ }
+ } else if ((!parentExhaustiveInventory || parentWeight == null) && batch.hasChildren()) {
+ samplingWeight = computeSumChildrenWeight(batch, options, weightGetter, indirectWeightGetter, true, aliveWeightFactorGetter);
+ if (samplingWeight != null) {
+ samplingRatio = 1d;
+ samplingFactor = new BigDecimal(1);
+ }
+ }
+
+ // When taxon group without weight: compute the simpling ratio by individual count
+ else if (parent.getTaxonGroupId() != null
+ && ArrayUtils.isNotEmpty(options.getTaxonGroupIdsNoWeight())
+ && ArrayUtils.contains(options.getTaxonGroupIdsNoWeight(), parent.getInheritedTaxonGroup().getId())) {
+ // TODO
+ log.warn("Batch {label: '{}'} - TODO try to compute samplingRatio using individualCount parent/child (taxon group no weight)", batch.getLabel());
+ }
+
+ if (samplingRatio == null || samplingFactor == null) {
+ // Use default value (samplingRatio=1) if:
+ // - batch has no children
+ // - batch is parent of a sampling batch
+ if (CollectionUtils.isEmpty(batch.getChildren())) {
+ samplingRatio = 1d;
+ samplingFactor = new BigDecimal(1);
+ } else {
+ throw new InvalidSamplingBatchException(String.format("Invalid sampling batch {id: %s, label: '%s'}: cannot get or compute the sampling ratio",
+ batch.getId(), batch.getLabel()));
+ }
+ }
+
+
+ // Remember values
+ batch.setSamplingRatio(samplingRatio);
+ batch.setSamplingFactor(samplingFactor);
+
+ // Find the weight of current sampling batch
+ if (samplingWeight == null) {
+ if (weight != null) {
+ samplingWeight = weight;
+ } else if (parentExhaustiveInventory) {
+ if (parentWeight != null) {
+ samplingWeight = new BigDecimal(parentWeight).multiply(new BigDecimal(samplingRatio))
+ .divide(new BigDecimal(1), WEIGHT_DECIMAL_SCALE, RoundingMode.HALF_UP)
+ .doubleValue();
+ }
+ }
+ }
+
+ return samplingWeight;
+ }
+
+
+ protected Double computeParentSamplingWeight(TempDenormalizedBatchVO parent,
+ boolean checkArgument,
+ Function weightGetter,
+ Function indirectWeightGetter
+ ) {
+ if (checkArgument) Preconditions.checkArgument(DenormalizedBatches.isParentOfSamplingBatch(parent));
+
+ // Use reference weight, if any
+ Double parentWeight = weightGetter.apply(parent);
+ if (parentWeight != null) return parentWeight;
+
+ TempDenormalizedBatchVO samplingBatch = (TempDenormalizedBatchVO) CollectionUtils.extractSingleton(parent.getChildren());
+ Double samplingWeight = weightGetter.apply(samplingBatch);
+ Double samplingIndirectWeight = indirectWeightGetter.apply(samplingBatch);
+ int scale = INTERMEDIATE_DECIMAL_SCALE;
+
+ Double samplingRatio = null;
+ BigDecimal samplingFactor = null;
+ if (samplingBatch.getSamplingRatio() != null) {
+ samplingRatio = samplingBatch.getSamplingRatio();
+ samplingFactor = samplingRatio <= 0d
+ ? new BigDecimal(0)
+ : new BigDecimal(1).divide(new BigDecimal(samplingRatio), scale, RoundingMode.HALF_UP);
+
+ // Try to use the sampling ratio text (more accuracy)
+ if (samplingRatio > 0 && StringUtils.isNotBlank(samplingBatch.getSamplingRatioText()) && samplingBatch.getSamplingRatioText().contains("/")) {
+ String[] parts = samplingBatch.getSamplingRatioText().split("/", 2);
+ try {
+ BigDecimal shouldBeSamplingWeight = new BigDecimal(parts[0]);
+ BigDecimal shouldBeParentWeight = new BigDecimal(parts[1]);
+ // If ratio text use the sampling weight, we have the parent weight
+ if (Objects.equals(shouldBeSamplingWeight, samplingWeight)
+ || Objects.equals(shouldBeSamplingWeight, samplingIndirectWeight)) {
+ parentWeight = shouldBeParentWeight.doubleValue();
+ }
+ samplingRatio = shouldBeParentWeight.doubleValue() <= 0d
+ ? 0
+ : shouldBeSamplingWeight.divide(shouldBeParentWeight, scale, RoundingMode.HALF_UP).doubleValue();
+ samplingFactor = shouldBeSamplingWeight.doubleValue() <= 0
+ ? new BigDecimal(0)
+ : shouldBeParentWeight.divide(shouldBeSamplingWeight, scale, RoundingMode.HALF_UP);
+ } catch (Exception e) {
+ log.warn(String.format("Cannot parse samplingRatioText on batch {id: %s, label: '%s', saplingRatioText: '%s'} : %s",
+ samplingBatch.getId(),
+ samplingBatch.getLabel(),
+ samplingBatch.getSamplingRatioText(),
+ e.getMessage()));
+ }
+ }
+ }
+
+ if (samplingRatio == null)
+ throw new SumarisTechnicalException(String.format("Invalid fraction batch {id: %s, label: '%s'}: cannot get or compute the sampling ratio",
+ samplingBatch.getId(), samplingBatch.getLabel()));
+
+ if (parentWeight == null) {
+ if (samplingWeight != null) {
+ parentWeight = new BigDecimal(samplingWeight).multiply(samplingFactor)
+ .divide(new BigDecimal(1), WEIGHT_DECIMAL_SCALE, RoundingMode.HALF_UP)
+ .doubleValue();
+ } else if (samplingIndirectWeight != null) {
+ parentWeight = new BigDecimal(samplingIndirectWeight).multiply(samplingFactor)
+ .divide(new BigDecimal(1), WEIGHT_DECIMAL_SCALE, RoundingMode.HALF_UP)
+ .doubleValue();
+ }
+ }
+
+ return parentWeight;
+ }
+
+ protected Double computeSumChildrenWeight(@NonNull DenormalizedBatchVO batch,
+ @NonNull DenormalizedBatchOptions options,
+ @NonNull Function weightGetter,
+ @NonNull Function indirectWeightGetter,
+ boolean applySamplingRatio,
+ @Nullable Function aliveWeightFactorGetter) {
+ // Cannot compute children sum, when:
+ // - Not exhaustive inventory
+ // - No children
+ if (!DenormalizedBatches.isExhaustiveInventory(batch)
+ || !batch.hasChildren()) {
+ return null;
+ }
+
+ // We track children alive weight factor. If not SAME => cannot compute sum
+ MutableDouble childrenAliveWeightFactor = new MutableDouble(-1d);
+
+ try {
+ return Beans.getStream(batch.getChildren())
+ .map(child -> (TempDenormalizedBatchVO) child)
+ .mapToDouble(child -> {
+ if (aliveWeightFactorGetter != null) {
+ Double aliveWeightFactor = aliveWeightFactorGetter.apply(child);
+ if (aliveWeightFactor == null) aliveWeightFactor = 1d;
+ // Not set: update and continue
+ if (childrenAliveWeightFactor.doubleValue() == -1d) {
+ childrenAliveWeightFactor.setValue(aliveWeightFactor);
+ }
+ // Control that all children have alive weight factor. If not, we cannot compute sum.
+ else if (!Objects.equals(childrenAliveWeightFactor.getValue(), aliveWeightFactor)) {
+ // Stop here, because we cannot sum all children's weight
+ throw new SumarisTechnicalException(String.format("No indirect weight"
+ + " (a child has a different dressing/preservation: {id: %s, label: '%s'})", child.getId(), child.getLabel()));
+ }
+ }
+ // Use child weight, if any
+ Double weight = weightGetter.apply(child);
+ if (weight != null) return weight;
+
+ // Compute indirect weight
+ Double indirectWeight = computeIndirectWeight(child, options, weightGetter, indirectWeightGetter, applySamplingRatio, aliveWeightFactorGetter);
+ if (indirectWeight != null) return indirectWeight;
+
+ // Stop here, because we cannot sum all children's weight
+ throw new SumarisTechnicalException(String.format("No indirect weight"
+ + " (a child has no weight {id: %s, label: '%s'})", child.getId(), child.getLabel()));
+ }).sum();
+ } catch (SumarisTechnicalException e) {
+ log.trace(e.getMessage());
+ return null;
+ }
+ }
+
+ protected BigDecimal computeIndirectIndividualCount(TempDenormalizedBatchVO batch) {
+ // Already computed: skip
+ if (batch.getIndirectIndividualCountDecimal() != null) return batch.getIndirectIndividualCountDecimal();
+
+ // Cannot compute when:
+ // - Not exhaustive inventory
+ // - No children
+ if (!DenormalizedBatches.isExhaustiveInventory(batch)
+ || !batch.hasChildren()) {
+ return null;
+ }
+
+ try {
+ return Beans.getStream(batch.getChildren())
+ .map(child -> (TempDenormalizedBatchVO) child)
+ .map(child -> {
+ if (child.getIndividualCount() != null) {
+ BigDecimal samplingFactor = Optional.ofNullable(child.getSamplingFactor()).orElse(new BigDecimal(1));
+ return samplingFactor.multiply(new BigDecimal(child.getIndividualCount()));
+ }
+ if (child.hasChildren()) {
+ BigDecimal indirectIndividualCount = computeIndirectIndividualCount(child);
+ if (indirectIndividualCount != null) {
+ return indirectIndividualCount;
+ }
+ }
+ throw new SumarisTechnicalException(String.format("No indirect individual count,"
+ + " (some child batch has no individual count {id: %s, label: '%s'})", child.getId(), child.getLabel()));
+ }).reduce(new BigDecimal(0), BigDecimal::add);
+ } catch (SumarisTechnicalException e) {
+ log.trace(e.getMessage());
+ return null;
+ }
+ }
+
+ protected void computeTreeIndent(DenormalizedBatchVO target) {
+ computeTreeIndent(target, "", true);
+ }
+
+ protected void computeTreeIndent(DenormalizedBatchVO target, String inheritedTreeIndent, boolean isLast) {
+ if (target.getParent() == null) {
+ target.setTreeIndent("-");
+ } else {
+ target.setTreeIndent(inheritedTreeIndent + (isLast ? "|_" : "|-"));
+ }
+
+ List children = target.getChildren();
+ if (CollectionUtils.isNotEmpty(children)) {
+ String childrenTreeIndent = inheritedTreeIndent + (isLast ? " " : "| ");
+ for (int i = 0; i < children.size(); i++) {
+ computeTreeIndent(children.get(i), childrenTreeIndent, i == children.size() - 1);
+ }
+ }
+ }
+
+ protected TempDenormalizedBatchVO createTempVO(BatchVO source) {
+ TempDenormalizedBatchVO target = new TempDenormalizedBatchVO();
+ denormalizedBatchRepository.copy(source, target, true);
+ return target;
+ }
+
+ /**
+ * Convert an alive weight into batch's dressing and preservation.
+ * If dressing/preservation are whole/fresh, then return unchanged weight
+ * @param batch
+ * @param options
+ * @param aliveWeight
+ * @return
+ */
+ protected Optional convertAliveWeightToContext(TempDenormalizedBatchVO batch,
+ DenormalizedBatchOptions options,
+ BigDecimal aliveWeight) {
+ // Apply inverse conversion (alive weight / conversion factor)
+ return computeAliveWeightFactor(batch, options, batch.isLeaf())
+ .map(conversionFactor -> {
+
+ // Check conversion is not negative or zero (should never occur)
+ if (conversionFactor <= 0) throw new SumarisTechnicalException("Invalid round weight conversion. Coefficient should be > 0");
+
+ // No conversion: skip
+ if (conversionFactor == 1d) return aliveWeight;
+
+ // Apply inverse conversion
+ return aliveWeight.divide(new BigDecimal(conversionFactor), WEIGHT_DECIMAL_SCALE, RoundingMode.HALF_UP);
+ });
+ }
+
+ protected Optional computeAliveWeightFactor(TempDenormalizedBatchVO batch,
+ DenormalizedBatchOptions options,
+ boolean applyDressingAndPreservationDefaults) {
+
+ if (batch.getAliveWeightFactor() != null) return Optional.of(batch.getAliveWeightFactor());
+
+ Integer taxonGroupId = batch.getTaxonGroupId();
+ if (taxonGroupId == null) return Optional.empty();
+
+ // No weight for this species:
+ if (ArrayUtils.isNotEmpty(options.getTaxonGroupIdsNoWeight())
+ && ArrayUtils.contains(options.getTaxonGroupIdsNoWeight(), taxonGroupId)) {
+ return Optional.empty();
+ }
+
+ // Get dressing, or 'WHL - Whole' by default
+ Integer dressingId = DenormalizedBatches.getDressingId(batch)
+ .orElseGet(() -> {
+ // Apply defaults
+ if (applyDressingAndPreservationDefaults) {
+ return Boolean.TRUE.equals(batch.getIsLanding())
+ ? options.getDefaultLandingDressingId()
+ : Boolean.TRUE.equals(batch.getIsDiscard())
+ ? options.getDefaultDiscardDressingId()
+ : QualitativeValueEnum.DRESSING_WHOLE.getId();
+ }
+ return null;
+ });
+
+ // Get preservation, or 'FRE - Fresh' by default
+ Integer preservationId = DenormalizedBatches.getPreservationId(batch)
+ .orElseGet(() -> {
+ // Apply default preservation
+ if (applyDressingAndPreservationDefaults) {
+ return Boolean.TRUE.equals(batch.getIsLanding())
+ ? options.getDefaultLandingPreservationId()
+ : Boolean.TRUE.equals(batch.getIsDiscard())
+ ? options.getDefaultDiscardPreservationId()
+ : QualitativeValueEnum.PRESERVATION_FRESH.getId();
+ }
+ return null;
+ });
+
+ // Skip (e.g. if enableDefaultsDressingAndPreservation is 'false')
+ if (dressingId == null || preservationId == null) return Optional.empty();
+
+ // Find the best conversion coefficient
+ Optional conversion = roundWeightConversionService.findFirstByFilter(RoundWeightConversionFilterVO.builder()
+ .taxonGroupIds(new Integer[]{taxonGroupId})
+ .dressingIds(new Integer[]{dressingId})
+ .preservingIds(new Integer[]{preservationId})
+ .locationIds(new Integer[]{options.getRoundWeightCountryLocationId()})
+ .date(options.getDay())
+ .build());
+
+ if (conversion.isEmpty()) {
+ log.warn("No RoundWeightConversion found for {taxonGroupId: {}, dressingId: {}, preservationId: {}, locationId: {}}",
+ taxonGroupId,
+ dressingId,
+ preservationId,
+ options.getRoundWeightCountryLocationId());
+ }
+
+
+ return conversion.map(RoundWeightConversionVO::getConversionCoefficient);
+ }
+
+ protected Double computeIndirectAliveWeightFactor(TempDenormalizedBatchVO batch, DenormalizedBatchOptions options) {
+ if (batch.getAliveWeightFactor() != null) return batch.getAliveWeightFactor();
+
+ // No taxon group, or no child => no indirect value
+ if (!batch.hasTaxonGroup() || !batch.hasChildren() || !DenormalizedBatches.isExhaustiveInventory(batch)) return null;
+
+ // Collect all children factors
+ List childAliveFactors = batch.getChildren()
+ .stream()
+ .map(child -> computeIndirectAliveWeightFactor((TempDenormalizedBatchVO) child, options))
+ .toList();
+
+ // Peek the first value (should be not null)
+ Double firstAliveFactor = childAliveFactors.get(0);
+ if (firstAliveFactor == null) return null;
+
+ // Check same value on each child. If not: return empty (cannot compute indirect value)
+ boolean alwaysSameValue = childAliveFactors.size() == 1 || !childAliveFactors.stream().anyMatch(value -> !firstAliveFactor.equals(value));
+ if (!alwaysSameValue) {
+ log.trace("No indirect alive weight. No unique value found in children");
+ return null;
+ }
+
+ return firstAliveFactor;
+ }
+
+ private void checkBaseEnumerations() {
+ EntityEnums.checkResolved(
+ QualitativeValueEnum.LANDING,
+ QualitativeValueEnum.DISCARD
+ );
+ }
+ private void checkRtpEnumerations() {
+ EntityEnums.checkResolved(
+ ParameterEnum.SEX,
+ QualitativeValueEnum.SEX_UNSEXED,
+ QualitativeValueEnum.DRESSING_WHOLE,
+ QualitativeValueEnum.DRESSING_GUTTED,
+ QualitativeValueEnum.PRESERVATION_FRESH);
+ }
+}
diff --git a/sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizedOperationService.java b/sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizedOperationService.java
new file mode 100644
index 0000000000..89f6dc3fa6
--- /dev/null
+++ b/sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizedOperationService.java
@@ -0,0 +1,57 @@
+package net.sumaris.core.service.data.denormalize;
+
+/*-
+ * #%L
+ * SUMARiS:: Core
+ * %%
+ * Copyright (C) 2018 SUMARiS Consortium
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program. If not, see
+ * .
+ * #L%
+ */
+
+
+import lombok.NonNull;
+import net.sumaris.core.vo.data.OperationVO;
+import net.sumaris.core.vo.data.batch.DenormalizedBatchOptions;
+import net.sumaris.core.vo.filter.OperationFilterVO;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Nullable;
+
+/**
+ * @author BLA
+ *
+ * Service in charge of operation data
+ *
+ */
+@Transactional
+public interface DenormalizedOperationService {
+
+ @Transactional(readOnly = true)
+ DenormalizedBatchOptions createOptionsByProgramId(int programId);
+
+ @Transactional(readOnly = true)
+ DenormalizedBatchOptions createOptionsByProgramLabel(String programLabel);
+
+ @Transactional(readOnly = true)
+ DenormalizedBatchOptions createOptionsByOperation(@NonNull OperationVO operation,
+ @Nullable DenormalizedBatchOptions inheritedOptions);
+
+ @Transactional(propagation = Propagation.NOT_SUPPORTED)
+ DenormalizedTripResultVO denormalizeByFilter(@NonNull OperationFilterVO filter, @Nullable DenormalizedBatchOptions options);
+
+}
diff --git a/sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizeTripResultVO.java b/sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizedTripResultVO.java
similarity index 78%
rename from sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizeTripResultVO.java
rename to sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizedTripResultVO.java
index fc6bbd9037..9b263f357b 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizeTripResultVO.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizedTripResultVO.java
@@ -24,16 +24,24 @@
import lombok.Builder;
import lombok.Data;
+import net.sumaris.core.model.technical.job.JobStatusEnum;
+import net.sumaris.core.vo.technical.job.IJobResultVO;
import java.io.Serializable;
@Data
@Builder
-public class DenormalizeTripResultVO implements Serializable {
+public class DenormalizedTripResultVO implements IJobResultVO, Serializable {
private long tripCount;
private long operationCount;
private long batchCount;
+
+ private long tripErrorCount;
private long invalidBatchCount;
private long executionTime;
+
+ private String message;
+
+ private JobStatusEnum status;
}
diff --git a/sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizeTripService.java b/sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizedTripService.java
similarity index 82%
rename from sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizeTripService.java
rename to sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizedTripService.java
index 33daab06fa..86180ddd85 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizeTripService.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizedTripService.java
@@ -36,14 +36,14 @@
*
*/
@Transactional
-public interface DenormalizeTripService {
+public interface DenormalizedTripService {
@Transactional(propagation = Propagation.NOT_SUPPORTED)
- DenormalizeTripResultVO denormalizeByFilter(@NonNull TripFilterVO filter);
+ DenormalizedTripResultVO denormalizeByFilter(@NonNull TripFilterVO filter);
@Transactional(propagation = Propagation.NOT_SUPPORTED)
- DenormalizeTripResultVO denormalizeByFilter(TripFilterVO filter, IProgressionModel progression);
+ DenormalizedTripResultVO denormalizeByFilter(TripFilterVO filter, IProgressionModel progression);
@Transactional
- DenormalizeTripResultVO denormalizeById(int tripId);
+ DenormalizedTripResultVO denormalizeById(int tripId);
}
diff --git a/sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizedTripServiceImpl.java b/sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizedTripServiceImpl.java
new file mode 100644
index 0000000000..7d3cd4c9d3
--- /dev/null
+++ b/sumaris-core/src/main/java/net/sumaris/core/service/data/denormalize/DenormalizedTripServiceImpl.java
@@ -0,0 +1,192 @@
+package net.sumaris.core.service.data.denormalize;
+
+/*-
+ * #%L
+ * SUMARiS:: Core
+ * %%
+ * Copyright (C) 2018 SUMARiS Consortium
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program. If not, see
+ * .
+ * #L%
+ */
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.Lists;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import net.sumaris.core.dao.technical.SortDirection;
+import net.sumaris.core.model.IProgressionModel;
+import net.sumaris.core.model.ProgressionModel;
+import net.sumaris.core.service.data.TripService;
+import net.sumaris.core.util.StringUtils;
+import net.sumaris.core.util.TimeUtils;
+import net.sumaris.core.vo.data.TripFetchOptions;
+import net.sumaris.core.vo.data.TripVO;
+import net.sumaris.core.vo.data.batch.DenormalizedBatchOptions;
+import net.sumaris.core.vo.filter.OperationFilterVO;
+import net.sumaris.core.vo.filter.TripFilterVO;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.mutable.MutableInt;
+import org.springframework.stereotype.Service;
+
+import java.util.Arrays;
+import java.util.List;
+
+@Service("denormalizeTripService")
+@RequiredArgsConstructor
+@Slf4j
+public class DenormalizedTripServiceImpl implements DenormalizedTripService {
+
+ private final TripService tripService;
+
+ private final DenormalizedBatchService denormalizedBatchService;
+
+ private final DenormalizedOperationService denormalizedOperationService;
+
+
+
+ @Override
+ public DenormalizedTripResultVO denormalizeByFilter(@NonNull TripFilterVO filter) {
+ ProgressionModel progress = new ProgressionModel();
+ progress.addPropertyChangeListener(ProgressionModel.Fields.MESSAGE, (event) -> {
+ if (event.getNewValue() != null) log.debug(event.getNewValue().toString());
+ });
+ return denormalizeByFilter(filter, new ProgressionModel());
+ }
+
+ @Override
+ public DenormalizedTripResultVO denormalizeByFilter(@NonNull TripFilterVO tripFilter, @NonNull IProgressionModel progression) {
+ long startTime = System.currentTimeMillis();
+
+ progression.setCurrent(0);
+ progression.setMessage(String.format("Starting trips denormalization... filter: %s", tripFilter));
+
+ TripFetchOptions tripFetchOptions = TripFetchOptions.builder()
+ .withChildrenEntities(false)
+ .withMeasurementValues(false)
+ .withRecorderPerson(false)
+ .build();
+
+ long tripTotal = tripService.countByFilter(tripFilter);
+ progression.setTotal(tripTotal);
+
+ boolean hasMoreData;
+ int offset = 0;
+ int pageSize = 10;
+ int tripCount = 0;
+ MutableInt tripErrorCount = new MutableInt(0);
+ MutableInt operationCount = new MutableInt(0);
+ MutableInt batchCount = new MutableInt(0);
+ MutableInt invalidBatchCount = new MutableInt(0);
+ List messages = Lists.newArrayList();
+
+ if (tripTotal > 0) {
+ progression.setCurrent(0);
+ progression.setMessage(String.format("Processing trips denormalization... 0/%s", tripTotal));
+
+ do {
+ // Fetch some trips
+ List trips = tripService.findAll(tripFilter,
+ offset, pageSize, // Page
+ TripVO.Fields.ID, SortDirection.ASC, // Sort by id, to keep continuity between pages
+ tripFetchOptions);
+
+ if (offset > 0 && offset % (pageSize * 2) == 0) {
+ progression.setCurrent(offset);
+ progression.setMessage(String.format("Processing trips denormalization... %s/%s", offset, tripTotal));
+ //log.trace(progression.getMessage());
+ }
+
+ // Denormalize each trip
+ trips.stream().parallel()
+ .forEach(trip -> {
+ // Load denormalized options
+ DenormalizedBatchOptions programOptions = denormalizedOperationService.createOptionsByProgramId(trip.getProgram().getId());
+
+ // Create operations filter, for this trip
+ OperationFilterVO operationFilter = OperationFilterVO.builder()
+ .tripId(trip.getId())
+ .includedIds(tripFilter.getOperationIds())
+ .hasNoChildOperation(true)
+ .build();
+
+ try {
+ // Denormalize trip's operation
+ DenormalizedTripResultVO result = denormalizedOperationService.denormalizeByFilter(operationFilter, programOptions);
+
+ operationCount.add(result.getOperationCount());
+ batchCount.add(result.getBatchCount());
+ invalidBatchCount.add(result.getInvalidBatchCount());
+ if (StringUtils.isNotBlank(result.getMessage())) {
+ messages.addAll(Splitter.on("\n").splitToList(result.getMessage()));
+ }
+ }
+ catch (Exception e) {
+ tripErrorCount.increment();
+ String message = String.format("Error while during denormalization of trip #%s: %s", trip.getId(), e.getMessage());
+ log.error(message, e);
+ messages.add(message);
+ }
+ });
+
+ offset += pageSize;
+ tripCount += trips.size();
+ hasMoreData = trips.size() >= pageSize;
+ if (tripCount > tripTotal) {
+ tripTotal = tripCount;
+ progression.adaptTotal(tripTotal);
+ }
+ } while (hasMoreData);
+ }
+
+ // Success log
+ progression.setCurrent(tripCount);
+ progression.setMessage(String.format("Trips denormalization finished, in %s - %s trips, %s operations, %s batches - %s trips in error, %s invalid batch trees (skipped)",
+ TimeUtils.printDurationFrom(startTime),
+ tripCount,
+ operationCount,
+ batchCount,
+ tripErrorCount,
+ invalidBatchCount));
+ //log.debug(progression.getMessage());
+
+ return DenormalizedTripResultVO.builder()
+ .tripCount(tripCount)
+ .tripErrorCount(tripErrorCount.intValue())
+ .operationCount(operationCount.intValue())
+ .batchCount(batchCount.intValue())
+ .invalidBatchCount(invalidBatchCount.intValue())
+ .message(CollectionUtils.isNotEmpty(messages) ? String.join("\n", messages) : null)
+ .executionTime(System.currentTimeMillis() - startTime)
+ .build();
+ }
+
+ @Override
+ public DenormalizedTripResultVO denormalizeById(int tripId) {
+ // Load denormalized options
+ int programId = tripService.getProgramIdById(tripId);
+ DenormalizedBatchOptions programOptions = denormalizedBatchService.createOptionsByProgramId(programId);
+
+ // Create operation filter, for this trip
+ OperationFilterVO operationFilter = OperationFilterVO.builder()
+ .tripId(tripId)
+ .hasNoChildOperation(true)
+ .build();
+
+ return denormalizedOperationService.denormalizeByFilter(operationFilter, programOptions);
+ }
+
+}
diff --git a/sumaris-core/src/main/java/net/sumaris/core/service/referential/LocationServiceImpl.java b/sumaris-core/src/main/java/net/sumaris/core/service/referential/LocationServiceImpl.java
index ed7dfd6ef8..9141a33b15 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/service/referential/LocationServiceImpl.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/service/referential/LocationServiceImpl.java
@@ -35,12 +35,13 @@
import net.sumaris.core.dao.referential.ValidityStatusRepository;
import net.sumaris.core.dao.referential.location.*;
import net.sumaris.core.dao.technical.Page;
+import net.sumaris.core.dao.technical.SortDirection;
import net.sumaris.core.event.config.ConfigurationEvent;
import net.sumaris.core.event.config.ConfigurationReadyEvent;
import net.sumaris.core.event.config.ConfigurationUpdatedEvent;
-import net.sumaris.core.event.entity.EntityInsertEvent;
-import net.sumaris.core.event.entity.EntityUpdateEvent;
+import net.sumaris.core.exception.SumarisTechnicalException;
import net.sumaris.core.model.referential.Status;
+import net.sumaris.core.model.referential.StatusEnum;
import net.sumaris.core.model.referential.ValidityStatus;
import net.sumaris.core.model.referential.ValidityStatusEnum;
import net.sumaris.core.model.referential.location.*;
@@ -51,11 +52,10 @@
import net.sumaris.core.vo.referential.LocationVO;
import net.sumaris.core.vo.referential.ReferentialFetchOptions;
import net.sumaris.core.vo.referential.ReferentialVO;
+import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.locationtech.jts.geom.Geometry;
-import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
-import org.springframework.context.event.EventListener;
import org.springframework.core.io.ResourceLoader;
import org.springframework.dao.DataAccessException;
import org.springframework.scheduling.annotation.Async;
@@ -450,27 +450,44 @@ public void updateLocationHierarchy() {
}
@Override
- public String getLocationLabelByLatLong(Number latitude, Number longitude) {
+ public Optional getStatisticalRectangleLabelByLatLong(Number latitude, Number longitude) {
if (longitude == null || latitude == null) {
throw new IllegalArgumentException("Arguments 'latitude' and 'longitude' should not be null.");
}
// Try to find a statistical rectangle
String rectangleLabel = Locations.getRectangleLabelByLatLong(latitude, longitude);
- if (StringUtils.isNotBlank(rectangleLabel)) return rectangleLabel;
+ if (StringUtils.isNotBlank(rectangleLabel)) {
+ return Optional.of(rectangleLabel);
+ }
- // TODO: find it from spatial query ?
- // Otherwise, return null
- return null;
+ // Otherwise, return empty
+ return Optional.empty();
}
@Override
- public Integer getLocationIdByLatLong(Number latitude, Number longitude) {
- String locationLabel = getLocationLabelByLatLong(latitude, longitude);
- if (locationLabel == null) return null;
- Optional location = referentialDao.findByUniqueLabel(Location.class.getSimpleName(), locationLabel);
- return location.map(ReferentialVO::getId).orElse(null);
+ public Optional getStatisticalRectangleIdByLatLong(Number latitude, Number longitude) {
+ return getStatisticalRectangleLabelByLatLong(latitude, longitude)
+ // Resolve the location, by its label (should be unique, for statistical rectangle - if not dao will return error)
+ .map(rectangleLabel -> {
+ List matches = referentialDao.findByFilter(Location.class.getSimpleName(),
+ ReferentialFilterVO.builder()
+ .label(rectangleLabel)
+ .levelIds(LocationLevels.getStatisticalRectangleLevelIds())
+ .build(), 0, 2, null, null, null);
+ if (CollectionUtils.isEmpty(matches)) return null;
+ try {
+ // Extract singleton (= unique check, because of size=2)
+ return CollectionUtils.extractSingleton(matches);
+ }
+ catch (IllegalArgumentException e) {
+ throw new SumarisTechnicalException(String.format("More than one statistical rectangle found with label '%s'", rectangleLabel));
+ }
+ })
+ .filter(Objects::nonNull)
+ // Extract the id
+ .map(ReferentialVO::getId);
}
@Override
diff --git a/sumaris-core/src/main/java/net/sumaris/core/service/referential/ReferentialService.java b/sumaris-core/src/main/java/net/sumaris/core/service/referential/ReferentialService.java
index 0e414963c1..076e084a48 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/service/referential/ReferentialService.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/service/referential/ReferentialService.java
@@ -25,6 +25,7 @@
import net.sumaris.core.dao.technical.SortDirection;
import net.sumaris.core.model.referential.IReferentialWithStatusEntity;
import net.sumaris.core.vo.filter.IReferentialFilter;
+import net.sumaris.core.vo.referential.ReferentialFetchOptions;
import net.sumaris.core.vo.referential.ReferentialTypeVO;
import net.sumaris.core.vo.referential.ReferentialVO;
import org.springframework.transaction.annotation.Transactional;
@@ -51,7 +52,9 @@ public interface ReferentialService {
List findByFilter(String entityName, IReferentialFilter filter, int offset, int size);
@Transactional(readOnly = true)
- List findByFilter(String entityName, IReferentialFilter filter, int offset, int size, String sortAttribute, SortDirection sortDirection);
+ List findByFilter(String entityName, IReferentialFilter filter, int offset, int size,
+ String sortAttribute, SortDirection sortDirection,
+ ReferentialFetchOptions fetchOptions);
@Transactional(readOnly = true)
Long countByFilter(String entityName, IReferentialFilter filter);
diff --git a/sumaris-core/src/main/java/net/sumaris/core/service/referential/ReferentialServiceImpl.java b/sumaris-core/src/main/java/net/sumaris/core/service/referential/ReferentialServiceImpl.java
index 1b830c9160..766c8dc560 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/service/referential/ReferentialServiceImpl.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/service/referential/ReferentialServiceImpl.java
@@ -28,6 +28,7 @@
import net.sumaris.core.dao.referential.ReferentialDao;
import net.sumaris.core.dao.referential.ReferentialEntities;
import net.sumaris.core.dao.technical.SortDirection;
+import net.sumaris.core.dao.technical.jpa.IFetchOptions;
import net.sumaris.core.event.config.ConfigurationEvent;
import net.sumaris.core.event.config.ConfigurationReadyEvent;
import net.sumaris.core.event.config.ConfigurationUpdatedEvent;
@@ -39,6 +40,7 @@
import net.sumaris.core.model.referential.IReferentialWithStatusEntity;
import net.sumaris.core.vo.filter.IReferentialFilter;
import net.sumaris.core.vo.filter.ReferentialFilterVO;
+import net.sumaris.core.vo.referential.ReferentialFetchOptions;
import net.sumaris.core.vo.referential.ReferentialTypeVO;
import net.sumaris.core.vo.referential.ReferentialVO;
import org.nuiton.i18n.I18n;
@@ -110,16 +112,20 @@ public ReferentialVO getLevelById(String entityName, int levelId) {
}
@Override
- public List findByFilter(String entityName, IReferentialFilter filter, int offset, int size, String sortAttribute, SortDirection sortDirection) {
+ public List findByFilter(String entityName,
+ IReferentialFilter filter, int offset, int size,
+ String sortAttribute, SortDirection sortDirection,
+ ReferentialFetchOptions fetchOptions) {
return referentialDao.findByFilter(entityName, filter != null ? filter : new ReferentialFilterVO(), offset, size, sortAttribute,
- sortDirection);
+ sortDirection,
+ fetchOptions);
}
@Override
public List findByFilter(String entityName, IReferentialFilter filter, int offset, int size) {
return findByFilter(entityName, filter != null ? filter : new ReferentialFilterVO(), offset, size,
IItemReferentialEntity.Fields.LABEL,
- SortDirection.ASC);
+ SortDirection.ASC, null);
}
@Override
diff --git a/sumaris-core/src/main/java/net/sumaris/core/service/referential/conversion/RoundWeightConversionService.java b/sumaris-core/src/main/java/net/sumaris/core/service/referential/conversion/RoundWeightConversionService.java
index a959aa2033..e11078c7f5 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/service/referential/conversion/RoundWeightConversionService.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/service/referential/conversion/RoundWeightConversionService.java
@@ -29,15 +29,22 @@
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
+import java.util.Optional;
@Transactional
public interface RoundWeightConversionService {
List findByFilter(RoundWeightConversionFilterVO filter, Page page, RoundWeightConversionFetchOptions fetchOptions);
+ Optional findFirstByFilter(RoundWeightConversionFilterVO filter);
+
+ Optional findFirstByFilter(RoundWeightConversionFilterVO filter, RoundWeightConversionFetchOptions fetchOptions);
+
long countByFilter(RoundWeightConversionFilterVO filter);
List saveAll(List source);
void deleteAllById(List ids);
+
+
}
diff --git a/sumaris-core/src/main/java/net/sumaris/core/service/referential/conversion/RoundWeightConversionServiceImpl.java b/sumaris-core/src/main/java/net/sumaris/core/service/referential/conversion/RoundWeightConversionServiceImpl.java
index f8f5e9f495..18674d5c08 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/service/referential/conversion/RoundWeightConversionServiceImpl.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/service/referential/conversion/RoundWeightConversionServiceImpl.java
@@ -22,26 +22,34 @@
package net.sumaris.core.service.referential.conversion;
-import com.google.common.collect.ListMultimap;
+import com.google.common.base.Preconditions;
+import lombok.NonNull;
+import net.sumaris.core.config.CacheConfiguration;
import net.sumaris.core.dao.referential.conversion.RoundWeightConversionRepository;
import net.sumaris.core.dao.technical.Page;
-import net.sumaris.core.model.referential.location.LocationLevels;
-import net.sumaris.core.service.referential.LocationService;
-import net.sumaris.core.util.Beans;
-import net.sumaris.core.vo.filter.LocationFilterVO;
-import net.sumaris.core.vo.referential.LocationVO;
+import net.sumaris.core.dao.technical.SortDirection;
+import net.sumaris.core.model.referential.conversion.RoundWeightConversion;
import net.sumaris.core.vo.referential.conversion.RoundWeightConversionFetchOptions;
import net.sumaris.core.vo.referential.conversion.RoundWeightConversionFilterVO;
import net.sumaris.core.vo.referential.conversion.RoundWeightConversionVO;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.ArrayUtils;
+import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
+import java.util.Optional;
import java.util.stream.Collectors;
@Service("roundWeightConversionService")
public class RoundWeightConversionServiceImpl implements RoundWeightConversionService {
+ private static final Page FIND_FIRST_PAGE = Page.builder().size(1)
+ .sortBy(RoundWeightConversion.Fields.START_DATE)
+ .sortDirection(SortDirection.DESC)
+ .build();
+
@Resource
private RoundWeightConversionRepository roundWeightConversionRepository;
@@ -52,6 +60,28 @@ public List findByFilter(RoundWeightConversionFilterVO
return roundWeightConversionRepository.findAll(filter, page, fetchOptions);
}
+ @Override
+ public Optional findFirstByFilter(@NonNull RoundWeightConversionFilterVO filter) {
+ return findFirstByFilter(filter, RoundWeightConversionFetchOptions.DEFAULT);
+ }
+
+ @Override
+ @Cacheable(cacheNames = CacheConfiguration.Names.ROUND_WEIGHT_CONVERSION_FIRST_BY_FILTER, key = "#filter.hashCode() * #fetchOptions.hashCode()")
+ public Optional findFirstByFilter(
+ @NonNull RoundWeightConversionFilterVO filter, @NonNull RoundWeightConversionFetchOptions fetchOptions) {
+ Preconditions.checkArgument(ArrayUtils.isNotEmpty(filter.getTaxonGroupIds()), "Require at least one taxonGroupId");
+ Preconditions.checkArgument(ArrayUtils.isNotEmpty(filter.getLocationIds()), "Require at least one locationIds");
+
+ // Try to find a conversion factor
+ List matches = findByFilter(filter,
+ FIND_FIRST_PAGE,
+ fetchOptions
+ );
+ if (CollectionUtils.isNotEmpty(matches)) return Optional.of(matches.get(0));
+
+ return Optional.empty();
+ }
+
@Override
public long countByFilter(RoundWeightConversionFilterVO filter) {
return roundWeightConversionRepository.count(filter);
diff --git a/sumaris-core/src/main/java/net/sumaris/core/service/referential/conversion/WeightLengthConversionService.java b/sumaris-core/src/main/java/net/sumaris/core/service/referential/conversion/WeightLengthConversionService.java
index e24181ac31..76532fb39c 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/service/referential/conversion/WeightLengthConversionService.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/service/referential/conversion/WeightLengthConversionService.java
@@ -28,22 +28,59 @@
import net.sumaris.core.vo.referential.conversion.WeightLengthConversionVO;
import org.springframework.transaction.annotation.Transactional;
+import javax.annotation.Nullable;
import java.math.BigDecimal;
import java.util.List;
+import java.util.Optional;
@Transactional
public interface WeightLengthConversionService {
- List findByFilter(WeightLengthConversionFilterVO filter, Page page, WeightLengthConversionFetchOptions fetchOptions);
+ @Transactional(readOnly = true)
+ List findByFilter(WeightLengthConversionFilterVO filter, Page page,
+ @Nullable WeightLengthConversionFetchOptions fetchOptions);
+ @Transactional(readOnly = true)
long countByFilter(WeightLengthConversionFilterVO filter);
+ /**
+ * Get the best fit weight-length conversion.
+ * Required at least a reference taxon and location.
+ * Will try to load using this order
+ *
+ * - pmfmId + year + month
+ * - pmfmId + year (without month)
+ * - pmfmId + month (without year)
+ * - TODO: Loop using parameterId (without pmfmId). If found, will convert unit
+ *
+ * @param filter
+ * @param page
+ * @param fetchOptions
+ * @return
+ */
+ @Transactional(readOnly = true)
+ Optional loadFirstByFilter(WeightLengthConversionFilterVO filter);
+
+ @Transactional(readOnly = true)
+ Optional loadFirstByFilter(WeightLengthConversionFilterVO filter, @Nullable WeightLengthConversionFetchOptions fetchOptions);
+
+
List saveAll(List source);
void deleteAllById(List ids);
+ @Transactional(readOnly = true)
BigDecimal computedWeight(WeightLengthConversionVO conversion,
Number length,
- int scale,
- Number individualCount);
+ String lengthUnit,
+ @Nullable Number lengthPrecision,
+ @Nullable Number individualCount,
+ String weightUnit,
+ int weightScale);
+
+ @Transactional(readOnly = true)
+ boolean isWeightLengthParameter(int parameterId);
+
+ @Transactional(readOnly = true)
+ boolean isWeightLengthPmfm(int pmfmId);
}
diff --git a/sumaris-core/src/main/java/net/sumaris/core/service/referential/conversion/WeightLengthConversionServiceImpl.java b/sumaris-core/src/main/java/net/sumaris/core/service/referential/conversion/WeightLengthConversionServiceImpl.java
index ee892d6a02..a9a258b795 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/service/referential/conversion/WeightLengthConversionServiceImpl.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/service/referential/conversion/WeightLengthConversionServiceImpl.java
@@ -23,32 +23,45 @@
package net.sumaris.core.service.referential.conversion;
import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimap;
import lombok.NonNull;
+import net.sumaris.core.config.CacheConfiguration;
import net.sumaris.core.dao.referential.conversion.WeightLengthConversionRepository;
import net.sumaris.core.dao.technical.Page;
+import net.sumaris.core.dao.technical.SortDirection;
import net.sumaris.core.model.referential.StatusEnum;
+import net.sumaris.core.model.referential.conversion.WeightLengthConversion;
import net.sumaris.core.model.referential.location.LocationLevels;
+import net.sumaris.core.model.referential.pmfm.UnitEnum;
import net.sumaris.core.service.referential.LocationService;
import net.sumaris.core.service.referential.pmfm.PmfmService;
import net.sumaris.core.util.Beans;
+import net.sumaris.core.util.conversion.UnitConversions;
import net.sumaris.core.vo.filter.LocationFilterVO;
import net.sumaris.core.vo.filter.PmfmPartsVO;
import net.sumaris.core.vo.referential.LocationVO;
import net.sumaris.core.vo.referential.conversion.WeightLengthConversionFetchOptions;
import net.sumaris.core.vo.referential.conversion.WeightLengthConversionFilterVO;
import net.sumaris.core.vo.referential.conversion.WeightLengthConversionVO;
+import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ArrayUtils;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;
+import javax.annotation.Nullable;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Iterator;
import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
import java.util.stream.Collectors;
@Service("weightLengthConversionService")
@@ -69,8 +82,8 @@ public List findByFilter(WeightLengthConversionFilterV
WeightLengthConversionFetchOptions fetchOptions) {
List result = weightLengthConversionRepository.findAll(filter, page, fetchOptions);
- // Add length pmfm Ids
- if (fetchOptions != null && fetchOptions.isWithRectangleLabels()) {
+ // Add pmfm Ids into VO
+ if (fetchOptions != null && fetchOptions.isWithLengthPmfmIds()) {
// Group by [parameter, unit]
Joiner mapKeyJoiner = Joiner.on('|');
Splitter mapKeySplitter = Splitter.on('|').limit(2);
@@ -128,29 +141,113 @@ public long countByFilter(WeightLengthConversionFilterVO filter) {
return weightLengthConversionRepository.count(filter);
}
+ @Override
+ public Optional loadFirstByFilter(WeightLengthConversionFilterVO filter) {
+ return loadFirstByFilter(filter, WeightLengthConversionFetchOptions.DEFAULT);
+ }
+
+ @Override
+ @Cacheable(cacheNames = CacheConfiguration.Names.WEIGHT_LENGTH_CONVERSION_FIRST_BY_FILTER, key = "#filter.hashCode() * #fetchOptions.hashCode()")
+ public Optional loadFirstByFilter(@NonNull WeightLengthConversionFilterVO filter, @NonNull WeightLengthConversionFetchOptions fetchOptions) {
+ Preconditions.checkArgument(ArrayUtils.isNotEmpty(filter.getReferenceTaxonIds()), "Require at least on referenceTaxonId");
+ Preconditions.checkArgument(ArrayUtils.isNotEmpty(filter.getLocationIds())
+ || ArrayUtils.isNotEmpty(filter.getChildLocationIds())
+ || ArrayUtils.isNotEmpty(filter.getRectangleLabels()), "Require at least one of rectangleLabels, childLocationIds or locationIds");
+
+ final Page page = Page.builder().size(1)
+ .sortBy(filter.getYear() != null ? WeightLengthConversion.Fields.YEAR : WeightLengthConversion.Fields.START_MONTH)
+ .sortDirection(SortDirection.DESC)
+ .build();
+
+ // First, try with full filter
+ List matches = this.findByFilter(filter, page, fetchOptions);
+ if (CollectionUtils.isNotEmpty(matches)) return Optional.of(matches.get(0));
+
+ if (filter.getYear() != null && filter.getMonth() != null) {
+ // Retry on year only (without month)
+ WeightLengthConversionFilterVO filterWithoutMonth = filter.clone(); // Copy, to keep original filter unchanged
+ filterWithoutMonth.setMonth(null);
+ matches = this.findByFilter(filterWithoutMonth, page, fetchOptions);
+ if (CollectionUtils.isNotEmpty(matches)) return Optional.of(matches.get(0));
+
+ // Retry on month only (without year)
+ WeightLengthConversionFilterVO filterWithoutYear = filter.clone(); // Copy, to keep original filter unchanged
+ filterWithoutYear.setYear(null);
+ page.setSortBy(WeightLengthConversion.Fields.YEAR);
+ matches = this.findByFilter(filterWithoutYear, page, fetchOptions);
+ if (CollectionUtils.isNotEmpty(matches)) return Optional.of(matches.get(0));
+ }
+
+ // Retry on parameter Id (=skip unit match)
+ if (ArrayUtils.isNotEmpty(filter.getLengthPmfmIds()) && ArrayUtils.isEmpty(filter.getLengthParameterIds())) {
+ // TODO loop on parameterIds then if result
+ }
+
+ // Not found
+ return Optional.empty();
+ }
+
@Override
public BigDecimal computedWeight(@NonNull WeightLengthConversionVO conversion,
@NonNull Number length,
- int scale,
- Number individualCount) {
+ @NonNull String lengthUnit,
+ @Nullable Number lengthPrecision,
+ @Nullable Number individualCount,
+ @NonNull String weightUnit,
+ int weightScale) {
+ BigDecimal lengthDecimal = new BigDecimal(length.toString());
+
+ // Convert length to expected unit
+ String conversionLengthUnit = conversion.getLengthUnit() != null
+ ? conversion.getLengthUnit().getLabel()
+ : UnitEnum.valueOf(conversion.getLengthUnitId()).getLabel();
+ if (!Objects.equals(conversionLengthUnit, lengthUnit)) {
+ // create a conversion factor
+ BigDecimal lengthUnitConversion = BigDecimal.valueOf(UnitConversions.lengthToMeterConversion(conversionLengthUnit))
+ .divide(new BigDecimal(UnitConversions.lengthToMeterConversion(lengthUnit)));
+
+ // Convert length to the expected unit
+ lengthDecimal = lengthDecimal.multiply(lengthUnitConversion);
+
+ // Round to half of the precision (see Allegro mantis #5598)
+ if (lengthPrecision != null) {
+ // length += 0.5 * precision * lengthUnitConversion
+ lengthDecimal = lengthDecimal.add(
+ new BigDecimal("0.5")
+ .multiply(new BigDecimal(lengthPrecision.toString()))
+ .multiply(lengthUnitConversion));
+ }
+ }
// CoefA * length ^ CoefB
- BigDecimal result = new BigDecimal(conversion.getConversionCoefficientA().toString())
- .multiply(new BigDecimal(
- Math.pow(length.doubleValue(), conversion.getConversionCoefficientB().doubleValue())
+ BigDecimal weightKg = BigDecimal.valueOf(conversion.getConversionCoefficientA())
+ .multiply(BigDecimal.valueOf(
+ Math.pow(lengthDecimal.doubleValue(), conversion.getConversionCoefficientB())
))
// * individual count
.multiply(new BigDecimal(individualCount != null ? individualCount.toString() : "1"));
- // Compute alive weight
-
- // Round to scale
- result = result.divide(new BigDecimal(1), scale, RoundingMode.HALF_UP);
+ // Convert to expected weight unit (kg by default)
+ if (!Objects.equals(weightUnit, "kg")) {
+ return weightKg.divide(
+ BigDecimal.valueOf(UnitConversions.weightToKgConversion(weightUnit)),
+ weightScale,
+ RoundingMode.HALF_UP
+ );
+ }
- return result;
+ // Round to expected weight scale
+ return weightKg.divide(new BigDecimal(1), weightScale, RoundingMode.HALF_UP);
}
@Override
+ @Caching(
+ evict = {
+ @CacheEvict(cacheNames = CacheConfiguration.Names.WEIGHT_LENGTH_CONVERSION_FIRST_BY_FILTER, allEntries = true),
+ @CacheEvict(cacheNames = CacheConfiguration.Names.WEIGHT_LENGTH_CONVERSION_IS_LENGTH_PARAMETER_ID, allEntries = true),
+ @CacheEvict(cacheNames = CacheConfiguration.Names.WEIGHT_LENGTH_CONVERSION_IS_LENGTH_PMFM_ID, allEntries = true)
+ }
+ )
public List saveAll(List sources) {
return sources.stream()
.map(weightLengthConversionRepository::save)
@@ -158,7 +255,30 @@ public List saveAll(List sou
}
@Override
+ @Caching(
+ evict = {
+ @CacheEvict(cacheNames = CacheConfiguration.Names.WEIGHT_LENGTH_CONVERSION_FIRST_BY_FILTER, allEntries = true),
+ @CacheEvict(cacheNames = CacheConfiguration.Names.WEIGHT_LENGTH_CONVERSION_IS_LENGTH_PARAMETER_ID, allEntries = true),
+ @CacheEvict(cacheNames = CacheConfiguration.Names.WEIGHT_LENGTH_CONVERSION_IS_LENGTH_PMFM_ID, allEntries = true)
+ }
+ )
public void deleteAllById(List ids) {
weightLengthConversionRepository.deleteAllById(ids);
}
+
+ @Override
+ @Cacheable(cacheNames = CacheConfiguration.Names.WEIGHT_LENGTH_CONVERSION_IS_LENGTH_PARAMETER_ID)
+ public boolean isWeightLengthParameter(int parameterId) {
+ return weightLengthConversionRepository.count(WeightLengthConversionFilterVO.builder()
+ .lengthParameterIds(new Integer[]{parameterId})
+ .build()) > 0;
+ }
+
+ @Override
+ @Cacheable(cacheNames = CacheConfiguration.Names.WEIGHT_LENGTH_CONVERSION_IS_LENGTH_PMFM_ID)
+ public boolean isWeightLengthPmfm(int pmfmId) {
+ return weightLengthConversionRepository.count(WeightLengthConversionFilterVO.builder()
+ .lengthPmfmIds(new Integer[]{pmfmId})
+ .build()) > 0;
+ }
}
diff --git a/sumaris-core/src/main/java/net/sumaris/core/service/technical/JobExecutionService.java b/sumaris-core/src/main/java/net/sumaris/core/service/technical/JobExecutionService.java
index c1ab07829f..c36c8de456 100644
--- a/sumaris-core/src/main/java/net/sumaris/core/service/technical/JobExecutionService.java
+++ b/sumaris-core/src/main/java/net/sumaris/core/service/technical/JobExecutionService.java
@@ -25,13 +25,19 @@
import io.reactivex.rxjava3.core.Observable;
import net.sumaris.core.event.job.JobProgressionVO;
+import net.sumaris.core.model.IProgressionModel;
import net.sumaris.core.vo.technical.job.JobVO;
+import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.function.Function;
public interface JobExecutionService {
- JobVO run(JobVO job, Function> asyncMethod);
+ JobVO run(JobVO job, Callable