Commit 3ca8aac0 authored by LAVENIER's avatar LAVENIER
Browse files

Merge branch 'feature/imagine' into develop

parents 4c2190ad ccc69aed
......@@ -36,9 +36,9 @@ public enum ExtractionWebConfigurationOption implements ConfigOptionDef {
String.class,
false),
AUTH_ROLE_NOT_SELF_EXTRACTION_ACCESS(
"sumaris.auth.notSelfExtractionAccess.role",
n("sumaris.config.option.auth.allExtractionTypeAccess.role.description"),
ACCESS_NOT_SELF_EXTRACTION_MIN_ROLE(
"sumaris.extraction.accessNotSelfExtraction.role",
n("sumaris.config.option.extraction.accessNotSelfExtraction.role.description"),
"ROLE_ADMIN", // Possible values: ROLE_GUEST, ROLE_USER, ROLE_SUPERVISOR, ROLE_ADMIN
String.class,
false),
......
......@@ -67,17 +67,17 @@ public class ExtractionSecurityServiceImpl implements ExtractionSecurityService
@Autowired
protected IAuthService<PersonVO> authService;
private String minRoleForNotSelfDataAccess;
private String accessNotSelfExtractionMinRole;
@EventListener({ConfigurationReadyEvent.class, ConfigurationUpdatedEvent.class})
protected void onConfigurationReady(ConfigurationEvent event) {
this.minRoleForNotSelfDataAccess = configuration.getApplicationConfig().getOption(ExtractionWebConfigurationOption.AUTH_ROLE_NOT_SELF_EXTRACTION_ACCESS.getKey());
this.accessNotSelfExtractionMinRole = configuration.getApplicationConfig().getOption(ExtractionWebConfigurationOption.ACCESS_NOT_SELF_EXTRACTION_MIN_ROLE.getKey());
}
@Override
public boolean canReadAll() {
return (StringUtils.isNotBlank(minRoleForNotSelfDataAccess)
&& authService.hasAuthority(minRoleForNotSelfDataAccess))
return (StringUtils.isNotBlank(accessNotSelfExtractionMinRole)
&& authService.hasAuthority(accessNotSelfExtractionMinRole))
|| authService.isAdmin();
}
......
......@@ -27,6 +27,8 @@ package net.sumaris.core.config;
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.ImmutableSet;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
......@@ -74,6 +76,11 @@ public class SumarisConfiguration extends PropertyPlaceholderConfigurer {
*/
protected final ApplicationConfig applicationConfig;
/**
* Cache for complexe options (e.g. for list of values, to avoid many call of split())
*/
protected final Cache<String, Object> complexOptionsCache = CacheBuilder.newBuilder().build();
private static SumarisConfiguration instance;
/**
......@@ -192,6 +199,10 @@ public class SumarisConfiguration extends PropertyPlaceholderConfigurer {
}
public void cleanCache() {
complexOptionsCache.invalidateAll();
}
/**
* Add alias to the given ApplicationConfig. <p/>
* This method could be override to add specific alias
......
......@@ -123,7 +123,7 @@ public class Application {
}
}
@Bean
@Bean("configuration")
public SumarisConfiguration configuration(ConfigurableEnvironment env) {
SumarisConfiguration config = SumarisConfiguration.getInstance();
......
......@@ -52,7 +52,7 @@ public interface StrategySpecifications extends ReferentialSpecifications<Strate
String REFERENCE_TAXON_IDS = "referenceTaxonIds";
String DEPARTMENT_IDS = "departmentIds";
String LOCATION_IDS = "locationIds";
String PARAMETER_IDS = "pmfmIds";
String PARAMETER_IDS = "parameterIds";
String UPDATE_DATE_GREATER_THAN_PARAM = "updateDateGreaterThan";
default Specification<Strategy> hasProgramIds(Integer... programIds) {
......@@ -168,13 +168,22 @@ public interface StrategySpecifications extends ReferentialSpecifications<Strate
// Avoid duplicated entries (because of inner join)
query.distinct(true);
Join<Strategy, PmfmStrategy> pmfmsInnerJoin = root.joinList(Strategy.Fields.PMFMS, JoinType.INNER);
ParameterExpression<Collection> parameter = criteriaBuilder.parameter(Collection.class, PARAMETER_IDS);
return criteriaBuilder.in(
root.join(Strategy.Fields.PMFMS, JoinType.INNER)
.join(PmfmStrategy.Fields.PMFM, JoinType.INNER)
.join(Pmfm.Fields.PARAMETER, JoinType.INNER)
.get(Parameter.Fields.ID))
.value(parameter);
return criteriaBuilder.or(
criteriaBuilder.in(
pmfmsInnerJoin
.join(PmfmStrategy.Fields.PMFM, JoinType.LEFT)
.join(Pmfm.Fields.PARAMETER, JoinType.LEFT)
.get(Parameter.Fields.ID))
.value(parameter),
criteriaBuilder.in(
pmfmsInnerJoin
.join(PmfmStrategy.Fields.PARAMETER, JoinType.LEFT)
.get(Parameter.Fields.ID))
.value(parameter)
);
});
specification.addBind(PARAMETER_IDS, Arrays.asList(parameterIds));
return specification;
......
......@@ -65,7 +65,8 @@ public class ObservedLocationRepositoryImpl
return super.toSpecification(filter, fetchOptions)
.and(hasLocationId(filter.getLocationId()))
.and(withStartDate(filter.getStartDate()))
.and(withEndDate(filter.getEndDate()));
.and(withEndDate(filter.getEndDate()))
.and(hasObserverPersonIds(filter.getObserverPersonIds()));
}
@Override
......
......@@ -26,9 +26,13 @@ import net.sumaris.core.dao.data.RootDataSpecifications;
import net.sumaris.core.dao.technical.jpa.BindableSpecification;
import net.sumaris.core.dao.technical.model.IEntity;
import net.sumaris.core.model.data.ObservedLocation;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.data.jpa.domain.Specification;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.ParameterExpression;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
/**
......@@ -39,6 +43,7 @@ public interface ObservedLocationSpecifications extends RootDataSpecifications<O
String LOCATION_ID_PARAM = "locationId";
String START_DATE_PARAM = "startDate";
String END_DATE_PARAM = "endDate";
String OBSERVER_PERSON_IDS_PARAM = "observerPersonIds";
default Specification<ObservedLocation> hasLocationId(Integer locationId) {
BindableSpecification<ObservedLocation> specification = BindableSpecification.where((root, query, criteriaBuilder) -> {
......@@ -76,5 +81,22 @@ public interface ObservedLocationSpecifications extends RootDataSpecifications<O
return specification;
}
default Specification<ObservedLocation> hasObserverPersonIds(Integer... observerPersonIds) {
if (ArrayUtils.isEmpty(observerPersonIds)) return null;
BindableSpecification<ObservedLocation> specification = BindableSpecification.where((root, query, criteriaBuilder) -> {
// Avoid duplicated entries (because of inner join)
query.distinct(true);
ParameterExpression<Collection> parameter = criteriaBuilder.parameter(Collection.class, OBSERVER_PERSON_IDS_PARAM);
return criteriaBuilder.in(
root.join(ObservedLocation.Fields.OBSERVERS, JoinType.INNER)
.get(IEntity.Fields.ID))
.value(parameter);
});
specification.addBind(OBSERVER_PERSON_IDS_PARAM, Arrays.asList(observerPersonIds));
return specification;
}
}
......@@ -181,7 +181,29 @@ public class ReferentialExternalDaoImpl implements ReferentialExternalDao {
&& (filter.getLevelId() == null || filter.getLevelId().equals(s.getLevelId()))
&& (filter.getLevelIds() == null || Arrays.asList(filter.getLevelIds()).contains(s.getLevelId()))
&& (filter.getStatusIds() == null || Arrays.asList(filter.getStatusIds()).contains(s.getStatusId()))
&& (filter.getSearchText() == null || s.getLabel().toUpperCase().contains(filter.getSearchText().toUpperCase()) || s.getName().toUpperCase().contains(filter.getSearchText().toUpperCase()));
&& (filter.getSearchText() == null || likeIgnoreCase(s.getLabel(), filter.getSearchText(),false) || likeIgnoreCase(s.getName(), filter.getSearchText(),true));
}
private static boolean likeIgnoreCase(String text, String searchText, boolean searchAny) {
if (StringUtils.isEmpty(text) || StringUtils.isEmpty(searchText)) return false;
return like(text.toLowerCase(), searchText.toLowerCase(), searchAny);
}
private static boolean like(String text, String searchText, boolean searchAny) {
if (StringUtils.isEmpty(text) || StringUtils.isEmpty(searchText)) return false;
// add leading wildcard (if searchAny specified) and trailing wildcard
searchText = ((searchAny ? "*" : "") + searchText + "*");
if (searchText.startsWith("*") && searchText.endsWith("*")) {
return text.contains(searchText.replace("*", ""));
} else if (searchText.startsWith("*")) {
return text.endsWith(searchText.replace("*", ""));
} else if (searchText.endsWith("*")) {
return text.startsWith(searchText.replace("*", ""));
} else {
return text.equals(searchText.replace("*", ""));
}
}
}
......@@ -32,7 +32,7 @@ public interface ConfigurationService {
boolean isReady();
void updateConfigFromSoftwareProperties();
void applySoftwareProperties();
}
......@@ -126,7 +126,7 @@ public class ConfigurationServiceImpl implements ConfigurationService {
else {
// Update the config, from the software properties
updateConfigFromSoftwareProperties();
applySoftwareProperties();
// Publish ready event
if (event instanceof SchemaReadyEvent) {
......@@ -161,7 +161,10 @@ public class ConfigurationServiceImpl implements ConfigurationService {
ready = false;
// Update the config, from the software properties
updateConfigFromSoftwareProperties();
applySoftwareProperties();
// Clean config cache
configuration.cleanCache();
// Publish update event
publisher.publishEvent(new ConfigurationUpdatedEvent(configuration));
......@@ -189,7 +192,7 @@ public class ConfigurationServiceImpl implements ConfigurationService {
}
@Override
public void updateConfigFromSoftwareProperties() {
public void applySoftwareProperties() {
boolean newDatabase = false;
......
......@@ -48,5 +48,6 @@ public class ObservedLocationFilterVO implements IRootDataFilter {
private Integer[] locationIds;
private Integer recorderDepartmentId;
private Integer recorderPersonId;
private Integer[] observerPersonIds;
}
......@@ -28,7 +28,6 @@ import net.sumaris.core.model.referential.QualityFlagEnum;
import net.sumaris.core.model.referential.UserProfileEnum;
import net.sumaris.core.service.AbstractServiceTest;
import net.sumaris.core.service.administration.PersonService;
import net.sumaris.core.service.referential.ReferentialService;
import net.sumaris.core.vo.administration.user.PersonVO;
import org.junit.*;
import org.junit.runners.MethodSorters;
......@@ -53,7 +52,7 @@ public class ConfigurationServiceOracleTest extends AbstractServiceTest {
@Before
public void setup() {
// force apply software configuration
configurationService.updateConfigFromSoftwareProperties();
configurationService.applySoftwareProperties();
}
@Test
......
......@@ -26,6 +26,9 @@ import it.ozimov.springboot.mail.configuration.EnableEmailTools;
import lombok.extern.slf4j.Slf4j;
import net.sumaris.core.config.SumarisConfiguration;
import net.sumaris.core.config.SumarisConfigurationOption;
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.util.ApplicationUtils;
import net.sumaris.core.util.I18nUtil;
......@@ -42,6 +45,7 @@ import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfigurati
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.event.EventListener;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;
......@@ -84,7 +88,7 @@ public class Application extends SpringBootServletInitializer {
SpringApplication.run(Application.class, args);
}
@Bean
@Bean("configuration")
public static SumarisServerConfiguration configuration(ConfigurableEnvironment env) {
SumarisServerConfiguration.initDefault(env);
SumarisServerConfiguration config = SumarisServerConfiguration.getInstance();
......@@ -173,5 +177,4 @@ public class Application extends SpringBootServletInitializer {
protected static String getI18nBundleName() {
return "sumaris-server-i18n";
}
}
......@@ -24,10 +24,14 @@ package net.sumaris.server.config;
* #L%
*/
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import lombok.extern.slf4j.Slf4j;
import net.sumaris.core.config.SumarisConfiguration;
import net.sumaris.core.config.SumarisConfigurationOption;
import net.sumaris.server.http.security.AuthTokenTypeEnum;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.compress.utils.Lists;
import org.apache.commons.lang3.StringUtils;
import org.nuiton.config.ApplicationConfig;
import org.nuiton.version.VersionBuilder;
......@@ -35,7 +39,8 @@ import org.nuiton.version.Version;
import org.springframework.core.env.ConfigurableEnvironment;
import java.io.File;
import java.util.TimeZone;
import java.util.*;
import java.util.stream.Collectors;
/**
* <p>SumarisServerConfiguration class.</p>
......@@ -98,16 +103,50 @@ public class SumarisServerConfiguration extends SumarisConfiguration {
super.overrideExternalModulesDefaultOptions(applicationConfig);
}
public int getSupervisorDepartment() {
return applicationConfig.getOptionAsInt(SumarisServerConfigurationOption.SUPERVISOR_DEPARTMENT.getKey());
public List<Integer> getAccessNotSelfDataDepartmentIds() {
final String optionKey = SumarisServerConfigurationOption.ACCESS_NOT_SELF_DATA_DEPARTMENT_IDS.getKey();
List<Integer> result = (List<Integer>) complexOptionsCache.getIfPresent(optionKey);
// Not exists in cache
if (result == null) {
String depIds = applicationConfig.getOption(optionKey);
if (StringUtils.isBlank(depIds)) {
result = ImmutableList.of();
}
else {
final List<String> invalidIds = Lists.newArrayList();
result = Splitter.on(",").omitEmptyStrings().trimResults()
.splitToList(depIds)
.stream()
.map(depId -> {
try {
return Integer.parseInt(depId);
}
catch (Exception e) {
invalidIds.add(depId);
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 options cache
complexOptionsCache.put(optionKey, result);
}
return result;
}
public String getAuthRoleForNotSelfData() {
return applicationConfig.getOption(SumarisServerConfigurationOption.AUTH_ROLE_NOT_SELF_DATA_ACCESS.getKey());
public String getAccessNotSelfDataMinRole() {
return applicationConfig.getOption(SumarisServerConfigurationOption.ACCESS_NOT_SELF_DATA_MIN_ROLE.getKey());
}
public String getAuthRoleForNotSelfExtraction() {
return applicationConfig.getOption(SumarisServerConfigurationOption.AUTH_ROLE_NOT_SELF_EXTRACTION_ACCESS.getKey());
public String getAccessNotSelfExtractionMinRole() {
return applicationConfig.getOption(SumarisServerConfigurationOption.ACCESS_NOT_SELF_EXTRACTION_MIN_ROLE.getKey());
}
public boolean enableAuthToken() {
......
......@@ -129,14 +129,21 @@ public enum SumarisServerConfigurationOption implements ConfigOptionDef {
Integer.class,
false),
AUTH_ROLE_NOT_SELF_DATA_ACCESS(
"sumaris.auth.notSelfDataAccess.role",
n("sumaris.config.option.auth.notSelfDataAccess.role.description"),
ACCESS_NOT_SELF_DATA_MIN_ROLE(
"sumaris.data.accessNotSelfData.role",
n("sumaris.config.option.data.accessNotSelfData.role.description"),
"ROLE_ADMIN", // Possible values: ROLE_GUEST, ROLE_USER, ROLE_SUPERVISOR, ROLE_ADMIN
String.class,
false),
AUTH_ROLE_NOT_SELF_EXTRACTION_ACCESS(ExtractionWebConfigurationOption.AUTH_ROLE_NOT_SELF_EXTRACTION_ACCESS),
ACCESS_NOT_SELF_DATA_DEPARTMENT_IDS(
"sumaris.data.accessNotSelfData.department.ids",
n("sumaris.config.option.data.accessNotSelfData.department.ids.description"),
null,
Integer.class,
false),
ACCESS_NOT_SELF_EXTRACTION_MIN_ROLE(ExtractionWebConfigurationOption.ACCESS_NOT_SELF_EXTRACTION_MIN_ROLE),
SECURITY_LDAP_ENABLED(
"spring.security.ldap.enabled",
......@@ -162,13 +169,6 @@ public enum SumarisServerConfigurationOption implements ConfigOptionDef {
null, // NUll == auto detected
String.class),
SUPERVISOR_DEPARTMENT(
"sumaris.supervisor.department",
n("sumaris.config.option.supervisor.department.description"),
null,
Integer.class,
false),
APP_MIN_VERSION(
"sumaris.app.version.min",
n("sumaris.config.option.sumaris.app.version.min.description"),
......
......@@ -26,6 +26,7 @@ import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import io.leangen.graphql.annotations.*;
import io.leangen.graphql.execution.ResolutionEnvironment;
import lombok.NonNull;
import net.sumaris.core.dao.referential.metier.MetierRepository;
import net.sumaris.core.dao.technical.Page;
import net.sumaris.core.dao.technical.Pageables;
......@@ -259,7 +260,6 @@ public class DataGraphQLService {
) {
filter = fillTripFilterDefaults(filter);
SortDirection sortDirection = SortDirection.fromString(direction, SortDirection.DESC);
// Read from trash
......@@ -276,6 +276,8 @@ public class DataGraphQLService {
TripVO.class).getContent();
}
filter = fillRootDataFilter(filter, TripFilterVO.class);
// Set default sort
sort = sort != null ? sort : TripVO.Fields.DEPARTURE_DATE_TIME;
......@@ -294,8 +296,8 @@ public class DataGraphQLService {
@GraphQLQuery(name = "tripsCount", description = "Get trips count")
@Transactional(readOnly = true)
@IsUser
public long getTripsCount(@GraphQLArgument(name = "filter") TripFilterVO filter,
@GraphQLArgument(name = "trash", defaultValue = "false") Boolean trash) {
public long countTrips(@GraphQLArgument(name = "filter") TripFilterVO filter,
@GraphQLArgument(name = "trash", defaultValue = "false") Boolean trash) {
if (trash) {
// Check user is admin
checkIsAdmin("Cannot access to trash");
......@@ -304,7 +306,9 @@ public class DataGraphQLService {
return trashService.count(Trip.class.getSimpleName());
}
return tripService.countByFilter(fillTripFilterDefaults(filter));
filter = fillRootDataFilter(filter, TripFilterVO.class);
return tripService.countByFilter(filter);
}
@GraphQLQuery(name = "trip", description = "Get a trip, by id")
......@@ -517,7 +521,6 @@ public class DataGraphQLService {
@GraphQLArgument(name = "trash", defaultValue = "false") Boolean trash,
@GraphQLEnvironment ResolutionEnvironment env
) {
filter = fillObserveLocationFilterDefaults(filter);
SortDirection sortDirection = SortDirection.fromString(direction, SortDirection.DESC);
// Read from trash
......@@ -534,6 +537,8 @@ public class DataGraphQLService {
ObservedLocationVO.class).getContent();
}
filter = fillRootDataFilter(filter, ObservedLocationFilterVO.class);
Set<String> fields = GraphQLUtils.fields(env);
final List<ObservedLocationVO> result = observedLocationService.findAll(
filter,
......@@ -550,11 +555,8 @@ public class DataGraphQLService {
@GraphQLQuery(name = "observedLocationsCount", description = "Get total number of observed locations")
@Transactional(readOnly = true)
@IsUser
public long getObservedLocationsCount(@GraphQLArgument(name = "filter") ObservedLocationFilterVO filter,
@GraphQLArgument(name = "trash", defaultValue = "false") Boolean trash) {
filter = fillObserveLocationFilterDefaults(filter);
public long countObservedLocations(@GraphQLArgument(name = "filter") ObservedLocationFilterVO filter,
@GraphQLArgument(name = "trash", defaultValue = "false") Boolean trash) {
if (trash) {
// Check user is admin
checkIsAdmin("Cannot access to trash");
......@@ -563,6 +565,8 @@ public class DataGraphQLService {
return trashService.count(ObservedLocation.class.getSimpleName());
}
filter = fillRootDataFilter(filter, ObservedLocationFilterVO.class);
return observedLocationService.count(filter);
}
......@@ -1392,52 +1396,47 @@ public class DataGraphQLService {
.build();
}
protected TripFilterVO fillTripFilterDefaults(TripFilterVO filter) {
TripFilterVO result = filter != null ? filter : new TripFilterVO();
// Restrict to self data and/or department data
PersonVO user = authService.getAuthenticatedUser().orElse(null);
if (user != null) {
if (!canAccessNotSelfData()) {
result.setRecorderPersonId(user.getId());
}
if (!canAccessNotSelfDepartmentData(user)) {
result.setRecorderDepartmentId(user.getDepartment().getId());
}
} else {
result.setRecorderPersonId(-999); // Hide all. Should never occur
/**
* Restrict to self data and/or department data
* @param filter
*/
protected <F extends IRootDataFilter> F fillRootDataFilter(F filter, Class<F> filterClass) {
try {
filter = filter != null ? filter : filterClass.newInstance();
}
catch (InstantiationException | IllegalAccessException e) {
log.error("Cannot create filter instance: {}", e.getMessage(), e);
}
return result;
}
protected ObservedLocationFilterVO fillObserveLocationFilterDefaults(ObservedLocationFilterVO filter) {
ObservedLocationFilterVO result = filter != null ? filter : new ObservedLocationFilterVO();
// Restrict to self data and/or department data
PersonVO user = authService.getAuthenticatedUser().orElse(null);
if (user != null) {
if (!canAccessNotSelfData()) {
result.setRecorderPersonId(user.getId());
if (!canUserAccessNotSelfData()) {
// Limit data access to self data
filter.setRecorderPersonId(user.getId());
}
if (!canAccessNotSelfDepartmentData(user)) {
result.setRecorderDepartmentId(user.getDepartment().getId());
else {
Integer depId = user.getDepartment().getId();
if (!canDepartmentAccessNotSelfData(depId)) {
// Limit data access to user's department
filter.setRecorderDepartmentId(depId);
}
}
} else {
result.setRecorderPersonId(-999); // Hide all. Should never occur
filter.setRecorderPersonId(-999); // Hide all. Should never occur
}