Commit 43fdf3b2 authored by LAVENIER's avatar LAVENIER
Browse files

[enh] Allow to listen changes (GraphQL subscription) on program's strategies

parent ce7e4d72
......@@ -32,6 +32,7 @@ import net.sumaris.core.dao.cache.CacheNames;
import net.sumaris.core.dao.referential.ReferentialDao;
import net.sumaris.core.dao.referential.ReferentialRepositoryImpl;
import net.sumaris.core.dao.referential.taxon.TaxonGroupRepository;
import net.sumaris.core.dao.technical.jpa.BindableSpecification;
import net.sumaris.core.model.administration.programStrategy.*;
import net.sumaris.core.model.referential.Status;
import net.sumaris.core.model.referential.StatusEnum;
......@@ -54,6 +55,7 @@ import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import javax.persistence.EntityManager;
......@@ -83,6 +85,17 @@ public class ProgramRepositoryImpl
setLockForUpdate(true);
}
@Override
public Optional<ProgramVO> findIfNewerByLabel(String label, Date updateDate, ProgramFetchOptions fetchOptions) {
Program source = getQuery(BindableSpecification.where(hasLabel(label))
.and(newerThan(updateDate)), Program.class, Sort.by(Program.Fields.ID))
.getSingleResult();
if (source == null) return Optional.empty();
ProgramVO target = toVO(source, fetchOptions);
return Optional.of(target);
}
@Override
@Cacheable(cacheNames = CacheNames.PROGRAM_BY_ID)
public Optional<ProgramVO> findById(int id) {
......
......@@ -26,6 +26,7 @@ import net.sumaris.core.dao.technical.jpa.BindableSpecification;
import net.sumaris.core.model.administration.programStrategy.Program;
import net.sumaris.core.model.administration.programStrategy.ProgramPrivilegeEnum;
import net.sumaris.core.model.administration.programStrategy.ProgramProperty;
import net.sumaris.core.model.administration.programStrategy.Strategy;
import net.sumaris.core.vo.administration.programStrategy.ProgramFetchOptions;
import net.sumaris.core.vo.administration.programStrategy.ProgramVO;
import net.sumaris.core.vo.referential.ReferentialVO;
......@@ -34,7 +35,9 @@ import org.springframework.data.jpa.domain.Specification;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.ParameterExpression;
import java.util.Date;
import java.util.List;
import java.util.Optional;
/**
* @author peck7 on 24/08/2020.
......@@ -42,6 +45,7 @@ import java.util.List;
public interface ProgramSpecifications {
String PROPERTY_LABEL_PARAM = "propertyLabel";
String UPDATE_DATE_GREATER_THAN_PARAM = "updateDateGreaterThan";
default Specification<Program> hasProperty(String propertyLabel) {
BindableSpecification<Program> specification = BindableSpecification.where((root, query, criteriaBuilder) -> {
......@@ -55,6 +59,17 @@ public interface ProgramSpecifications {
return specification;
}
default Specification<Program> newerThan(Date updateDate) {
BindableSpecification<Program> specification = BindableSpecification.where((root, query, criteriaBuilder) -> {
ParameterExpression<Date> updateDateParam = criteriaBuilder.parameter(Date.class, UPDATE_DATE_GREATER_THAN_PARAM);
return criteriaBuilder.greaterThan(root.get(Program.Fields.UPDATE_DATE), updateDateParam);
});
specification.addBind(UPDATE_DATE_GREATER_THAN_PARAM, updateDate);
return specification;
}
Optional<ProgramVO> findIfNewerByLabel(String label, Date updateDate, ProgramFetchOptions fetchOptions);
ProgramVO toVO(Program source, ProgramFetchOptions fetchOptions);
List<TaxonGroupVO> getTaxonGroups(int programId);
......
......@@ -33,6 +33,7 @@ import net.sumaris.core.dao.referential.ReferentialRepositoryImpl;
import net.sumaris.core.dao.referential.location.LocationRepository;
import net.sumaris.core.dao.referential.pmfm.PmfmRepository;
import net.sumaris.core.dao.referential.taxon.TaxonNameRepository;
import net.sumaris.core.dao.technical.jpa.BindableSpecification;
import net.sumaris.core.exception.NotUniqueException;
import net.sumaris.core.exception.SumarisTechnicalException;
import net.sumaris.core.model.administration.programStrategy.*;
......@@ -272,6 +273,15 @@ public class StrategyRepositoryImpl
return result;
}
@Override
public List<StrategyVO> findNewerByProgramId(final int programId, final Date updateDate, final StrategyFetchOptions fetchOptions) {
return findAll(
BindableSpecification.where(hasProgramIds(programId))
.and(newerThan(updateDate))).stream()
.map(entity -> toVO(entity, fetchOptions))
.collect(Collectors.toList());
}
@Override
protected void onBeforeSaveEntity(StrategyVO vo, Strategy entity, boolean isNew) {
super.onBeforeSaveEntity(vo, entity, isNew);
......
......@@ -32,9 +32,11 @@ import net.sumaris.core.vo.referential.ReferentialVO;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.data.jpa.domain.Specification;
import javax.persistence.Parameter;
import javax.persistence.criteria.ParameterExpression;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;
/**
......@@ -44,9 +46,13 @@ public interface StrategySpecifications {
String PROGRAM_IDS_PARAM = "programIds";
String HAS_PROGRAM_PARAM = "hasProgram";
String UPDATE_DATE_GREATER_THAN_PARAM = "updateDateGreaterThan";
default Specification<Strategy> hasProgramIds(StrategyFilterVO filter) {
Integer[] programIds = filter.getProgramId() != null ? new Integer[]{filter.getProgramId()} : filter.getProgramIds();
return hasProgramIds(filter.getProgramId() != null ? new Integer[]{filter.getProgramId()} : filter.getProgramIds());
}
default Specification<Strategy> hasProgramIds(Integer... programIds) {
BindableSpecification<Strategy> specification = BindableSpecification.where((root, query, criteriaBuilder) -> {
ParameterExpression<Collection> programIdsParam = criteriaBuilder.parameter(Collection.class, PROGRAM_IDS_PARAM);
ParameterExpression<Boolean> hasProgramParam = criteriaBuilder.parameter(Boolean.class, HAS_PROGRAM_PARAM);
......@@ -60,6 +66,15 @@ public interface StrategySpecifications {
return specification;
}
default Specification<Strategy> newerThan(Date updateDate) {
BindableSpecification<Strategy> specification = BindableSpecification.where((root, query, criteriaBuilder) -> {
ParameterExpression<Date> updateDateParam = criteriaBuilder.parameter(Date.class, UPDATE_DATE_GREATER_THAN_PARAM);
return criteriaBuilder.greaterThan(root.get(Strategy.Fields.UPDATE_DATE), updateDateParam);
});
specification.addBind(UPDATE_DATE_GREATER_THAN_PARAM, updateDate);
return specification;
}
List<StrategyVO> saveByProgramId(int programId, List<StrategyVO> sources);
List<ReferentialVO> getGears(int strategyId);
......@@ -82,6 +97,8 @@ public interface StrategySpecifications {
String computeNextLabelByProgramId(int programId, String labelPrefix, int nbDigit);
List<StrategyVO> findNewerByProgramId(final int programId, final Date updateDate, final StrategyFetchOptions fetchOptions);
void saveProgramLocationsByStrategyId(int strategyId);
boolean hasUserPrivilege(int strategyId, int personId, ProgramPrivilegeEnum privilege);
......
......@@ -22,16 +22,19 @@ package net.sumaris.core.service;
* #L%
*/
import net.sumaris.core.dao.administration.programStrategy.ProgramRepository;
import net.sumaris.core.dao.administration.user.PersonRepository;
import net.sumaris.core.dao.data.landing.LandingRepository;
import net.sumaris.core.dao.data.observedLocation.ObservedLocationRepository;
import net.sumaris.core.dao.data.operation.OperationRepository;
import net.sumaris.core.dao.data.trip.TripRepository;
import net.sumaris.core.model.administration.programStrategy.Program;
import net.sumaris.core.model.administration.user.Person;
import net.sumaris.core.model.data.Landing;
import net.sumaris.core.model.data.ObservedLocation;
import net.sumaris.core.model.data.Operation;
import net.sumaris.core.model.data.Trip;
import net.sumaris.core.vo.administration.programStrategy.ProgramVO;
import net.sumaris.core.vo.administration.user.PersonVO;
import net.sumaris.core.vo.data.LandingVO;
import net.sumaris.core.vo.data.ObservedLocationVO;
......@@ -61,14 +64,25 @@ public class ConversionServiceImpl extends GenericConversionService {
@Autowired
private PersonRepository personRepository;
@Autowired
private ProgramRepository programRepository;
/**
* Add Entity->VO converters
*/
@PostConstruct
private void initConverters() {
// Entity->VO converters
// Referential (@see ReferentialServiceImpl)
// Administration
addConverter(Person.class, PersonVO.class, personRepository::toVO);
addConverter(Program.class, ProgramVO.class, programRepository::toVO);
// Data
addConverter(Trip.class, TripVO.class, tripRepository::toVO);
addConverter(ObservedLocation.class, ObservedLocationVO.class, observedLocationRepository::toVO);
addConverter(Operation.class, OperationVO.class, operationRepository::toVO);
addConverter(Landing.class, LandingVO.class, landingRepository::toVO);
addConverter(Person.class, PersonVO.class, personRepository::toVO);
}
}
......@@ -25,12 +25,13 @@ package net.sumaris.core.service.administration.programStrategy;
import net.sumaris.core.dao.technical.SortDirection;
import net.sumaris.core.model.administration.programStrategy.ProgramPrivilegeEnum;
import net.sumaris.core.vo.administration.programStrategy.ProgramSaveOptions;
import net.sumaris.core.vo.administration.programStrategy.ProgramVO;
import net.sumaris.core.vo.administration.programStrategy.*;
import net.sumaris.core.vo.filter.ProgramFilterVO;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;
import java.util.Optional;
/**
* @author BLA
......@@ -47,12 +48,17 @@ public interface ProgramService {
@Transactional(readOnly = true)
ProgramVO getByLabel(String label);
@Transactional(readOnly = true)
Optional<ProgramVO> findIfNewerByLabel(String label, final Date updateDate, ProgramFetchOptions fetchOptions);
@Transactional(readOnly = true)
List<ProgramVO> getAll();
@Transactional(readOnly = true)
List<ProgramVO> findByFilter(ProgramFilterVO filter, int offset, int size, String sortAttribute, SortDirection sortDirection);
ProgramVO save(ProgramVO program, ProgramSaveOptions options);
void delete(int id);
......
......@@ -28,15 +28,17 @@ import lombok.extern.slf4j.Slf4j;
import net.sumaris.core.dao.administration.programStrategy.ProgramRepository;
import net.sumaris.core.dao.technical.SortDirection;
import net.sumaris.core.model.administration.programStrategy.ProgramPrivilegeEnum;
import net.sumaris.core.vo.administration.programStrategy.ProgramFetchOptions;
import net.sumaris.core.vo.administration.programStrategy.ProgramSaveOptions;
import net.sumaris.core.vo.administration.programStrategy.ProgramVO;
import net.sumaris.core.vo.administration.programStrategy.StrategyVO;
import net.sumaris.core.vo.data.TripSaveOptions;
import net.sumaris.core.vo.filter.ProgramFilterVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
import java.util.Optional;
@Service("programService")
@Slf4j
......@@ -70,6 +72,13 @@ public class ProgramServiceImpl implements ProgramService {
return programRepository.getByLabel(label);
}
@Override
public Optional<ProgramVO> findIfNewerByLabel(String label, Date updateDate, ProgramFetchOptions fetchOptions) {
Preconditions.checkNotNull(label);
Preconditions.checkNotNull(updateDate);
return programRepository.findIfNewerByLabel(label, updateDate, fetchOptions);
}
@Override
public ProgramVO save(ProgramVO source, ProgramSaveOptions options) {
Preconditions.checkNotNull(source);
......@@ -100,5 +109,6 @@ public class ProgramServiceImpl implements ProgramService {
public boolean hasDepartmentPrivilege(int programId, int departmentId, ProgramPrivilegeEnum privilege) {
return programRepository.hasDepartmentPrivilege(programId, departmentId, privilege);
}
}
......@@ -31,6 +31,7 @@ import net.sumaris.core.vo.referential.ReferentialVO;
import org.springframework.data.domain.Pageable;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;
/**
......@@ -106,4 +107,6 @@ public interface StrategyService {
boolean hasDepartmentPrivilege(int strategyId, int departmentId, ProgramPrivilegeEnum privilege);
List<StrategyVO> findNewerByProgramId(final int programId, final Date updateDate, final StrategyFetchOptions fetchOptions);
}
......@@ -39,6 +39,7 @@ import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
......@@ -124,6 +125,11 @@ public class StrategyServiceImpl implements StrategyService {
return denormalizedPmfmStrategyRepository.findByStrategyId(strategyId, fetchOptions);
}
@Override
public List<StrategyVO> findNewerByProgramId(int programId, Date updateDate, StrategyFetchOptions fetchOptions) {
return strategyRepository.findNewerByProgramId(programId, updateDate, fetchOptions);
}
@Override
public List<ReferentialVO> getGears(int strategyId) {
return strategyRepository.getGears(strategyId);
......
......@@ -56,6 +56,8 @@ import net.sumaris.server.http.security.AuthService;
import net.sumaris.server.http.security.IsAdmin;
import net.sumaris.server.http.security.IsSupervisor;
import net.sumaris.server.http.security.IsUser;
import net.sumaris.server.service.technical.ChangesPublisherService;
import org.reactivestreams.Publisher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Service;
......@@ -88,6 +90,9 @@ public class ProgramGraphQLService {
@Autowired
private AuthService authService;
@Autowired
private ChangesPublisherService changesPublisherService;
@Autowired
public ProgramGraphQLService() {
super();
......@@ -288,6 +293,50 @@ public class ProgramGraphQLService {
nbDigit == null ? 0 : nbDigit);
}
@GraphQLSubscription(name = "updateProgram", description = "Subscribe to changes on a program")
@IsUser
public Publisher<ProgramVO> updateProgram(@GraphQLArgument(name = "id") final Integer id,
@GraphQLArgument(name = "label") final String label,
@GraphQLArgument(name = "interval", defaultValue = "30", description = "Minimum interval to find changes, in seconds.") final Integer minIntervalInSecond,
@GraphQLEnvironment() Set<String> fields) {
// Watch by id
if (id != null) {
Preconditions.checkArgument(id >= 0, "Invalid 'id' argument");
return changesPublisherService.getPublisher(Program.class, ProgramVO.class, id, minIntervalInSecond, true);
}
// Watch by label
Preconditions.checkNotNull(label, "Invalid 'label' argument");
ProgramFetchOptions fetchOptions = getProgramFetchOptions(fields);
return changesPublisherService.getPublisher((lastUpdateDate) -> programService.findIfNewerByLabel(label, lastUpdateDate, fetchOptions).orElse(null), minIntervalInSecond, true);
}
@GraphQLSubscription(name = "updateProgramStrategies", description = "Subscribe to changes on program's strategies")
@IsUser
public Publisher<List<StrategyVO>> updateProgramStrategies(@GraphQLNonNull @GraphQLArgument(name = "programId") final int programId,
@GraphQLArgument(name = "interval", defaultValue = "30", description = "Minimum interval to find changes, in seconds.") final Integer minIntervalInSecond,
@GraphQLEnvironment() Set<String> fields) {
StrategyFetchOptions fetchOptions = getStrategyFetchOptions(fields);
Preconditions.checkArgument(programId >= 0, "Invalid programId");
return changesPublisherService.getListPublisher((lastUpdateDate) -> {
if (lastUpdateDate == null) {
// Get all
return strategyService.findByProgram(programId, fetchOptions);
}
// Get newer strategies
return strategyService.findNewerByProgramId(programId, lastUpdateDate, fetchOptions);
}, minIntervalInSecond, false /*get only updates, not actual list*/);
}
/* -- Mutations -- */
@GraphQLMutation(name = "saveProgram", description = "Save a program (with strategies)")
......
package net.sumaris.server.http.graphql.data;
/*-
* #%L
* SUMARiS:: Server
* %%
* 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
* <http://www.gnu.org/licenses/gpl-3.0.html>.
* #L%
*/
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import io.leangen.graphql.annotations.*;
import net.sumaris.core.dao.data.RootDataRepository;
import net.sumaris.core.dao.data.trip.TripRepository;
import net.sumaris.core.exception.SumarisTechnicalException;
import net.sumaris.core.util.Beans;
import net.sumaris.core.vo.data.*;
import net.sumaris.server.http.security.IsSupervisor;
import net.sumaris.server.http.security.IsUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import java.util.*;
/**
* Tentative de créer un service générique de controle/validation
* => FIXME: Problème: le cache coté client doit etre mis à jour (updateDate) mais l'entité retournées n'est pas du type d'origine (TripVO, etc.)
*
*/
@Service
@Transactional
public class DataQualityGraphQLService {
/* Logger */
private static final Logger log = LoggerFactory.getLogger(DataQualityGraphQLService.class);
@Autowired
private TripRepository tripRepository;
private Map<String, RootDataRepository<?, ? extends IRootDataVO<Integer>, ?, ?>> serviceByEntityNameMap;
private Map<String, Class<? extends IRootDataVO<Integer>>> voTypeByEntityNameMap;
private Map<String, Function<DataReferenceVO, ? extends IRootDataVO<Integer>>> conversionMap;
@PostConstruct
protected void init() {
serviceByEntityNameMap = ImmutableMap.of(
TripVO.TYPENAME, tripRepository
);
voTypeByEntityNameMap = ImmutableMap.of(
TripVO.TYPENAME, TripVO.class
);
}
@GraphQLMutation(name = "control", description = "Control a data, by reference")
@IsUser
public DataReferenceVO control(@GraphQLArgument(name = "reference") DataReferenceVO reference) {
checkReference(reference);
Date updateDate = getDataService(reference.getEntityName())
.control(reference.getId(), reference.getUpdateDate());
reference.setUpdateDate(updateDate);
return reference;
}
/*@GraphQLMutation(name = "validate", description = "Validate a data, by reference")
@IsSupervisor
public DataReferenceVO validate(@GraphQLArgument(name = "reference") DataReferenceVO ref) {
checkReference(ref);
Date updateDate = getDataService(ref.getEntityName())
.validate(ref.getId(), ref.getUpdateDate());
ref.setUpdateDate(updateDate);
return ref;
}
@GraphQLMutation(name = "unvalidate", description = "Unvalidate a data, by reference")
@IsSupervisor
public DataReferenceVO unvalidate(@GraphQLArgument(name = "reference") DataReferenceVO ref) {
checkReference(ref);
Date updateDate = getDataService(ref.getEntityName())
.unValidate(ref.getId(), ref.getUpdateDate());
ref.setUpdateDate(updateDate);
return ref;
}
@GraphQLMutation(name = "qualify", description = "Qualify a data, by reference")
@IsSupervisor
public DataReferenceVO qualify(@GraphQLArgument(name = "reference") DataReferenceVO ref) {
checkReference(ref);
Date updateDate = getDataService(ref.getEntityName())
.qualify(ref.getId(), ref.getUpdateDate());
ref.setUpdateDate(updateDate);
return ref;
}*/
/* -- protected -- */
protected RootDataRepository<?, ? extends IRootDataVO<Integer>, ?, ?> getDataService(String entityName) {
RootDataRepository service = serviceByEntityNameMap.get(entityName);
Preconditions.checkNotNull(service, String.format("EntityName '%s' is not a root entity", entityName));
return service;
}
protected IRootDataVO<Integer> toVO(DataReferenceVO source) {
checkReference(source);
String entityName = source.getEntityName();
Class<? extends IRootDataVO<Integer>> clazz = voTypeByEntityNameMap.get(entityName);
Preconditions.checkNotNull(clazz, String.format("EntityName '%s' is not a root entity", entityName));
try {
IRootDataVO<Integer> target = clazz.newInstance();
Beans.copyProperties(source, target);
return target;
}
catch (Exception e) {
throw new SumarisTechnicalException("Unable to create VO of type: " + clazz.getCanonicalName(), e);
}
}
protected void checkReference(DataReferenceVO data) {
Preconditions.checkNotNull(data);
Preconditions.checkNotNull(data.getId());
Preconditions.checkNotNull(data.getUpdateDate());
Preconditions.checkNotNull(data.getEntityName());
}
}
......@@ -28,10 +28,25 @@ import org.springframework.transaction.annotation.Transactional;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
@Transactional
public interface ChangesPublisherService {
@Transactional(readOnly = true)
<K extends Serializable, D extends Date, V extends IUpdateDateEntityBean<K, D>, L extends List<V>> Publisher<L>
getListPublisher(final Function<Date, L> supplier,
Integer minIntervalInSecond,
boolean startWithActualValue);
@Transactional(readOnly = true)
<K extends Serializable, D extends Date, V extends IUpdateDateEntityBean<K, D>> Publisher<V>
getPublisher(final Function<Date, V> supplier,
Integer minIntervalInSecond,
boolean startWithActualValue);
@Transactional(readOnly = true)
<K extends Serializable, D extends Date, T extends IUpdateDateEntityBean<K, D>, V extends IUpdateDateEntityBean<K, D>> Publisher<V>
getPublisher(Class<T> entityClass,
......