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 = { 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 { ...@@ -55,7 +55,7 @@ entity Screen {
*/ */
reference String unique required reference String unique required
description TextBlob description TextBlob
displayCondition TextBlob, nextScreenExpression TextBlob,
items TextBlob required 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; ...@@ -2,11 +2,25 @@ package com.unantes.orientactive.domain;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.unantes.orientactive.converter.bean.Item; import com.unantes.orientactive.converter.bean.Item;
import com.unantes.orientactive.validation.CheckExpression;
import com.unantes.orientactive.validation.CheckJson; import com.unantes.orientactive.validation.CheckJson;
import java.io.Serializable; import java.io.Serializable;
import javax.persistence.*; import java.util.ArrayList;
import javax.validation.constraints.*; 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.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy; import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.annotations.Type; import org.hibernate.annotations.Type;
...@@ -45,11 +59,19 @@ public class Screen implements Serializable { ...@@ -45,11 +59,19 @@ public class Screen implements Serializable {
@Column(name = "description") @Column(name = "description")
private String description; private String description;
@Lob /**
@CheckExpression * La référence de l'écran suivant par défaut. Utilisé si aucune {@link #nextScreenExpressions} n'est vérifiée.
@Type(type = "org.hibernate.type.TextType") * Si cette référence est vide, {@link #next} est utilisé.
@Column(name = "display_condition") */
private String displayCondition; @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 @Lob
@CheckJson(isList = true, javaType = Item.class) @CheckJson(isList = true, javaType = Item.class)
...@@ -123,17 +145,49 @@ public class Screen implements Serializable { ...@@ -123,17 +145,49 @@ public class Screen implements Serializable {
this.description = description; this.description = description;
} }
public String getDisplayCondition() { public static long getSerialVersionUID() {
return this.displayCondition; return serialVersionUID;
}
public String getDefaultNextScreenReference() {
return defaultNextScreenReference;
} }
public Screen displayCondition(String displayCondition) { public void setDefaultNextScreenReference(final String defaultNextScreenReference) {
this.displayCondition = displayCondition; this.defaultNextScreenReference = defaultNextScreenReference;
}
public Screen defaultNextScreenReference(final String defaultNextScreenReference) {
setDefaultNextScreenReference(defaultNextScreenReference);
return this; return this;
} }
public void setDisplayCondition(String displayCondition) { public List<NextScreenExpression> getNextScreenExpressions() {
this.displayCondition = displayCondition; 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() { public String getItems() {
...@@ -216,13 +270,6 @@ public class Screen implements Serializable { ...@@ -216,13 +270,6 @@ public class Screen implements Serializable {
// prettier-ignore // prettier-ignore
@Override @Override
public String toString() { public String toString() {
return "Screen{" + return "Screen{" + "id=" + getId() + ", name='" + getName() + "'" + ", reference='" + getReference() + "'" + ", description='" + getDescription() + "'" + ", nextScreenExpressions='" + getNextScreenExpressions() + "'" + ", items='" + getItems() + "'" + "}";
"id=" + getId() +
", name='" + getName() + "'" +
", reference='" + getReference() + "'" +
", description='" + getDescription() + "'" +
", displayCondition='" + getDisplayCondition() + "'" +
", items='" + getItems() + "'" +
"}";
} }
} }
package com.unantes.orientactive.navigation; 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.condition.Expression;
import com.unantes.orientactive.converter.ServiceConverter;
import com.unantes.orientactive.converter.bean.ExpressionItem; import com.unantes.orientactive.converter.bean.ExpressionItem;
import com.unantes.orientactive.converter.bean.Item; import com.unantes.orientactive.converter.bean.Item;
import com.unantes.orientactive.domain.Screen; import com.unantes.orientactive.domain.Screen;
...@@ -17,9 +8,20 @@ import com.unantes.orientactive.service.AnswerService; ...@@ -17,9 +8,20 @@ import com.unantes.orientactive.service.AnswerService;
import com.unantes.orientactive.service.ScreenService; import com.unantes.orientactive.service.ScreenService;
import com.unantes.orientactive.service.VariableService; import com.unantes.orientactive.service.VariableService;
import com.unantes.orientactive.service.dto.AnswerDTO; 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.ScreenDTO;
import com.unantes.orientactive.service.dto.VariableDTO; import com.unantes.orientactive.service.dto.VariableDTO;
import com.unantes.orientactive.web.rest.errors.UnableToFindNextScreenException; 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é. * 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; ...@@ -27,10 +29,7 @@ import com.unantes.orientactive.web.rest.errors.UnableToFindNextScreenException;
@Service @Service
public class ScreenNavigationService { public class ScreenNavigationService {
/** private static final Logger LOGGER = LoggerFactory.getLogger(ScreenNavigationService.class);
* 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;
/** /**
* Pour charger l'écran suivant de l'écran courant. * Pour charger l'écran suivant de l'écran courant.
...@@ -60,43 +59,90 @@ public class ScreenNavigationService { ...@@ -60,43 +59,90 @@ public class ScreenNavigationService {
} }
/** /**
* Récupère l'écran suivant en fonction des conditions d'affichage : * Détermine l'écran suivant et filtre les items qui ne doivent pas être affichés.
* <ul> * L'écran suivant est calculé à partir de {@link Screen#getNextScreenExpressions()}, {@link Screen#getDefaultNextScreenReference()} et de {@link Screen#getNext()}.
* <li>construit une {@link Expression} à partir des réponses de la session</li> * Chaque expression de {@link Screen#getNextScreenExpressions()} est évaluée : la première qui est vérifiée donne l'écran suivant.
* <li>évalue les conditions d'affichage de l'écran suivant; s'il est affichable : l'affiche; sinon itère sur l'écran suivant</li> * Si aucune n'est vérifiée, {@link Screen#getDefaultNextScreenReference()} est utilisé s'il est non vide
* <li>filtre les items de l'écran suivant affichable</li> * {@link Screen#getNext()} est utilisé en dernier recours.
* </ul>
* @param sessionId une session de réponses (l'identifiant commun à toutes les réponses d'un utilisateur) * @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 * @param screen l'écran courant de l'utilisateur
* @return * @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) { public ScreenDTO getNextScreen(final String sessionId, final ScreenDTO screen) throws UnableToFindNextScreenException {
ScreenDTO currentNextScreen = screen; // il ne faut pas appeler cette méthode lorsque l'écran courant est le dernier !
if (currentNextScreen.getNextId() == null) { if (screen.getNextId() == null) {
throw new UnableToFindNextScreenException(); 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); final Expression expression = initializeExpression(screen.getFormId(), sessionId);
// pour déterminer le prochain écran, on évalue les expression de chaque écran final ScreenDTO nextScreen;
Boolean displayCurrentScreen = false; if (CollectionUtils.isEmpty(screen.getNextScreenExpressions())) {
int iterationCount = 0; nextScreen = getDefaultNextScreen(screen);
do { LOGGER.debug("Prochain écran (valeur par défaut) : {}", nextScreen.getReference());
currentNextScreen = screenService.findOne(currentNextScreen.getNextId()).orElseThrow(() -> new IllegalStateException("le screen n'existe pas en base")); } else {
displayCurrentScreen = isScreenDisplayable(currentNextScreen, expression); nextScreen = getNextScreenFromExpressions(expression, screen);
checkInfiniteLoop(displayCurrentScreen, iterationCount++); LOGGER.debug("Prochain écran (à partir des expressions) : {}", nextScreen.getReference());
} while (!displayCurrentScreen); }
filterScreenItems(currentNextScreen, expression); filterScreenItems(nextScreen, expression);
return currentNextScreen; 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. * Deux écrans par défaut possibles :
* @param displayCurrentScreen * <ul>
* @param iterationCount * <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) { private ScreenDTO getDefaultNextScreen(final ScreenDTO screen) {
if (!displayCurrentScreen && iterationCount > INFINITE_LOOP_THRESHOLD) { if (StringUtils.isBlank(screen.getDefaultNextScreenReference())) {
throw new IllegalStateException("Boucle infinie détectée : le traitement est interrompu !"); 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 { ...@@ -117,7 +163,11 @@ public class ScreenNavigationService {
protected void filterScreenItems(final ScreenDTO screen, final Expression expressionEngine) { protected void filterScreenItems(final ScreenDTO screen, final Expression expressionEngine) {
final Predicate<Item> conditionNotMetFilter = filterItem(expressionEngine); final Predicate<Item> conditionNotMetFilter = filterItem(expressionEngine);
final List<Item> items = screen.getItemsList().stream().filter(conditionNotMetFilter).collect(Collectors.toList()); 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) { for (ExpressionItem expressionItem : expressionItems) {
String expression = expressionItem.getContent(); String expression = expressionItem.getContent();
String evaluatedExpression = StringUtils.trimToEmpty(expressionEngine.evaluate(expression, String.class)); String evaluatedExpression = StringUtils.trimToEmpty(expressionEngine.evaluate(expression, String.class));
...@@ -144,29 +194,6 @@ public class ScreenNavigationService { ...@@ -144,29 +194,6 @@ public class ScreenNavigationService {
return expression; 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. * 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 * @param expression une expression initialisée avec les réponses de l'utilisateur
......
...@@ -68,14 +68,14 @@ public abstract class PermissionService<T extends PermissionEntity> { ...@@ -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. * 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 user L'utilisateur.
* @param parentElement L'élément parent. * @param idParentElement L'identifiant de l'élément parent.
* @return Les entités. * @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)) { 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> { ...@@ -98,7 +98,7 @@ public abstract class PermissionService<T extends PermissionEntity> {
* *
* @return Les entités. * @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. * Permet la récupération d'une entité en particulier via son identifiant.
...@@ -119,10 +119,10 @@ public abstract class PermissionService<T extends PermissionEntity> { ...@@ -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. * Méthode interne permettant de récupérer les entités que l'utilisateur a la permission d'administer.
* *
* @param user L'utilisateur. * @param user L'utilisateur.