Commit 004b1962 authored by François-Xavier Lebastard's avatar François-Xavier Lebastard
Browse files

Merge remote-tracking branch 'origin/develop' into feature/tabs

parents 54747437 8d7a5b82
module.exports = {
'{,src/**/,webpack/}*.{md,json,yml,html,js,ts,tsx,css,scss,vue,java}': ['prettier --write'],
'{,src/**/,webpack/}*.{md,json,html,js,ts,tsx,css,scss,vue}': ['prettier --write'],
};
......@@ -55,7 +55,7 @@ entity Screen {
*/
reference String unique required
description TextBlob
displayCondition TextBlob,
nextScreenExpression TextBlob,
items TextBlob required
}
......
package com.unantes.orientactive.domain;
import com.unantes.orientactive.validation.CheckExpression;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.ManyToOne;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.annotations.Type;
/**
* Une expression qui permet de calcul l'écran suivant d'un écran.
* Un écran peut posséder plusieurs expressions. Elles sont évaluées successivement.
* La premiere, dont l'{@link #expression} est vérifiée, est choisie.
*/
@Entity
@Table(name = "next_screen_expression")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class NextScreenExpression implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
@SequenceGenerator(name = "sequenceGenerator")
private Long id;
@NotNull
@Column(name = "next_screen_reference", nullable = false)
private String nextScreenReference;
@NotNull
@Lob
@CheckExpression
@Type(type = "org.hibernate.type.TextType")
@Column(name = "expression", nullable = false)
private String expression;
/**
* L'écran associé à cette expression de calcul d'écran suivant.
*/
@ManyToOne
private Screen screen;
public Long getId() {
return id;
}
public void setId(final Long id) {
this.id = id;
}
public String getNextScreenReference() {
return nextScreenReference;
}
public void setNextScreenReference(final String nextScreenReference) {
this.nextScreenReference = nextScreenReference;
}
public String getExpression() {
return expression;
}
public void setExpression(final String expression) {
this.expression = expression;
}
public Screen getScreen() {
return screen;
}
public void setScreen(final Screen screen) {
this.screen = screen;
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final NextScreenExpression that = (NextScreenExpression) o;
return new EqualsBuilder()
.append(id, that.id)
.append(nextScreenReference, that.nextScreenReference)
.append(expression, that.expression)
.append(screen, that.screen)
.isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder(17, 37).append(id).append(nextScreenReference).append(expression).append(screen).toHashCode();
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("id", id)
.append("nextScreenReference", nextScreenReference)
.append("expression", expression)
.append("screen", screen)
.toString();
}
}
......@@ -2,11 +2,25 @@ package com.unantes.orientactive.domain;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.unantes.orientactive.converter.bean.Item;
import com.unantes.orientactive.validation.CheckExpression;
import com.unantes.orientactive.validation.CheckJson;
import java.io.Serializable;
import javax.persistence.*;
import javax.validation.constraints.*;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.Lob;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
import org.apache.commons.collections4.CollectionUtils;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.annotations.Type;
......@@ -45,11 +59,19 @@ public class Screen implements Serializable {
@Column(name = "description")
private String description;
@Lob
@CheckExpression
@Type(type = "org.hibernate.type.TextType")
@Column(name = "display_condition")
private String displayCondition;
/**
* La référence de l'écran suivant par défaut. Utilisé si aucune {@link #nextScreenExpressions} n'est vérifiée.
* Si cette référence est vide, {@link #next} est utilisé.
*/
@Column(name = "default_next_screen_reference")
private String defaultNextScreenReference;
/**
* La liste des expressions à évaluer pour déterminer l'écran suivant.
*/
@OneToMany(mappedBy = "screen", cascade = CascadeType.ALL, orphanRemoval = true)
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
private List<NextScreenExpression> nextScreenExpressions = new ArrayList<>();
@Lob
@CheckJson(isList = true, javaType = Item.class)
......@@ -123,17 +145,49 @@ public class Screen implements Serializable {
this.description = description;
}
public String getDisplayCondition() {
return this.displayCondition;
public static long getSerialVersionUID() {
return serialVersionUID;
}
public String getDefaultNextScreenReference() {
return defaultNextScreenReference;
}
public Screen displayCondition(String displayCondition) {
this.displayCondition = displayCondition;
public void setDefaultNextScreenReference(final String defaultNextScreenReference) {
this.defaultNextScreenReference = defaultNextScreenReference;
}
public Screen defaultNextScreenReference(final String defaultNextScreenReference) {
setDefaultNextScreenReference(defaultNextScreenReference);
return this;
}
public void setDisplayCondition(String displayCondition) {
this.displayCondition = displayCondition;
public List<NextScreenExpression> getNextScreenExpressions() {
return nextScreenExpressions;
}
public Screen addNextScreenExpression(NextScreenExpression nextScreenExpression) {
this.nextScreenExpressions.add(nextScreenExpression);
nextScreenExpression.setScreen(this);
return this;
}
public Screen removeNextScreenExpression(NextScreenExpression nextScreenExpression) {
this.nextScreenExpressions.remove(nextScreenExpression);
nextScreenExpression.setScreen(null);
return this;
}
public void setNextScreenExpressions(final List<NextScreenExpression> nextScreenExpressions) {
nextScreenExpressions(nextScreenExpressions);
}
public Screen nextScreenExpressions(final List<NextScreenExpression> aNextScreenExpressions) {
nextScreenExpressions.forEach(this::removeNextScreenExpression);
if (CollectionUtils.isNotEmpty(aNextScreenExpressions)) {
aNextScreenExpressions.forEach(this::addNextScreenExpression);
}
return this;
}
public String getItems() {
......@@ -216,13 +270,6 @@ public class Screen implements Serializable {
// prettier-ignore
@Override
public String toString() {
return "Screen{" +
"id=" + getId() +
", name='" + getName() + "'" +
", reference='" + getReference() + "'" +
", description='" + getDescription() + "'" +
", displayCondition='" + getDisplayCondition() + "'" +
", items='" + getItems() + "'" +
"}";
return "Screen{" + "id=" + getId() + ", name='" + getName() + "'" + ", reference='" + getReference() + "'" + ", description='" + getDescription() + "'" + ", nextScreenExpressions='" + getNextScreenExpressions() + "'" + ", items='" + getItems() + "'" + "}";
}
}
package com.unantes.orientactive.navigation;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import com.unantes.orientactive.condition.Expression;
import com.unantes.orientactive.converter.ServiceConverter;
import com.unantes.orientactive.converter.bean.ExpressionItem;
import com.unantes.orientactive.converter.bean.Item;
import com.unantes.orientactive.domain.Screen;
......@@ -17,9 +8,20 @@ import com.unantes.orientactive.service.AnswerService;
import com.unantes.orientactive.service.ScreenService;
import com.unantes.orientactive.service.VariableService;
import com.unantes.orientactive.service.dto.AnswerDTO;
import com.unantes.orientactive.service.dto.NextScreenExpressionDTO;
import com.unantes.orientactive.service.dto.ScreenDTO;
import com.unantes.orientactive.service.dto.VariableDTO;
import com.unantes.orientactive.web.rest.errors.UnableToFindNextScreenException;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
/**
* Ce service contient la logique de navigation d'un écran vers le suivant. Il contient également les méthodes permettants de déterminer si un item doit être affiché.
......@@ -27,10 +29,7 @@ import com.unantes.orientactive.web.rest.errors.UnableToFindNextScreenException;
@Service
public class ScreenNavigationService {
/**
* Lors du calcul de l'écran suivant {@link #getNextScreen(String, ScreenDTO)}, une boucle infi peut se produire. Cette constante en est le seuil de détection.
*/
public static final int INFINITE_LOOP_THRESHOLD = 100;
private static final Logger LOGGER = LoggerFactory.getLogger(ScreenNavigationService.class);
/**
* Pour charger l'écran suivant de l'écran courant.
......@@ -60,43 +59,90 @@ public class ScreenNavigationService {
}
/**
* Récupère l'écran suivant en fonction des conditions d'affichage :
* <ul>
* <li>construit une {@link Expression} à partir des réponses de la session</li>
* <li>évalue les conditions d'affichage de l'écran suivant; s'il est affichable : l'affiche; sinon itère sur l'écran suivant</li>
* <li>filtre les items de l'écran suivant affichable</li>
* </ul>
* Détermine l'écran suivant et filtre les items qui ne doivent pas être affichés.
* L'écran suivant est calculé à partir de {@link Screen#getNextScreenExpressions()}, {@link Screen#getDefaultNextScreenReference()} et de {@link Screen#getNext()}.
* Chaque expression de {@link Screen#getNextScreenExpressions()} est évaluée : la première qui est vérifiée donne l'écran suivant.
* Si aucune n'est vérifiée, {@link Screen#getDefaultNextScreenReference()} est utilisé s'il est non vide
* {@link Screen#getNext()} est utilisé en dernier recours.
* @param sessionId une session de réponses (l'identifiant commun à toutes les réponses d'un utilisateur)
* @param screen l'écran courant de l'utilisateur
* @return
* @throws UnableToFindNextScreenException la méthode a été appelée avec un écran final ou l'expression du screen courant retourne un screen qui n'existe pas
*
*/
public ScreenDTO getNextScreen(final String sessionId, final ScreenDTO screen) {
ScreenDTO currentNextScreen = screen;
if (currentNextScreen.getNextId() == null) {
public ScreenDTO getNextScreen(final String sessionId, final ScreenDTO screen) throws UnableToFindNextScreenException {
// il ne faut pas appeler cette méthode lorsque l'écran courant est le dernier !
if (screen.getNextId() == null) {
throw new UnableToFindNextScreenException();
}
// initialisation d'une expression. Elle est utilisée pour evaluer les expressions permettant de déterminer les écrans suivants et pour filter les items affichés dans l'écran suivant.
final Expression expression = initializeExpression(screen.getFormId(), sessionId);
// pour déterminer le prochain écran, on évalue les expression de chaque écran
Boolean displayCurrentScreen = false;
int iterationCount = 0;
do {
currentNextScreen = screenService.findOne(currentNextScreen.getNextId()).orElseThrow(() -> new IllegalStateException("le screen n'existe pas en base"));
displayCurrentScreen = isScreenDisplayable(currentNextScreen, expression);
checkInfiniteLoop(displayCurrentScreen, iterationCount++);
} while (!displayCurrentScreen);
filterScreenItems(currentNextScreen, expression);
return currentNextScreen;
final ScreenDTO nextScreen;
if (CollectionUtils.isEmpty(screen.getNextScreenExpressions())) {
nextScreen = getDefaultNextScreen(screen);
LOGGER.debug("Prochain écran (valeur par défaut) : {}", nextScreen.getReference());
} else {
nextScreen = getNextScreenFromExpressions(expression, screen);
LOGGER.debug("Prochain écran (à partir des expressions) : {}", nextScreen.getReference());
}
filterScreenItems(nextScreen, expression);
return nextScreen;
}
/**
* Détermine l'écran suivant en fonction des expressions {@link ScreenDTO#getNextScreenExpressions()}.
* @param expression le moteur d'expressions SpEL initialisé avec les données de l'utilisateur
* @param screen l'écran courant
* @return
*/
private ScreenDTO getNextScreenFromExpressions(final Expression expression, final ScreenDTO screen) {
final Predicate<NextScreenExpressionDTO> matchingExpressionPredicate = nextScreenExpression ->
expression.evaluate(nextScreenExpression.getExpression(), Boolean.class);
//@formatter:off
final Optional<NextScreenExpressionDTO> matchingExpression = screen.getNextScreenExpressions().stream()
.filter(matchingExpressionPredicate)
.findFirst();
return matchingExpression
.map(NextScreenExpressionDTO::getNextScreenReference)
.map(this::getScreenByRef)
.orElseGet(() -> getDefaultNextScreen(screen));
//@formatter:on
}
/**
* La méthode {@link #getNextScreen(String, ScreenDTO)} peut générer une boucle infinie : cette méhode jette une exception le cas échéant pour éviter de faire tomber le serveur.
* @param displayCurrentScreen
* @param iterationCount
* Deux écrans par défaut possibles :
* <ul>
* <li>{@link Screen#getDefaultNextScreenReference()}; utilisé si renseigné</li>
* <li>{@link Screen#getNext()}; champ obligatoire; utilisé pour définir l'ordre en back office et en dernier recours pour l'ordre en front.</li>
* </ul>
* @param screen
* @return
*/
private void checkInfiniteLoop(final Boolean displayCurrentScreen, final int iterationCount) {
if (!displayCurrentScreen && iterationCount > INFINITE_LOOP_THRESHOLD) {
throw new IllegalStateException("Boucle infinie détectée : le traitement est interrompu !");
private ScreenDTO getDefaultNextScreen(final ScreenDTO screen) {
if (StringUtils.isBlank(screen.getDefaultNextScreenReference())) {
return getScreenById(screen.getNextId());
}
return getScreenByRef(screen.getDefaultNextScreenReference());
}
/**
* Wrapper de la méthode {@link ScreenService#findOne(Long)}; Jette une exception si l'écran n'existe pas.
* @param nextId
* @return
*/
private ScreenDTO getScreenById(final Long nextId) {
return screenService.findOne(nextId).orElseThrow(() -> new UnableToFindNextScreenException("l'écran n'existe pas en base"));
}
/**
* Wrapper de la méthode {@link ScreenService#findOneByReference(String)}; Jette une exception si l'écran n'existe pas.
* @param screenRef
* @return
*/
private ScreenDTO getScreenByRef(final String screenRef) {
return screenService
.findOneByReference(screenRef)
.orElseThrow(() -> new UnableToFindNextScreenException("l'écran n'existe pas en base"));
}
/**
......@@ -117,7 +163,11 @@ public class ScreenNavigationService {
protected void filterScreenItems(final ScreenDTO screen, final Expression expressionEngine) {
final Predicate<Item> conditionNotMetFilter = filterItem(expressionEngine);
final List<Item> items = screen.getItemsList().stream().filter(conditionNotMetFilter).collect(Collectors.toList());
final Set<ExpressionItem> expressionItems = items.stream().filter(ExpressionItem.class::isInstance).map(ExpressionItem.class::cast).collect(Collectors.toSet());
final Set<ExpressionItem> expressionItems = items
.stream()
.filter(ExpressionItem.class::isInstance)
.map(ExpressionItem.class::cast)
.collect(Collectors.toSet());
for (ExpressionItem expressionItem : expressionItems) {
String expression = expressionItem.getContent();
String evaluatedExpression = StringUtils.trimToEmpty(expressionEngine.evaluate(expression, String.class));
......@@ -144,29 +194,6 @@ public class ScreenNavigationService {
return expression;
}
/**
* Un écran est affichable si
* <ul>
* <li>il est le dernier de la liste</li>
* <li>il n'a pas de condition d'affichage</li>
* <li>sa condition d'affichage est vérifiée</li>
* </ul>
* @param screen un écran
* @param expression l'expression qui permet d'évaluer la condition d'affichage
* @return
*/
private Boolean isScreenDisplayable(final ScreenDTO screen, final Expression expression) {
if (screen.getNextId() == null) {
return true;
}
final String displayCondition = screen.getDisplayCondition();
if (StringUtils.isBlank(displayCondition)) {
return true;
}
Boolean evaluate = expression.evaluate(displayCondition, Boolean.class);
return evaluate == null || evaluate;
}
/**
* Construit une lambda qui pemet de déterminer si un item doit être affiché dans une écran.
* @param expression une expression initialisée avec les réponses de l'utilisateur
......
......@@ -68,14 +68,14 @@ public abstract class PermissionService<T extends PermissionEntity> {
* Cette méthode s'occupe de vérifier la présence du wildcard et, dans le cas ou il est présent, de simplement récupérer toutes les entités.
*
* @param user L'utilisateur.
* @param parentElement L'élément parent.
* @param idParentElement L'identifiant de l'élément parent.
* @return Les entités.
*/
public <U extends PermissionEntity> List<T> find(User user, U parentElement) {
public List<T> find(User user, Long idParentElement) {
if (hasWildcard(user)) {
return findAllByParent(parentElement);
return findAllByParent(idParentElement);
}
return findWithPermission(user, parentElement);
return findWithPermission(user, idParentElement);
}
/**
......@@ -98,7 +98,7 @@ public abstract class PermissionService<T extends PermissionEntity> {
*
* @return Les entités.
*/
public abstract <U extends PermissionEntity> List<T> findAllByParent(U parent);
public abstract List<T> findAllByParent(Long idParent);
/**
* Permet la récupération d'une entité en particulier via son identifiant.
......@@ -119,10 +119,10 @@ public abstract class PermissionService<T extends PermissionEntity> {
* Méthode interne permettant de récupérer les entités que l'utilisateur a la permission d'administer.
*
* @param user L'utilisateur.
* @param parentElement L'élément parent.
* @param idParentElement L'identifiant de l'élément parent.
* @return Les entités.
*/
protected abstract <U extends PermissionEntity> List<T> findWithPermission(User user, U parentElement);
protected abstract List<T> findWithPermission(User user, Long idParentElement);
/**
* Méthode interne permettant de vérifier unitairement la permission d'un utilisateur sur une entité.
......
......@@ -149,9 +149,8 @@ public abstract class PermissionController {
@GetMapping("/find/{loginUser}/{idParentEtity}")
public List<PermissionEntity> find(@PathVariable final String loginUser, @PathVariable final Long idParentEtity) {
final Optional<User> userOptional = userService.getUserWithAuthoritiesByLogin(loginUser);
final Optional<PermissionEntity> entityOptional = permissionService.findOne(idParentEtity);
if (userOptional.isPresent() && entityOptional.isPresent()) {
return permissionService.find(userOptional.get(), entityOptional.get());
if (userOptional.isPresent()) {
return permissionService.find(userOptional.get(), idParentEtity);
}
return Collections.emptyList();
}
......
......@@ -177,9 +177,9 @@ public class FormService extends PermissionService<FormDTO> {
@PreAuthorize("hasAuthority('VIEW_FORMS')")
@Override
protected <U extends PermissionEntity> List<FormDTO> findWithPermission(User user, U parentElement) {
protected List<FormDTO> findWithPermission(User user, Long idParentElement) {
return formRepository
.findWithPermission(user.getId(), parentElement.getId())
.findWithPermission(user.getId(), idParentElement)
.stream()
.map(formMapper::toDto)
.collect(Collectors.toList());
......@@ -187,9 +187,9 @@ public class FormService extends PermissionService<FormDTO> {
@PreAuthorize("hasAuthority('VIEW_FORMS')")
@Override
public <U extends PermissionEntity> List<FormDTO> findAllByParent(U parentElement) {
public List<FormDTO> findAllByParent(Long idParentElement) {
return formRepository
.findAllByWorkspaceId(parentElement.getId())
.findAllByWorkspaceId(idParentElement)
.stream()
.map(formMapper::toDto)
.collect(Collectors.toCollection(LinkedList::new));
......
......@@ -146,14 +146,14 @@ public class WorkspaceService extends PermissionService<WorkspaceDTO> {
@PreAuthorize("hasAuthority('VIEW_WORKSPACE')")
@Override
public <U extends PermissionEntity> List<WorkspaceDTO> findAllByParent(U parent) {
public List<WorkspaceDTO> findAllByParent(Long idParent) {
// On ignore l'élément parent car le workspace n'en possède pas.
return findAll();
}
@PreAuthorize("hasAuthority('VIEW_WORKSPACE')")
@Override
protected <U extends PermissionEntity> List<WorkspaceDTO> findWithPermission(User user, U parentElement) {
protected List<WorkspaceDTO> findWithPermission(User user, Long idParentElement) {
// On ignore l'élément parent car le workspace n'en possède pas.
return workspaceRepository.findWithPermission(user.getId()).stream().map(workspaceMapper::toDto).collect(Collectors.toList());
}
......
package com.unantes.orientactive.service.dto;
import java.io.Serializable;
import com.unantes.orientactive.validation.CheckExpression;