Commit 9c615d32 authored by calocedre TAC's avatar calocedre TAC
Browse files

Merge branch 'feature/clea/scoring-conf' into 'develop'

Implementation of the scoring configuration module

See merge request backend-server!214
parents 7dd91ad8 86b2ca73
......@@ -63,6 +63,12 @@
<artifactId>spring-cloud-vault-config-consul</artifactId>
</dependency>
<dependency>
<groupId>${project.parent.groupId}</groupId>
<artifactId>clea-scoring-conf</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
......
......@@ -4,13 +4,17 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import javax.annotation.PostConstruct;
@Getter
@Setter
@ToString
@NoArgsConstructor
@ConfigurationProperties(prefix = "clea.batch.cluster")
@Slf4j
public class BatchProperties {
/**
......@@ -29,4 +33,9 @@ public class BatchProperties {
private int indexationStepChunkSize;
private int prefixesComputingStepChunkSize;
@PostConstruct
private void logConfiguration() {
log.info(this.toString());
}
}
package fr.gouv.clea.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
......@@ -9,7 +8,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
public class CleaBatchJobConfig {
......
package fr.gouv.clea.config;
import fr.gouv.clea.dto.SinglePlaceCluster;
import fr.gouv.clea.dto.SinglePlaceClusterPeriod;
import fr.gouv.clea.dto.SinglePlaceExposedVisits;
import fr.gouv.clea.identification.ExposedVisitRowMapper;
import fr.gouv.clea.identification.RiskConfigurationService;
import fr.gouv.clea.identification.processor.ClusterToPeriodsProcessor;
import fr.gouv.clea.identification.processor.SinglePlaceExposedVisitsBuilder;
import fr.gouv.clea.identification.processor.SinglePlaceExposedVisitsProcessor;
import fr.gouv.clea.identification.writer.SinglePlaceClusterPeriodListWriter;
import fr.gouv.clea.mapper.ClusterPeriodModelsMapper;
import static fr.gouv.clea.config.BatchConstants.SQL_SELECT_DISTINCT_LTID_FROM_EXPOSEDVISITS;
import java.util.List;
import javax.sql.DataSource;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.item.ItemProcessor;
......@@ -24,10 +20,16 @@ import org.springframework.core.task.TaskExecutor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import javax.sql.DataSource;
import java.util.List;
import static fr.gouv.clea.config.BatchConstants.SQL_SELECT_DISTINCT_LTID_FROM_EXPOSEDVISITS;
import fr.gouv.clea.dto.SinglePlaceCluster;
import fr.gouv.clea.dto.SinglePlaceClusterPeriod;
import fr.gouv.clea.dto.SinglePlaceExposedVisits;
import fr.gouv.clea.identification.ExposedVisitRowMapper;
import fr.gouv.clea.identification.processor.ClusterToPeriodsProcessor;
import fr.gouv.clea.identification.processor.SinglePlaceExposedVisitsBuilder;
import fr.gouv.clea.identification.processor.SinglePlaceExposedVisitsProcessor;
import fr.gouv.clea.identification.writer.SinglePlaceClusterPeriodListWriter;
import fr.gouv.clea.mapper.ClusterPeriodModelsMapper;
import fr.gouv.clea.scoring.configuration.risk.RiskConfiguration;
@Configuration
public class IdentificationStepBatchConfig {
......@@ -45,7 +47,7 @@ public class IdentificationStepBatchConfig {
private ClusterPeriodModelsMapper mapper;
@Autowired
private RiskConfigurationService riskConfigurationService;
private RiskConfiguration riskConfig;
@Bean
public Step clusterIdentification() {
......@@ -85,7 +87,7 @@ public class IdentificationStepBatchConfig {
@Bean
public ItemProcessor<SinglePlaceExposedVisits, SinglePlaceCluster> singleClusterPlaceBuilder() {
return new SinglePlaceExposedVisitsProcessor(properties, riskConfigurationService);
return new SinglePlaceExposedVisitsProcessor(properties, riskConfig);
}
@Bean
......
package fr.gouv.clea.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import fr.gouv.clea.indexation.IndexationPartitioner;
import fr.gouv.clea.indexation.model.output.ClusterFile;
import fr.gouv.clea.indexation.processor.SinglePlaceClusterBuilder;
import fr.gouv.clea.indexation.reader.StepExecutionContextReader;
import fr.gouv.clea.indexation.writer.IndexationWriter;
import fr.gouv.clea.mapper.ClusterPeriodModelsMapper;
import fr.gouv.clea.service.PrefixesStorageService;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.Map;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepScope;
......@@ -23,11 +17,16 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
import fr.gouv.clea.indexation.IndexationPartitioner;
import fr.gouv.clea.indexation.model.output.ClusterFile;
import fr.gouv.clea.indexation.processor.SinglePlaceClusterBuilder;
import fr.gouv.clea.indexation.reader.StepExecutionContextReader;
import fr.gouv.clea.indexation.writer.IndexationWriter;
import fr.gouv.clea.mapper.ClusterPeriodModelsMapper;
import fr.gouv.clea.service.PrefixesStorageService;
@Slf4j
@Configuration
public class IndexationStepBatchConfig {
......
package fr.gouv.clea.identification;
import org.springframework.stereotype.Component;
import java.util.Optional;
/**
*
* Mock implementation that return same value for any venue type/categories
*
*/
@Component
public class RiskConfigurationService {
static final RiskLevelConfig DEFAULT=new RiskLevelConfig(3, 1, 3.0f, 2.0f);
public Optional<RiskLevelConfig> evaluate(int venueType, int venueCategory1, int venueCategory2) {
return Optional.of(DEFAULT);
}
}
package fr.gouv.clea.identification;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* Read-only configuration risk for one {type/categ1/categ2} visit
*/
@AllArgsConstructor
@Getter
public class RiskLevelConfig {
private int backwardTheshold;
private int forwardTheshold;
private float backwardRisk;
private float forwardRisk;
}
package fr.gouv.clea.identification.processor;
import org.springframework.batch.item.ItemProcessor;
import fr.gouv.clea.config.BatchProperties;
import fr.gouv.clea.dto.ClusterPeriod;
import fr.gouv.clea.dto.SinglePlaceCluster;
import fr.gouv.clea.dto.SinglePlaceExposedVisits;
import fr.gouv.clea.entity.ExposedVisit;
import fr.gouv.clea.identification.RiskConfigurationService;
import fr.gouv.clea.identification.RiskLevelConfig;
import fr.gouv.clea.scoring.configuration.risk.RiskConfiguration;
import fr.gouv.clea.scoring.configuration.risk.RiskRule;
import fr.gouv.clea.utils.ExposedVisitComparator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.item.ItemProcessor;
import java.util.Optional;
@Slf4j
public class SinglePlaceExposedVisitsProcessor implements ItemProcessor<SinglePlaceExposedVisits, SinglePlaceCluster> {
private final BatchProperties properties;
private final RiskConfigurationService riskConfigurationService;
private final RiskConfiguration riskConfiguration;
public SinglePlaceExposedVisitsProcessor(BatchProperties properties, RiskConfigurationService riskConfigurationService) {
public SinglePlaceExposedVisitsProcessor(BatchProperties properties, RiskConfiguration riskConfiguration) {
this.properties = properties;
this.riskConfigurationService = riskConfigurationService;
this.riskConfiguration = riskConfiguration;
}
@Override
......@@ -35,13 +32,7 @@ public class SinglePlaceExposedVisitsProcessor implements ItemProcessor<SinglePl
ClusterPeriod backPeriod = null;
ClusterPeriod forwardPeriod = null;
Optional<RiskLevelConfig> riskLevelConfigOptional = riskConfigurationService.evaluate(cluster.getVenueType(), cluster.getVenueCategory1(), cluster.getVenueCategory2());
if (riskLevelConfigOptional.isEmpty()) {
log.warn("No Risk configuration for [type={},categ1={},categ2={}]", cluster.getVenueType(), cluster.getVenueCategory1(), cluster.getVenueCategory2());
return null;
}
final RiskLevelConfig riskLevelConfig = riskLevelConfigOptional.get();
final RiskRule riskRule = riskConfiguration.getConfigurationFor(cluster.getVenueType(), cluster.getVenueCategory1(), cluster.getVenueCategory2());
// Sorted visits by period then slot
record.getVisits().sort(new ExposedVisitComparator());
......@@ -49,10 +40,10 @@ public class SinglePlaceExposedVisitsProcessor implements ItemProcessor<SinglePl
for (ExposedVisit visit : record.getVisits()) {
// Backward
backPeriod = processVisit(visit, cluster, backPeriod, visit.getBackwardVisits(),
riskLevelConfig.getBackwardTheshold(), riskLevelConfig.getBackwardRisk());
riskRule.getClusterThresholdBackward(), riskRule.getRiskLevelBackward());
// Forward
forwardPeriod = processVisit(visit, cluster, forwardPeriod, visit.getForwardVisits(),
riskLevelConfig.getForwardTheshold(), riskLevelConfig.getForwardRisk());
riskRule.getClusterThresholdForward(), riskRule.getRiskLevelForward());
}
// Finalize last periods after the loop
......
package fr.gouv.clea.identification.writer;
import fr.gouv.clea.dto.SinglePlaceClusterPeriod;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import static fr.gouv.clea.config.BatchConstants.CLUSTER_DURATION_COL;
import static fr.gouv.clea.config.BatchConstants.CLUSTER_START_COL;
import static fr.gouv.clea.config.BatchConstants.FIRST_TIMESLOT_COL;
import static fr.gouv.clea.config.BatchConstants.LAST_TIMESLOT_COL;
import static fr.gouv.clea.config.BatchConstants.LTID_COL;
import static fr.gouv.clea.config.BatchConstants.PERIOD_START_COL;
import static fr.gouv.clea.config.BatchConstants.RISK_LEVEL_COL;
import static fr.gouv.clea.config.BatchConstants.SINGLE_PLACE_CLUSTER_PERIOD_TABLE;
import static fr.gouv.clea.config.BatchConstants.VENUE_CAT1_COL;
import static fr.gouv.clea.config.BatchConstants.VENUE_CAT2_COL;
import static fr.gouv.clea.config.BatchConstants.VENUE_TYPE_COL;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.batch.item.ItemWriter;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils;
import java.util.List;
import java.util.stream.Collectors;
import static fr.gouv.clea.config.BatchConstants.*;
import fr.gouv.clea.dto.SinglePlaceClusterPeriod;
import lombok.RequiredArgsConstructor;
@Slf4j
@RequiredArgsConstructor
public class SinglePlaceClusterPeriodListWriter implements ItemWriter<List<SinglePlaceClusterPeriod>> {
......
package fr.gouv.clea.indexation.reader;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.item.ItemReader;
import java.util.AbstractMap;
import java.util.List;
import java.util.Map;
@Slf4j
import org.springframework.batch.item.ItemReader;
import lombok.Getter;
public class StepExecutionContextReader implements ItemReader<Map.Entry<String, List<String>>> {
@Getter
......
package fr.gouv.clea.prefixes;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import org.springframework.batch.item.ExecutionContext;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemStream;
import org.springframework.batch.item.ItemStreamException;
import org.springframework.batch.item.database.JdbcCursorItemReader;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
@Slf4j
@RequiredArgsConstructor
public class ListItemReader implements ItemReader<List<String>>, ItemStream {
......
......@@ -6,7 +6,6 @@ spring:
jpa:
hibernate.ddl-auto: none
database-platform: org.hibernate.dialect.PostgreSQL95Dialect # even with h2 database
batch:
# Manage it's batch metadata tables
initialize-schema: never
#prevent job to launch on startup
......@@ -21,6 +20,14 @@ spring:
indent-output: true
clea:
conf:
exposure:
enabled: "false"
risk:
enabled: "true"
rules: # FIXME: change with real values
# venueType, venueCat1, venueCat2, clusterThresholdBackward, clusterThresholdForward, riskLevelBackward, riskLevelForward
- '*,*,*,3,1,3.0,2.0'
batch:
cluster:
duration-unit-in-seconds: 1800
......
package fr.gouv.clea.identification.processor;
import fr.gouv.clea.dto.ClusterPeriod;
import fr.gouv.clea.dto.SinglePlaceCluster;
import fr.gouv.clea.dto.SinglePlaceClusterPeriod;
import fr.gouv.clea.mapper.ClusterPeriodModelsMapper;
import fr.gouv.clea.mapper.ClusterPeriodModelsMapperImpl;
import org.junit.jupiter.api.Assertions;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import java.util.UUID;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import java.util.List;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import fr.gouv.clea.dto.ClusterPeriod;
import fr.gouv.clea.dto.SinglePlaceCluster;
import fr.gouv.clea.dto.SinglePlaceClusterPeriod;
import fr.gouv.clea.mapper.ClusterPeriodModelsMapper;
import fr.gouv.clea.mapper.ClusterPeriodModelsMapperImpl;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@ExtendWith(MockitoExtension.class)
......
package fr.gouv.clea.identification.processor;
import fr.gouv.clea.config.BatchProperties;
import fr.gouv.clea.dto.ClusterPeriod;
import fr.gouv.clea.dto.SinglePlaceCluster;
import fr.gouv.clea.dto.SinglePlaceExposedVisits;
import fr.gouv.clea.entity.ExposedVisit;
import fr.gouv.clea.identification.RiskConfigurationService;
import fr.gouv.clea.identification.RiskLevelConfig;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import java.util.UUID;
import org.assertj.core.data.Offset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import fr.gouv.clea.config.BatchProperties;
import fr.gouv.clea.dto.ClusterPeriod;
import fr.gouv.clea.dto.SinglePlaceCluster;
import fr.gouv.clea.dto.SinglePlaceExposedVisits;
import fr.gouv.clea.entity.ExposedVisit;
import fr.gouv.clea.scoring.configuration.ScoringRule;
import fr.gouv.clea.scoring.configuration.risk.RiskConfiguration;
import fr.gouv.clea.scoring.configuration.risk.RiskRule;
@ExtendWith(MockitoExtension.class)
class SinglePlaceExposedVisitsProcessorTest {
private BatchProperties properties = new BatchProperties();
RiskConfigurationService eval = new RiskConfigurationService();
RiskConfiguration riskConfig = new RiskConfiguration();
public SinglePlaceExposedVisitsProcessorTest() {
properties.setDurationUnitInSeconds(180);
riskConfig.setRules(List.of(
RiskRule.builder()
.venueType(ScoringRule.WILDCARD_VALUE)
.venueCategory1(ScoringRule.WILDCARD_VALUE)
.venueCategory2(ScoringRule.WILDCARD_VALUE)
.clusterThresholdBackward(3)
.clusterThresholdForward(1)
.riskLevelBackward(3.0f)
.riskLevelForward(2.0f)
.build()
));
}
private final UUID UUID_SAMPLE = UUID.fromString("fa35fa88-2c44-4f13-9ec9-d38e77324c93");
......@@ -38,7 +51,7 @@ class SinglePlaceExposedVisitsProcessorTest {
spe.setVenueCategory1(3);
spe.setVenueCategory2(2);
SinglePlaceCluster res = new SinglePlaceExposedVisitsProcessor(properties,eval).process(spe);
SinglePlaceCluster res = new SinglePlaceExposedVisitsProcessor(properties, riskConfig).process(spe);
assertThat(res).isNull();
}
......@@ -53,7 +66,7 @@ class SinglePlaceExposedVisitsProcessorTest {
spe.addVisit(ExposedVisit.builder().periodStart(periodStart).timeSlot(0).forwardVisits(0).build());
spe.addVisit(ExposedVisit.builder().periodStart(periodStart).timeSlot(1).forwardVisits(0).build());
SinglePlaceCluster res = new SinglePlaceExposedVisitsProcessor(properties,eval).process(spe);
SinglePlaceCluster res = new SinglePlaceExposedVisitsProcessor(properties,riskConfig).process(spe);
assertThat(res).isNull();
}
......@@ -72,7 +85,7 @@ class SinglePlaceExposedVisitsProcessorTest {
spe.addVisit(ExposedVisit.builder().periodStart(periodStart).timeSlot(4).forwardVisits(0).build());
SinglePlaceCluster res = new SinglePlaceExposedVisitsProcessor(properties,eval).process(spe);
SinglePlaceCluster res = new SinglePlaceExposedVisitsProcessor(properties,riskConfig).process(spe);
assertThat(res).isNotNull();
assertThat(res.getPeriods()).hasSize(1);
......@@ -99,7 +112,7 @@ class SinglePlaceExposedVisitsProcessorTest {
spe.addVisit(ExposedVisit.builder().periodStart(periodStart).timeSlot(2).forwardVisits(0).build());
spe.addVisit(ExposedVisit.builder().periodStart(anotherPeriodStart).timeSlot(0).forwardVisits(1).build());
SinglePlaceCluster res = new SinglePlaceExposedVisitsProcessor(properties,eval).process(spe);
SinglePlaceCluster res = new SinglePlaceExposedVisitsProcessor(properties,riskConfig).process(spe);
assertThat(res).isNotNull();
assertThat(res.getPeriods()).hasSize(2);
......@@ -123,13 +136,13 @@ class SinglePlaceExposedVisitsProcessorTest {
spe.setVenueCategory2(2);
spe.addVisit(ExposedVisit.builder().periodStart(periodStart).timeSlot(0).forwardVisits(100).build());
SinglePlaceCluster res = new SinglePlaceExposedVisitsProcessor(properties,eval).process(spe);
SinglePlaceCluster res = new SinglePlaceExposedVisitsProcessor(properties,riskConfig).process(spe);
assertThat(res).isNotNull();
assertThat(res.getPeriods()).hasSize(1);
ClusterPeriod p = res.getPeriods().get(0);
Optional<RiskLevelConfig> riskLevelEvaluation = eval.evaluate(spe.getVenueType(), spe.getVenueCategory1(), spe.getVenueCategory2());
riskLevelEvaluation.ifPresent(evaluatedRiskLevel -> assertThat(p.getRiskLevel()).as("riskLevel").isCloseTo(evaluatedRiskLevel.getForwardRisk(), Offset.offset(0.01f)));
RiskRule evaluatedRiskLevel = riskConfig.getConfigurationFor(spe.getVenueType(), spe.getVenueCategory1(), spe.getVenueCategory2());
assertThat(p.getRiskLevel()).as("riskLevel").isCloseTo(evaluatedRiskLevel.getRiskLevelForward(), Offset.offset(0.01f));
}
@Test
......@@ -141,12 +154,12 @@ class SinglePlaceExposedVisitsProcessorTest {
spe.setVenueCategory2(2);
spe.addVisit(ExposedVisit.builder().periodStart(periodStart).timeSlot(0).backwardVisits(100).build());
SinglePlaceCluster res = new SinglePlaceExposedVisitsProcessor(properties,eval).process(spe);
SinglePlaceCluster res = new SinglePlaceExposedVisitsProcessor(properties,riskConfig).process(spe);
assertThat(res).isNotNull();
assertThat(res.getPeriods()).hasSize(1);
ClusterPeriod p = res.getPeriods().get(0);
Optional<RiskLevelConfig> riskLevelEvaluation = eval.evaluate(spe.getVenueType(), spe.getVenueCategory1(), spe.getVenueCategory2());
riskLevelEvaluation.ifPresent(evaluatedRiskLevel -> assertThat(p.getRiskLevel()).as("riskLevel").isCloseTo(evaluatedRiskLevel.getBackwardRisk(), Offset.offset(0.01f)));
RiskRule evaluatedRiskLevel = riskConfig.getConfigurationFor(spe.getVenueType(), spe.getVenueCategory1(), spe.getVenueCategory2());
assertThat(p.getRiskLevel()).as("riskLevel").isCloseTo(evaluatedRiskLevel.getRiskLevelBackward(), Offset.offset(0.01f));
}
}
package fr.gouv.clea.indexation.reader;
import org.assertj.core.data.MapEntry;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
......
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>fr.gouv.clea</groupId>
<artifactId>clea-server</artifactId>
<version>0.1-SNAPSHOT</version>
</parent>
<artifactId>clea-scoring-conf</artifactId>
<name>clea-scoring-conf</name>
<description>Configuration of the cluster detection scoring</description>
<packaging>jar</packaging>
<properties>
<java.version>11</java.version>
</properties>