diff --git a/pom.xml b/pom.xml
index b0234842..5d555db4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -48,11 +48,12 @@
1.13.0
2.0.0-SNAPSHOT
- 1.5.0
+ 1.6.0-SNAPSHOT
4.13.1
3.18.0
+ 2.3.31
1.5.18
2.18.3
2.18.3
@@ -121,6 +122,11 @@
antlr4-runtime
${version.antlr4}
+
+ org.freemarker
+ freemarker
+ ${version.freemarker}
+
@@ -146,6 +152,10 @@
antlr4-runtime
compile
+
+ org.freemarker
+ freemarker
+
com.fasterxml.jackson.core
jackson-core
diff --git a/src/main/java/eu/europa/ted/eforms/sdk/ComponentFactory.java b/src/main/java/eu/europa/ted/eforms/sdk/ComponentFactory.java
index 41671d0d..33db51b9 100644
--- a/src/main/java/eu/europa/ted/eforms/sdk/ComponentFactory.java
+++ b/src/main/java/eu/europa/ted/eforms/sdk/ComponentFactory.java
@@ -11,6 +11,7 @@
import eu.europa.ted.efx.interfaces.ScriptGenerator;
import eu.europa.ted.efx.interfaces.SymbolResolver;
import eu.europa.ted.efx.interfaces.TranslatorOptions;
+import eu.europa.ted.efx.interfaces.ValidatorGenerator;
public class ComponentFactory extends SdkComponentFactory {
public static final ComponentFactory INSTANCE = new ComponentFactory();
@@ -96,7 +97,7 @@ public static synchronized SymbolResolver getSymbolResolver(final String sdkVers
// Create new instance (this can throw InstantiationException)
SymbolResolver newInstance = ComponentFactory.INSTANCE.getComponentImpl(sdkVersion,
SdkComponentType.SYMBOL_RESOLVER, qualifier, SymbolResolver.class, sdkVersion, sdkRootPath);
-
+
// Store and return
instances.put(key, newInstance);
return newInstance;
@@ -113,6 +114,18 @@ public static MarkupGenerator getMarkupGenerator(final String sdkVersion, final
SdkComponentType.MARKUP_GENERATOR, qualifier, MarkupGenerator.class, options);
}
+ public static ValidatorGenerator getValidatorGenerator(final String sdkVersion,
+ TranslatorOptions options) throws InstantiationException {
+ return getValidatorGenerator(sdkVersion, "", options);
+ }
+
+ public static ValidatorGenerator getValidatorGenerator(final String sdkVersion,
+ final String qualifier, TranslatorOptions options) throws InstantiationException {
+ return ComponentFactory.INSTANCE.getComponentImpl(sdkVersion,
+ SdkComponentType.VALIDATOR_GENERATOR, qualifier, ValidatorGenerator.class,
+ options);
+ }
+
public static ScriptGenerator getScriptGenerator(final String sdkVersion, TranslatorOptions options)
throws InstantiationException {
return getScriptGenerator(sdkVersion, "", options);
diff --git a/src/main/java/eu/europa/ted/eforms/sdk/SdkSymbolResolver.java b/src/main/java/eu/europa/ted/eforms/sdk/SdkSymbolResolver.java
index b8317013..2c403dbd 100644
--- a/src/main/java/eu/europa/ted/eforms/sdk/SdkSymbolResolver.java
+++ b/src/main/java/eu/europa/ted/eforms/sdk/SdkSymbolResolver.java
@@ -14,9 +14,11 @@
import eu.europa.ted.eforms.sdk.entity.SdkCodelist;
import eu.europa.ted.eforms.sdk.entity.SdkField;
import eu.europa.ted.eforms.sdk.entity.SdkNode;
+import eu.europa.ted.eforms.sdk.entity.SdkNoticeSubtype;
import eu.europa.ted.eforms.sdk.repository.SdkCodelistRepository;
import eu.europa.ted.eforms.sdk.repository.SdkFieldRepository;
import eu.europa.ted.eforms.sdk.repository.SdkNodeRepository;
+import eu.europa.ted.eforms.sdk.repository.SdkNoticeTypeRepository;
import eu.europa.ted.eforms.sdk.resource.SdkResourceLoader;
import eu.europa.ted.eforms.xpath.XPathInfo;
import eu.europa.ted.eforms.xpath.XPathProcessor;
@@ -41,6 +43,8 @@ public class SdkSymbolResolver implements SymbolResolver {
protected Map codelistById;
+ protected Map noticeTypesById;
+
/**
* Builds EFX list from the passed codelist reference. This will lazily compute
* and cache the
@@ -77,12 +81,15 @@ protected void loadMapData(final String sdkVersion, final Path sdkRootPath)
SdkConstants.SdkResource.FIELDS_JSON, sdkRootPath);
Path codelistsPath = SdkResourceLoader.getResourceAsPath(sdkVersion,
SdkConstants.SdkResource.CODELISTS, sdkRootPath);
+ Path noticeTypesPath = SdkResourceLoader.getResourceAsPath(sdkVersion,
+ SdkConstants.SdkResource.NOTICE_TYPES_JSON, sdkRootPath);
this.fieldById = new SdkFieldRepository(sdkVersion, jsonPath);
this.fieldByAlias = indexFieldsByAlias();
this.nodeById = new SdkNodeRepository(sdkVersion, jsonPath);
this.nodeByAlias = indexNodesByAlias();
this.codelistById = new SdkCodelistRepository(sdkVersion, codelistsPath);
+ this.noticeTypesById = new SdkNoticeTypeRepository(sdkVersion, noticeTypesPath);
}
/**
@@ -227,6 +234,10 @@ public String getNodeIdFromAlias(String alias) {
return null;
}
+ @Override
+ public List getAllNoticeSubtypeIds() {
+ return noticeTypesById.keySet().stream().map(String::toUpperCase).sorted().toList();
+ }
private HashMap indexFieldsByAlias() {
return this.fieldById.values().stream()
@@ -268,8 +279,8 @@ private void cacheAdditionalFieldInfo(final String fieldId) {
}
XPathInfo xpathInfo = XPathProcessor.parse(this.getAbsolutePathOfField(fieldId).getScript());
additionalFieldInfoMap.put(fieldId, xpathInfo);
- }
-
+ }
+
// #endregion Temporary helpers ------------------------------------------------
}
diff --git a/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronAssert.java b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronAssert.java
new file mode 100644
index 00000000..1a89197f
--- /dev/null
+++ b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronAssert.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2022 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
+ * compliance with the Licence. You may obtain a copy of the Licence at:
+ * https://joinup.ec.europa.eu/software/page/eupl
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the Licence
+ * is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the Licence for the specific language governing permissions and limitations under
+ * the Licence.
+ */
+package eu.europa.ted.eforms.sdk.schematron;
+
+import eu.europa.ted.efx.model.Context;
+import eu.europa.ted.efx.model.rules.ValidationRule;
+
+/**
+ * Represents a Schematron <assert> element.
+ * Fires when the test expression evaluates to false.
+ */
+public class SchematronAssert extends SchematronTest {
+
+ public SchematronAssert(ValidationRule rule, Context ruleContext) {
+ super(rule, ruleContext);
+ }
+
+ @Override
+ public String getElementName() {
+ return "assert";
+ }
+}
diff --git a/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronDiagnostic.java b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronDiagnostic.java
new file mode 100644
index 00000000..749d0e6a
--- /dev/null
+++ b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronDiagnostic.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2022 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
+ * compliance with the Licence. You may obtain a copy of the Licence at:
+ * https://joinup.ec.europa.eu/software/page/eupl
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the Licence
+ * is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the Licence for the specific language governing permissions and limitations under
+ * the Licence.
+ */
+package eu.europa.ted.eforms.sdk.schematron;
+
+import eu.europa.ted.eforms.xpath.XPathProcessor;
+import eu.europa.ted.efx.model.Context;
+
+/**
+ * Represents a Schematron <diagnostic> element.
+ * Used in the <diagnostics> section of complete-validation.sch.
+ * Format: <diagnostic id="..." see="field:...">xpath</diagnostic>
+ */
+public class SchematronDiagnostic {
+
+ private final String id;
+ private final String seeAttribute;
+ private final String xpath;
+
+ private static String sanitize(String identifier) {
+ return identifier.replace("(", "_").replace(")", "_");
+ }
+
+ public SchematronDiagnostic(Context subject, Context ruleContext) {
+ this.xpath = XPathProcessor.contextualize(
+ ruleContext.absolutePath().getScript(), subject.absolutePath().getScript());
+ this.id = sanitize(ruleContext.symbol()) + "_" + sanitize(subject.symbol());
+ String prefix = subject.isFieldContext() ? "field:" : "node:";
+ this.seeAttribute = prefix + subject.symbol();
+ }
+
+ /** Used by pattern.ftl and complete-validation.ftl */
+ public String getId() {
+ return this.id;
+ }
+
+ /** Used by complete-validation.ftl */
+ public String getSeeAttribute() {
+ return this.seeAttribute;
+ }
+
+ /** Used by complete-validation.ftl */
+ public String getXpath() {
+ return this.xpath;
+ }
+}
diff --git a/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronGenerator.java b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronGenerator.java
new file mode 100644
index 00000000..33108d3e
--- /dev/null
+++ b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronGenerator.java
@@ -0,0 +1,363 @@
+/*
+ * Copyright 2022 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
+ * compliance with the Licence. You may obtain a copy of the Licence at:
+ * https://joinup.ec.europa.eu/software/page/eupl
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the Licence
+ * is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the Licence for the specific language governing permissions and limitations under
+ * the Licence.
+ */
+package eu.europa.ted.eforms.sdk.schematron;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+
+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.ValidationStage;
+import eu.europa.ted.efx.model.variables.Variable;
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+import freemarker.template.TemplateExceptionHandler;
+
+/**
+ * Generates Schematron XML markup using Freemarker templates.
+ *
+ * This class is responsible for transforming the intermediate validation model
+ * (ValidationStage) into Schematron XML format. It implements ValidatorGenerator
+ * to provide a clean separation between translation and output generation.
+ */
+@SdkComponent(versions = {"2"}, componentType = SdkComponentType.VALIDATOR_GENERATOR)
+public class SchematronGenerator implements ValidatorGenerator {
+
+ private static final Logger logger = LoggerFactory.getLogger(SchematronGenerator.class);
+
+ private static final ObjectMapper JSON_MAPPER = new ObjectMapper()
+ .enable(SerializationFeature.INDENT_OUTPUT);
+
+ private final Configuration freemarkerConfig;
+
+ public SchematronGenerator() {
+ this.freemarkerConfig = new Configuration(Configuration.VERSION_2_3_31);
+ this.freemarkerConfig.setClassForTemplateLoading(SchematronGenerator.class,
+ "/freemarker/schematron");
+ this.freemarkerConfig.setDefaultEncoding("UTF-8");
+ this.freemarkerConfig.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
+ this.freemarkerConfig.setLogTemplateExceptions(false);
+ }
+
+ // #region ValidatorMarkupGenerator Implementation
+
+ @Override
+ public Map generateOutput(
+ CompleteValidation completeValidation
+ ) throws IOException {
+ logger.debug("Generating Schematron output from {} stages", completeValidation.getStages().size());
+
+ // Create local state for this generation run
+ SchematronSchema schema = new SchematronSchema("eForms schematron rules");
+ List patterns = new ArrayList<>();
+ Map diagnosticsMap = new LinkedHashMap<>();
+
+ // 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);
+ }
+
+ // Transform intermediate model (ValidationStage) to Schematron model (SchematronPattern)
+ transformStagesToPatterns(completeValidation.getStages(), patterns, diagnosticsMap);
+
+ // Add collected diagnostics to schema
+ addDiagnosticsToSchema(diagnosticsMap, schema);
+
+ // Generate all output files
+ return generateOutputFiles(completeValidation.getNoticeSubtypes(), patterns, schema);
+ }
+
+ // #endregion ValidatorMarkupGenerator Implementation
+
+ // #region Freemarker Template Methods
+
+ /**
+ * Generates the complete-validation.sch file content.
+ *
+ * @param schema The SchematronSchema object containing global variables, phases, and includes.
+ * @return The XML content of complete-validation.sch as a string.
+ * @throws IOException If template loading fails.
+ * @throws TemplateException If template processing fails.
+ */
+ public String generateCompleteValidation(SchematronSchema schema)
+ throws IOException, TemplateException {
+ Template template = freemarkerConfig.getTemplate("complete-validation.ftl");
+ StringWriter writer = new StringWriter();
+ template.process(schema, writer);
+ return writer.toString();
+ }
+
+ /**
+ * Generates a single pattern file content (e.g., validation-stage-1a.sch).
+ *
+ * @param pattern The SchematronPattern object containing rules and assertions.
+ * @param config The output configuration specifying which rule natures to include.
+ * @return The XML content of the pattern file as a string.
+ * @throws IOException If template loading fails.
+ * @throws TemplateException If template processing fails.
+ */
+ public String generatePattern(SchematronPattern pattern, SchematronOutputConfig config)
+ throws IOException, TemplateException {
+ Template template = freemarkerConfig.getTemplate("pattern.ftl");
+ StringWriter writer = new StringWriter();
+
+ Map model = new HashMap<>();
+ model.put("id", pattern.getId());
+ model.put("variables", pattern.getVariables());
+ model.put("rules", pattern.getRules());
+ model.put("tags", config.ruleNatures().stream()
+ .map(Enum::name)
+ .toList());
+
+ template.process(model, writer);
+ return writer.toString();
+ }
+
+ // #endregion Freemarker Template Methods
+
+ // #region Transformation Methods
+
+ /**
+ * Transforms validation stages into Schematron patterns.
+ * Creates one pattern per (stage, noticeType) combination, where each pattern
+ * only contains the rules/assertions that apply to that specific notice type.
+ */
+ private void transformStagesToPatterns(List stages,
+ List patterns, Map diagnosticsMap) {
+ for (ValidationStage stage : stages) {
+ // Get all notice types referenced in this stage
+ for (String noticeType : SchematronPattern.getNoticeTypesInStage(stage)) {
+ SchematronPattern pattern = new SchematronPattern(stage, noticeType);
+ if (pattern.hasRules()) {
+ patterns.add(pattern);
+ diagnosticsMap.putAll(pattern.getDiagnostics());
+ logger.debug("Created pattern {} for stage {} / notice type {}",
+ pattern.getId(), stage.getName(), noticeType);
+ }
+ }
+ }
+ }
+
+ private void addDiagnosticsToSchema(Map diagnosticsMap,
+ SchematronSchema schema) {
+ for (SchematronDiagnostic diagnostic : diagnosticsMap.values()) {
+ schema.addDiagnostic(diagnostic);
+ }
+ }
+
+ // #endregion Transformation Methods
+
+ // #region Output Generation Methods
+
+ /**
+ * Generates all Schematron output files from the processed patterns and schema.
+ * This includes individual pattern files, the complete-validation.sch master file,
+ * and the schematrons.json metadata file.
+ *
+ * Output is organized into subfolders based on rule nature:
+ * - dynamic/: Contains all rules (static + dynamic) for full validation
+ * - static/: Contains only static rules for validation without external services
+ *
+ * @return A map of filename to file content for all generated Schematron files
+ * @throws IOException If an error occurs during file generation
+ */
+ private Map generateOutputFiles(List noticeTypeIds,
+ List patterns, SchematronSchema baseSchema) throws IOException {
+ logger.debug("Generating Schematron output files");
+
+ Map outputFiles = new HashMap<>();
+ List