Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 European Union
* Copyright 2025 European Union
*
* Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European
* Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in
Expand All @@ -13,21 +13,121 @@
*/
package eu.europa.ted.eforms.sdk.schematron;

import java.util.List;

import eu.europa.ted.efx.model.Context;
import eu.europa.ted.efx.model.rules.RuleNature;
import eu.europa.ted.efx.model.rules.RuleSeverity;
import eu.europa.ted.efx.model.rules.ValidationRule;
import eu.europa.ted.efx.model.variables.DynamicVariable;

/**
* Represents a Schematron <assert> element.
* Fires when the test expression evaluates to false.
* For rules referencing dynamic variables, the test is guarded so that API errors
* do not cause the main rule to fire — a companion {@link NoApiError} assert handles that.
*/
public class SchematronAssert extends SchematronTest {

public SchematronAssert(ValidationRule rule, Context ruleContext) {
private final List<DynamicVariable> dynamicVariables;

public SchematronAssert(final ValidationRule rule, final Context ruleContext) {
super(rule, ruleContext);
this.dynamicVariables = rule.findReferencedDynamicVariables();
}

@Override
public RuleNature getRuleNature() {
if (!this.dynamicVariables.isEmpty()) {
return RuleNature.DYNAMIC;
}
return super.getRuleNature();
}

@Override
public String getElementName() {
return "assert";
}

@Override
public String getTest() {
String baseTest = super.getTest();
if (this.getRuleNature() != RuleNature.DYNAMIC) {
return baseTest;
}
// An assert fires when the test is false. Prepending "($varName = -1) or" makes the
// test true when any dynamic variable errored, preventing the main assert from firing.
// Companion NoApiError asserts handle API errors separately.
StringBuilder sb = new StringBuilder();
for (var dynamicVar : this.dynamicVariables) {
sb.append("($").append(dynamicVar.name).append(" = -1) or ");
}
for (var variable : this.rule.getAutoGeneratedVariables()) {
sb.append("($").append(variable.name).append(" = -1) or ");
}
if (this.rule.getCondition() != null) {
// WHEN clause: combineWithOrParenthesized already wrapped each operand in parens.
sb.append(baseTest);
} else {
// No WHEN clause: the raw expression needs wrapping to isolate it from the guards.
sb.append("(").append(baseTest).append(")");
}
return sb.toString();
}

/**
* A companion Schematron &lt;assert&gt; that checks a dynamic variable did not return an error (-1).
* Generated for each dynamic variable (declared or auto-generated) referenced by a rule.
*/
public static class NoApiError extends SchematronAssert {

private final DynamicVariable dynamicVariable;

public NoApiError(ValidationRule rule, DynamicVariable dynamicVariable, Context ruleContext) {
super(rule, ruleContext);
this.dynamicVariable = dynamicVariable;
}

@Override
public RuleNature getRuleNature() {
return RuleNature.DYNAMIC;
}

@Override
public SchematronLet getLetElement() {
if (this.dynamicVariable instanceof DynamicVariable.AutoGenerated) {
return new SchematronLet(this.dynamicVariable);
}
return null;
}

@Override
public String getId() {
return this.rule.getId() + "-api-error-" + this.dynamicVariable.identity();
}

@Override
public String getRole() {
return this.dynamicVariable.errorSeverity().toString().toUpperCase();
}

@Override
public String getTest() {
return "not($" + this.dynamicVariable.name + " = -1)";
}

@Override
public String getMessage() {
if (this.dynamicVariable.errorLabel() != null) {
return this.dynamicVariable.errorLabel();
}
return this.dynamicVariable.errorSeverity() == RuleSeverity.WARNING
? "rule|text|api-warning" : "rule|text|api-error";
}

@Override
public SchematronDiagnostic getDiagnostic() {
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 European Union
* Copyright 2025 European Union
*
* Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European
* Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 European Union
* Copyright 2025 European Union
*
* Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European
* Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in
Expand All @@ -20,6 +20,7 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.slf4j.Logger;
Expand All @@ -32,7 +33,8 @@
import eu.europa.ted.eforms.sdk.component.SdkComponent;
import eu.europa.ted.eforms.sdk.component.SdkComponentType;
import eu.europa.ted.efx.interfaces.ValidatorGenerator;
import eu.europa.ted.efx.model.rules.CompleteValidation;
import eu.europa.ted.efx.model.rules.ValidationPlan;
import eu.europa.ted.efx.model.rules.RuleNature;
import eu.europa.ted.efx.model.rules.ValidationStage;
import eu.europa.ted.efx.model.variables.Variable;
import freemarker.template.Configuration;
Expand Down Expand Up @@ -69,31 +71,59 @@ public SchematronGenerator() {
// #region ValidatorMarkupGenerator Implementation

@Override
public Map<String, String> generateOutput(CompleteValidation completeValidation) {
logger.debug("Generating Schematron output from {} stages", completeValidation.getStages().size());
public Map<String, String> generateOutput(ValidationPlan validationPlan) {
logger.debug("Generating Schematron output from {} stages", validationPlan.getStages().size());

// Create local state for this generation run
SchematronSchema schema = new SchematronSchema("eForms schematron rules");
List<SchematronPattern> patterns = new ArrayList<>();
Map<String, SchematronDiagnostic> diagnosticsMap = new LinkedHashMap<>();

// Add endpoint params to schema
for (Map.Entry<String, String> endpoint : validationPlan.getEndpoints().entrySet()) {
String url = endpoint.getValue() != null ? endpoint.getValue() : "";
SchematronParam param = new SchematronParam("apiUrl-" + endpoint.getKey(), "'" + url + "'");
schema.addParam(param);
logger.debug("Added endpoint param: {} = {}", param.getName(), param.getValue());
}

// Add global variables to schema
for (Variable variable : completeValidation.getGlobalVariables()) {
String xpathValue = variable.initializationExpression.getScript();
SchematronLet globalVar = new SchematronLet(variable.name, xpathValue);
schema.addGlobalVariable(globalVar);
logger.debug("Added global variable: {} = {}", variable.name, xpathValue);
for (Variable variable : validationPlan.getVariables()) {
SchematronLet letElement = new SchematronLet(variable);
schema.addLetElement(letElement);
logger.debug("Added global variable: {} = {}", variable.name, variable.initializationExpression.getScript());
}

// Transform intermediate model (ValidationStage) to Schematron model (SchematronPattern)
transformStagesToPatterns(completeValidation.getStages(), patterns, diagnosticsMap);
for (ValidationStage stage : validationPlan.getStages()) {
if (stage.containsUniversalRules()) {
SchematronPattern sharedPattern = new SchematronPattern(stage);
if (sharedPattern.hasRules()) {
patterns.add(sharedPattern);
diagnosticsMap.putAll(sharedPattern.getDiagnostics());
logger.debug("Created shared pattern {} for stage {}",
sharedPattern.getId(), stage.getName());
}
}
for (String noticeSubtype : stage.getNoticeSubtypes()) {
SchematronPattern pattern = new SchematronPattern(stage, noticeSubtype);
if (pattern.hasRules()) {
patterns.add(pattern);
diagnosticsMap.putAll(pattern.getDiagnostics());
logger.debug("Created pattern {} for stage {} / notice subtype {}",
pattern.getId(), stage.getName(), noticeSubtype);
}
}
}

// Add collected diagnostics to schema
addDiagnosticsToSchema(diagnosticsMap, schema);
for (SchematronDiagnostic diagnostic : diagnosticsMap.values()) {
schema.addDiagnostic(diagnostic);
}

// Generate all output files
try {
return generateOutputFiles(completeValidation.getNoticeSubtypes(), patterns, schema);
return this.generateOutputFiles(validationPlan.getNoticeSubtypes(), patterns, schema);
} catch (IOException e) {
throw new RuntimeException("Failed to generate Schematron output", e);
}
Expand Down Expand Up @@ -135,61 +165,20 @@ public String generatePattern(SchematronPattern pattern, SchematronOutputConfig

Map<String, Object> model = new HashMap<>();
model.put("id", pattern.getId());
model.put("variables", pattern.getVariables());
List<String> tags = config.ruleNatures().stream()
.map(Enum::name).collect(Collectors.toList());
List<SchematronLet> letElements = pattern.getLetElements().stream()
.filter(v -> tags.contains(v.getTag())).collect(Collectors.toList());
model.put("letElements", letElements);
model.put("rules", pattern.getRules());
model.put("tags", config.ruleNatures().stream()
.map(Enum::name)
.collect(Collectors.toList()));
model.put("tags", tags);

template.process(model, writer);
return writer.toString();
}

// #endregion Freemarker Template Methods

// #region Transformation Methods

/**
* Transforms validation stages into Schematron patterns.
* For each stage, first creates a shared pattern for rules that apply to all subtypes,
* then creates subtype-specific patterns for the remaining rules.
*/
private void transformStagesToPatterns(List<ValidationStage> stages,
List<SchematronPattern> patterns, Map<String, SchematronDiagnostic> diagnosticsMap) {
for (ValidationStage stage : stages) {
// Create shared pattern for rules that apply to all subtypes
if (stage.containsUniversalRules()) {
SchematronPattern sharedPattern = new SchematronPattern(stage);
if (sharedPattern.hasRules()) {
patterns.add(sharedPattern);
diagnosticsMap.putAll(sharedPattern.getDiagnostics());
logger.debug("Created shared pattern {} for stage {}",
sharedPattern.getId(), stage.getName());
}
}

// Create subtype-specific patterns for remaining rules
for (String noticeSubtype : stage.getNoticeSubtypes()) {
SchematronPattern pattern = new SchematronPattern(stage, noticeSubtype);
if (pattern.hasRules()) {
patterns.add(pattern);
diagnosticsMap.putAll(pattern.getDiagnostics());
logger.debug("Created pattern {} for stage {} / notice subtype {}",
pattern.getId(), stage.getName(), noticeSubtype);
}
}
}
}

private void addDiagnosticsToSchema(Map<String, SchematronDiagnostic> diagnosticsMap,
SchematronSchema schema) {
for (SchematronDiagnostic diagnostic : diagnosticsMap.values()) {
schema.addDiagnostic(diagnostic);
}
}

// #endregion Transformation Methods

// #region Output Generation Methods

/**
Expand Down Expand Up @@ -219,11 +208,11 @@ private Map<String, String> generateOutputFiles(List<String> noticeSubtypeIds,

try {
for (SchematronOutputConfig config : configs) {
generateOutputForConfig(config, noticeSubtypeIds, patterns, baseSchema, outputFiles, schematronsMetadata);
this.generateOutputForConfig(config, noticeSubtypeIds, patterns, baseSchema, outputFiles, schematronsMetadata);
}

// Generate schematrons.json with entries from all configurations
String schematronsJson = generateSchematronsJson(schematronsMetadata);
String schematronsJson = this.generateSchematronsJson(schematronsMetadata);
outputFiles.put("schematrons.json", schematronsJson);

logger.debug("Generated {} Schematron files", outputFiles.size());
Expand Down Expand Up @@ -251,8 +240,18 @@ private void generateOutputForConfig(

// Create a fresh schema for this configuration
SchematronSchema schema = new SchematronSchema(baseSchema.getTitle());
for (SchematronLet globalVar : baseSchema.getGlobalVariables()) {
schema.addGlobalVariable(globalVar);
// API endpoint params are only relevant for configurations that include dynamic rules
if (config.ruleNatures().contains(RuleNature.DYNAMIC)) {
for (SchematronParam param : baseSchema.getParams()) {
schema.addParam(param);
}
}
Set<String> configTags = config.ruleNatures().stream()
.map(Enum::name).collect(Collectors.toSet());
for (SchematronLet letElement : baseSchema.getLetElements()) {
if (configTags.contains(letElement.getTag())) {
schema.addLetElement(letElement);
}
}
for (SchematronDiagnostic diagnostic : baseSchema.getDiagnostics()) {
schema.addDiagnostic(diagnostic);
Expand Down Expand Up @@ -307,9 +306,9 @@ private void generateOutputForConfig(
}

// Generate complete-validation.sch for this configuration
String completeValidation = generateCompleteValidation(schema);
String completeValidationContent = generateCompleteValidation(schema);
String completeFilename = folderPrefix + "complete-validation.sch";
outputFiles.put(completeFilename, completeValidation);
outputFiles.put(completeFilename, completeValidationContent);

// Add complete-validation to metadata at the beginning of this config's entries
Map<String, Object> completeMetadata = new LinkedHashMap<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 European Union
* Copyright 2025 European Union
*
* Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European
* Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in
Expand All @@ -13,16 +13,32 @@
*/
package eu.europa.ted.eforms.sdk.schematron;

import eu.europa.ted.efx.model.rules.RuleNature;
import eu.europa.ted.efx.model.variables.DynamicVariable;
import eu.europa.ted.efx.model.variables.Variable;

/**
* Represents a Schematron &lt;let&gt; element for variable declarations.
*/
public class SchematronLet {
private final String name;
private final String value;
private final RuleNature nature;

public SchematronLet(final Variable variable) {
this(variable.name, variable.initializationExpression.getScript(),
variable instanceof DynamicVariable || variable.hasDynamicDependencies()
? RuleNature.DYNAMIC : RuleNature.STATIC);
}

public SchematronLet(String name, String value) {
this(name, value, RuleNature.STATIC);
}

public SchematronLet(String name, String value, RuleNature nature) {
this.name = name;
this.value = value;
this.nature = nature;
}

/** Used by pattern.ftl and complete-validation.ftl */
Expand All @@ -34,4 +50,9 @@ public String getName() {
public String getValue() {
return this.value;
}

/** Used by pattern.ftl — returns the tag for filtering (derived from nature) */
public String getTag() {
return this.nature.name();
}
}
Loading
Loading