WorkflowValidationService.java
package com.licensis.notaire.service;
import com.licensis.notaire.negocio.WorkflowNode;
import com.licensis.notaire.negocio.WorkflowNodeType;
import com.licensis.notaire.negocio.WorkflowTransition;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Service
public class WorkflowValidationService {
public ValidationResult validate(List<WorkflowNode> nodes, List<WorkflowTransition> transitions) {
List<String> errors = new ArrayList<>();
if (nodes.isEmpty()) {
errors.add("El workflow no tiene nodos.");
return ValidationResult.invalid(errors);
}
long initialCount = nodes.stream().filter(n -> n.getTipo() == WorkflowNodeType.INITIAL).count();
if (initialCount == 0) {
errors.add("El workflow debe tener exactamente un nodo inicial.");
} else if (initialCount > 1) {
errors.add("El workflow tiene más de un nodo inicial (" + initialCount + ").");
}
long finalCount = nodes.stream().filter(n -> n.getTipo() == WorkflowNodeType.FINAL).count();
if (finalCount == 0) {
errors.add("El workflow debe tener al menos un nodo final.");
}
if (!errors.isEmpty()) {
return ValidationResult.invalid(errors);
}
Set<Integer> reachable = reachableFrom(
nodes.stream().filter(n -> n.getTipo() == WorkflowNodeType.INITIAL)
.findFirst().map(WorkflowNode::getId).orElse(-1),
transitions);
Set<Integer> allIds = nodes.stream().map(WorkflowNode::getId).collect(Collectors.toSet());
Set<Integer> unreachable = new HashSet<>(allIds);
unreachable.removeAll(reachable);
if (!unreachable.isEmpty()) {
String names = nodes.stream()
.filter(n -> unreachable.contains(n.getId()))
.map(n -> n.getEstadoDeGestion() != null ? n.getEstadoDeGestion().getNombre() : String.valueOf(n.getId()))
.collect(Collectors.joining(", "));
errors.add("Nodos no alcanzables desde el nodo inicial (aislados): " + names + ".");
}
return errors.isEmpty() ? ValidationResult.valid() : ValidationResult.invalid(errors);
}
private Set<Integer> reachableFrom(Integer startId, List<WorkflowTransition> transitions) {
Map<Integer, List<Integer>> adjacency = transitions.stream()
.collect(Collectors.groupingBy(
t -> t.getNodoOrigen().getId(),
Collectors.mapping(t -> t.getNodoDestino().getId(), Collectors.toList())));
Set<Integer> visited = new HashSet<>();
dfs(startId, adjacency, visited);
return visited;
}
private void dfs(Integer node, Map<Integer, List<Integer>> adjacency, Set<Integer> visited) {
if (visited.contains(node)) {
return;
}
visited.add(node);
for (Integer neighbour : adjacency.getOrDefault(node, List.of())) {
dfs(neighbour, adjacency, visited);
}
}
public static final class ValidationResult {
private final boolean valid;
private final List<String> errors;
private ValidationResult(boolean valid, List<String> errors) {
this.valid = valid;
this.errors = errors;
}
public static ValidationResult valid() {
return new ValidationResult(true, List.of());
}
public static ValidationResult invalid(List<String> errors) {
return new ValidationResult(false, errors);
}
public boolean isValid() {
return valid;
}
public List<String> getErrors() {
return errors;
}
}
}