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 e7e2baf5..faefcaf7 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/SdkSymbolResolver.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/SdkSymbolResolver.java @@ -1,9 +1,24 @@ +/* + * 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; import java.nio.file.Path; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; @@ -20,24 +35,25 @@ 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; import eu.europa.ted.efx.interfaces.SymbolResolver; -import eu.europa.ted.efx.model.expressions.Expression; -import eu.europa.ted.efx.model.expressions.path.NodePathExpression; -import eu.europa.ted.efx.model.expressions.path.PathExpression; +import eu.europa.ted.efx.model.expressions.PathExpression; +import eu.europa.ted.efx.model.expressions.scalar.NodePath; +import eu.europa.ted.efx.model.expressions.scalar.ScalarPath; +import eu.europa.ted.efx.model.expressions.sequence.NodeSequencePath; +import eu.europa.ted.efx.model.expressions.sequence.SequencePath; import eu.europa.ted.efx.model.types.FieldTypes; -import eu.europa.ted.efx.sdk2.entity.SdkFieldV2; -import eu.europa.ted.efx.sdk2.entity.SdkNodeV2; +import eu.europa.ted.eforms.sdk.entity.v2.SdkFieldV2; +import eu.europa.ted.eforms.sdk.entity.v2.SdkNodeV2; import eu.europa.ted.efx.xpath.XPathContextualizer; @SdkComponent(versions = { "1", "2" }, componentType = SdkComponentType.SYMBOL_RESOLVER) public class SdkSymbolResolver implements SymbolResolver { - protected Map fieldById; + protected SdkFieldRepository fieldById; protected Map fieldByAlias; - protected Map nodeById; + protected SdkNodeRepository nodeById; protected Map nodeByAlias; @@ -45,15 +61,8 @@ public class SdkSymbolResolver implements SymbolResolver { protected Map noticeTypesById; - /** - * Builds EFX list from the passed codelist reference. This will lazily compute - * and cache the - * result for reuse as the operation can be costly on some large lists. - * - * @param codelistId A reference to an SDK codelist. - * @return The EFX string representation of the list of all the codes of the - * referenced codelist. - */ + private SdkNode cachedRootNode; + @Override public final List expandCodelist(final String codelistId) { final SdkCodelist codelist = codelistById.get(codelistId); @@ -64,7 +73,14 @@ public final List expandCodelist(final String codelistId) { } /** - * Private, use getInstance method instead. + * Protected constructor for subclasses that load data differently (e.g., test mocks). + * Subclasses must populate the field/node/codelist maps themselves. + */ + protected SdkSymbolResolver() { + } + + /** + * Creates a symbol resolver by loading SDK metadata from the given path. * * @param sdkVersion The version of the SDK. * @param sdkRootPath The path to the root of the SDK. @@ -84,53 +100,70 @@ protected void loadMapData(final String sdkVersion, final Path sdkRootPath) Path noticeTypesPath = SdkResourceLoader.getResourceAsPath(sdkVersion, SdkConstants.SdkResource.NOTICE_TYPES_JSON, sdkRootPath); - this.fieldById = new SdkFieldRepository(sdkVersion, jsonPath); - this.fieldByAlias = indexFieldsByAlias(); + // Load nodes first (fields depend on nodes for parent wiring) this.nodeById = new SdkNodeRepository(sdkVersion, jsonPath); this.nodeByAlias = indexNodesByAlias(); + + // Load fields with parent node wiring + this.fieldById = new SdkFieldRepository(sdkVersion, jsonPath, this.nodeById); + this.fieldByAlias = indexFieldsByAlias(); + this.codelistById = new SdkCodelistRepository(sdkVersion, codelistsPath); this.noticeTypesById = new SdkNoticeTypeRepository(sdkVersion, noticeTypesPath); } - /** - * Gets the id of the parent node of a given field. - * - * @param fieldId The id of the field who's parent node we are looking for. - * @return The id of the parent node of the given field. - */ @Override public String getParentNodeOfField(final String fieldId) { - final SdkField sdkField = fieldById.get(fieldId); + final SdkField sdkField = this.resolveField(fieldId); if (sdkField != null) { return sdkField.getParentNodeId(); } - throw SymbolResolutionException.unknownField(fieldId); + throw SymbolResolutionException.unknownSymbol(fieldId); } /** * @param fieldId The id of a field. - * @return The xPath of the given field. + * @return The xPath of the given field as a {@link SequencePath} if the field + * is repeatable from root context, {@link ScalarPath} otherwise. */ @Override public PathExpression getAbsolutePathOfField(final String fieldId) { - final SdkField sdkField = fieldById.get(fieldId); + final SdkField sdkField = this.resolveField(fieldId); if (sdkField == null) { - throw SymbolResolutionException.unknownField(fieldId); + throw SymbolResolutionException.unknownSymbol(fieldId); + } + return this.getAbsolutePathOfField(sdkField); + } + + private PathExpression getAbsolutePathOfField(final SdkField sdkField) { + FieldTypes fieldType = FieldTypes.fromString(sdkField.getType()); + if (this.isFieldRepeatableFromContext(sdkField, this.getRootNode())) { + return SequencePath.instantiate(sdkField.getXpathAbsolute(), fieldType); + } else { + return ScalarPath.instantiate(sdkField.getXpathAbsolute(), fieldType); } - return PathExpression.instantiate(sdkField.getXpathAbsolute(), FieldTypes.fromString(sdkField.getType())); } /** - * @param nodeId The id of a node or a field. - * @return The xPath of the given node or field. + * @param nodeId The id of a node. + * @return The xPath of the given node as a {@link NodeSequencePath} if + * the node is repeatable from root context, {@link NodePath} otherwise. */ @Override public PathExpression getAbsolutePathOfNode(final String nodeId) { - final SdkNode sdkNode = nodeById.get(nodeId); + final SdkNode sdkNode = this.resolveNode(nodeId); if (sdkNode == null) { - throw SymbolResolutionException.unknownNode(nodeId); + throw SymbolResolutionException.unknownSymbol(nodeId); + } + return this.getAbsolutePathOfNode(sdkNode); + } + + private PathExpression getAbsolutePathOfNode(final SdkNode sdkNode) { + if (this.isNodeRepeatableFromContext(sdkNode, null)) { + return new NodeSequencePath(sdkNode.getXpathAbsolute()); + } else { + return new NodePath(sdkNode.getXpathAbsolute()); } - return new NodePathExpression(sdkNode.getXpathAbsolute()); } /** @@ -140,11 +173,57 @@ public PathExpression getAbsolutePathOfNode(final String nodeId) { * xPath. * @param contextPath xPath indicating the context. * @return The xPath of the given field relative to the given context. + * @deprecated Use {@link #getRelativePathOfField(String, String)} instead. */ + @Deprecated(forRemoval = true) @Override public PathExpression getRelativePathOfField(String fieldId, PathExpression contextPath) { - final PathExpression xpath = getAbsolutePathOfField(fieldId); - return XPathContextualizer.contextualize(contextPath, xpath); + return XPathContextualizer.contextualize(contextPath, this.getAbsolutePathOfField(fieldId)); + } + + @Override + public PathExpression getRelativePathOfField(String fieldId, String contextId) { + SdkField sdkField = this.resolveField(fieldId); + if (sdkField == null) { + throw SymbolResolutionException.unknownSymbol(fieldId); + } + + // null contextId means root context - return absolute path with correct type + if (contextId == null) { + return this.getAbsolutePathOfField(sdkField); + } + + var context = this.resolveSymbol(contextId); + if (context.isEmpty()) { + throw SymbolResolutionException.unknownSymbol(contextId); + } + + if (context.isField()) { + return this.getRelativePathOfField(sdkField, context.field); + } + + return this.getRelativePathOfField(sdkField, context.node); + } + + private PathExpression getRelativePathOfField(SdkField sdkField, SdkNode context) { + + String relativeScript = XPathProcessor.contextualize(context.getXpathAbsolute(), sdkField.getXpathAbsolute()); + + if (isFieldRepeatableFromContext(sdkField, context)) { + return SequencePath.instantiate(relativeScript, FieldTypes.fromString(sdkField.getType())); + } else { + return ScalarPath.instantiate(relativeScript, FieldTypes.fromString(sdkField.getType())); + } + } + + private PathExpression getRelativePathOfField(SdkField sdkField, SdkField context) { + String relativeScript = XPathProcessor.contextualize(context.getXpathAbsolute(), sdkField.getXpathAbsolute()); + + if (this.isFieldRepeatableFromContext(sdkField, context)) { + return SequencePath.instantiate(relativeScript, FieldTypes.fromString(sdkField.getType())); + } else { + return ScalarPath.instantiate(relativeScript, FieldTypes.fromString(sdkField.getType())); + } } /** @@ -154,13 +233,58 @@ public PathExpression getRelativePathOfField(String fieldId, PathExpression cont * xPath. * @param contextPath XPath indicating the context. * @return The XPath of the given node relative to the given context. + * @deprecated Use {@link #getRelativePathOfNode(String, String)} instead. */ + @Deprecated(forRemoval = true) @Override public PathExpression getRelativePathOfNode(String nodeId, PathExpression contextPath) { - final PathExpression xpath = getAbsolutePathOfNode(nodeId); - return XPathContextualizer.contextualize(contextPath, xpath); + return XPathContextualizer.contextualize(contextPath, this.getAbsolutePathOfNode(nodeId)); + } + + @Override + public PathExpression getRelativePathOfNode(String nodeId, String contextId) { + SdkNode sdkNode = this.resolveNode(nodeId); + if (sdkNode == null) { + throw SymbolResolutionException.unknownSymbol(nodeId); + } + + // null contextId means root context - return absolute path with correct type + if (contextId == null) { + return this.getAbsolutePathOfNode(sdkNode); + } + + var context = this.resolveSymbol(contextId); + if (context.isEmpty()) { + throw SymbolResolutionException.unknownSymbol(contextId); + } + if (context.isField()) { + return this.getRelativePathOfNode(sdkNode, context.field); + } + + return this.getRelativePathOfNode(sdkNode, context.node); + } + + private PathExpression getRelativePathOfNode(SdkNode sdkNode, SdkNode context) { + String relativeScript = XPathProcessor.contextualize(context.getXpathAbsolute(), sdkNode.getXpathAbsolute()); + + if (this.isNodeRepeatableFromContext(sdkNode, context)) { + return new NodeSequencePath(relativeScript); + } else { + return new NodePath(relativeScript); + } + } + + private PathExpression getRelativePathOfNode(SdkNode sdkNode, SdkField context) { + String relativeScript = XPathProcessor.contextualize(context.getXpathAbsolute(), sdkNode.getXpathAbsolute()); + + if (this.isNodeRepeatableFromContext(sdkNode, context.getParentNode())) { + return new NodeSequencePath(relativeScript); + } else { + return new NodePath(relativeScript); + } } + @Deprecated(forRemoval = true) @Override public PathExpression getRelativePath(PathExpression absolutePath, PathExpression contextPath) { return XPathContextualizer.contextualize(contextPath, absolutePath); @@ -168,18 +292,18 @@ public PathExpression getRelativePath(PathExpression absolutePath, PathExpressio @Override public String getTypeOfField(String fieldId) { - final SdkField sdkField = fieldById.get(fieldId); + final SdkField sdkField = this.resolveField(fieldId); if (sdkField == null) { - throw SymbolResolutionException.unknownField(fieldId); + throw SymbolResolutionException.unknownSymbol(fieldId); } return sdkField.getType(); } @Override public String getRootCodelistOfField(final String fieldId) { - final SdkField sdkField = fieldById.get(fieldId); + final SdkField sdkField = this.resolveField(fieldId); if (sdkField == null) { - throw SymbolResolutionException.unknownField(fieldId); + throw SymbolResolutionException.unknownSymbol(fieldId); } final String codelistId = sdkField.getCodelistId(); if (codelistId == null) { @@ -196,26 +320,37 @@ public String getRootCodelistOfField(final String fieldId) { @Override public boolean isAttributeField(final String fieldId) { - if (!additionalFieldInfoMap.containsKey(fieldId)) { - this.cacheAdditionalFieldInfo(fieldId); + final SdkField sdkField = this.resolveField(fieldId); + if (sdkField == null) { + throw SymbolResolutionException.unknownSymbol(fieldId); } - return additionalFieldInfoMap.get(fieldId).isAttribute(); + return sdkField.getXpathInfo().isAttribute(); } @Override public String getAttributeNameFromAttributeField(final String fieldId) { - if (!additionalFieldInfoMap.containsKey(fieldId)) { - this.cacheAdditionalFieldInfo(fieldId); + final SdkField sdkField = this.resolveField(fieldId); + if (sdkField == null) { + throw SymbolResolutionException.unknownSymbol(fieldId); } - return additionalFieldInfoMap.get(fieldId).getAttributeName(); + return sdkField.getXpathInfo().getAttributeName(); } @Override public PathExpression getAbsolutePathOfFieldWithoutTheAttribute(final String fieldId) { - if (!additionalFieldInfoMap.containsKey(fieldId)) { - this.cacheAdditionalFieldInfo(fieldId); + final SdkField sdkField = this.resolveField(fieldId); + if (sdkField == null) { + throw SymbolResolutionException.unknownSymbol(fieldId); + } + + String pathToElement = sdkField.getXpathInfo().getPathToLastElement(); + SdkNode parentNode = sdkField.getParentNode(); + + if (parentNode != null && this.isNodeRepeatableFromContext(parentNode, null)) { + return new NodeSequencePath(pathToElement); + } else { + return new NodePath(pathToElement); } - return Expression.instantiate(additionalFieldInfoMap.get(fieldId).getPathToLastElement(), NodePathExpression.class); } @Override @@ -236,11 +371,10 @@ public String getNodeIdFromAlias(String alias) { @Override public List getAllNoticeSubtypeIds() { - return noticeTypesById.keySet().stream().map(String::toUpperCase).sorted() - .collect(Collectors.toList()); + return noticeTypesById.keySet().stream().map(String::toUpperCase).sorted().collect(Collectors.toList()); } - private HashMap indexFieldsByAlias() { + protected HashMap indexFieldsByAlias() { return this.fieldById.values().stream() .filter(SdkFieldV2.class::isInstance) .map(SdkFieldV2.class::cast) @@ -253,7 +387,7 @@ private HashMap indexFieldsByAlias() { )); } - private HashMap indexNodesByAlias() { + protected HashMap indexNodesByAlias() { return this.nodeById.values().stream() .filter(SdkNodeV2.class::isInstance) .map(SdkNodeV2.class::cast) @@ -266,22 +400,222 @@ private HashMap indexNodesByAlias() { )); } - // #region Temporary helpers ------------------------------------------------ + @Override + public boolean isFieldRepeatableFromContext(final String fieldId, final String contextNodeId) { + final SdkField sdkField = this.resolveField(fieldId); + if (sdkField == null) { + throw SymbolResolutionException.unknownSymbol(fieldId); + } + var contextNode = contextNodeId != null ? this.resolveNode(contextNodeId) : this.getRootNode(); + if (contextNode == null) { + throw SymbolResolutionException.unknownSymbol(contextNodeId); + } + return this.isFieldRepeatableFromContext(sdkField, contextNode); + } + + private boolean isFieldRepeatableFromContext(final SdkField sdkField, final SdkField context) { + // If the field itself is repeatable, it returns multiple values + if (sdkField.isRepeatable()) { + return true; + } - /** - * Caches the results of xpath parsing to mitigate performance impact. - * This is a temporary solution until we move the additional info to the SdkField class. - */ - Map additionalFieldInfoMap = new HashMap<>(); + // Use cached ancestry from node + List contextAncestry = context != null + ? context.getParentNode().getAncestry() + : Collections.emptyList(); + + // Walk up from the field's parent node toward root, looking for a repeatable + // node + String currentNodeId = sdkField.getParentNodeId(); + while (currentNodeId != null) { + // Context boundary reached - the context node (even if repeatable) doesn't + // count because we're positioned inside one instance of it. No repeatable node exists + // between the field and context, so the field doesn't repeat from this context. + if (contextAncestry.contains(currentNodeId)) { + return false; + } + + SdkNode node = nodeById.get(currentNodeId); + if (node == null) { + break; + } + + // If this node is repeatable, the field returns multiple values from context + if (node.isRepeatable()) { + return true; + } + + currentNodeId = node.getParentId(); + } + + return false; + } + + private boolean isFieldRepeatableFromContext(final SdkField sdkField, final SdkNode context) { + // If the field itself is repeatable, it returns multiple values + if (sdkField.isRepeatable()) { + return true; + } + + // Use cached ancestry from node + List contextAncestry = context != null + ? context.getAncestry() + : Collections.emptyList(); + + // Walk up from the field's parent node toward root, looking for a repeatable + // node + String currentNodeId = sdkField.getParentNodeId(); + while (currentNodeId != null) { + // Context boundary reached - the context node (even if repeatable) doesn't + // count + // because we're positioned inside one instance of it. No repeatable node exists + // between the field and context, so the field doesn't repeat from this context. + if (contextAncestry.contains(currentNodeId)) { + return false; + } + + SdkNode node = nodeById.get(currentNodeId); + if (node == null) { + break; + } + + // If this node is repeatable, the field returns multiple values from context + if (node.isRepeatable()) { + return true; + } + + currentNodeId = node.getParentId(); + } + + return false; + } + + @Override + public boolean isNodeRepeatableFromContext(final String nodeId, final String contextNodeId) { + final SdkNode sdkNode = this.resolveNode(nodeId); + if (sdkNode == null) { + throw SymbolResolutionException.unknownSymbol(nodeId); + } + + final SdkNode contextNode = contextNodeId != null + ? this.resolveNode(contextNodeId) + : null; + if (contextNodeId != null && contextNode == null) { + throw SymbolResolutionException.unknownSymbol(contextNodeId); + } + + return this.isNodeRepeatableFromContext(sdkNode, contextNode); + } + + private boolean isNodeRepeatableFromContext(final SdkNode sdkNode, final SdkNode contextNode) { + // Use cached ancestry from node + List contextAncestry = contextNode != null + ? contextNode.getAncestry() + : Collections.emptyList(); + + // Walk up from the node toward root, looking for a repeatable node + String currentNodeId = sdkNode.getId(); + while (currentNodeId != null) { + // Context boundary reached - the context node (even if repeatable) doesn't count + // because we're positioned inside one instance of it. No repeatable node exists + // between the target node and context, so it doesn't repeat from this context. + if (contextAncestry.contains(currentNodeId)) { + return false; + } + + SdkNode node = this.nodeById.get(currentNodeId); + if (node == null) { + break; + } + + // If this node is repeatable, return true + if (node.isRepeatable()) { + return true; + } + + currentNodeId = node.getParentId(); + } + + return false; + } + + @Override + public String getRootNodeId() { + return this.getRootNode().getId(); + } + + @Override + public PathExpression getRootPath() { + return new NodePath(this.getRootNode().getXpathAbsolute()); + } + + private SdkNode getRootNode() { + if (this.cachedRootNode == null) { + this.cachedRootNode = this.nodeById.values().stream() + .filter(node -> node.getParentId() == null) + .findFirst() + .orElseThrow(SymbolResolutionException::rootNodeNotFound); + } + return this.cachedRootNode; + } + // #region Identifier Resolution ------------------------------------------------ + + static class SdkEntity { + final SdkNode node; + final SdkField field; + + static SdkEntity EMPTY = new SdkEntity(null, null); + + static SdkEntity ofNode(SdkNode node) { + return new SdkEntity(node, null); + } + + static SdkEntity ofField(SdkField field) { + return new SdkEntity(null, field); + } - private void cacheAdditionalFieldInfo(final String fieldId) { - if (additionalFieldInfoMap.containsKey(fieldId)) { - return; + private SdkEntity(SdkNode node, SdkField field) { + this.node = node; + this.field = field; } - XPathInfo xpathInfo = XPathProcessor.parse(this.getAbsolutePathOfField(fieldId).getScript()); - additionalFieldInfoMap.put(fieldId, xpathInfo); + + boolean isNode() { + return this.node != null; + } + + boolean isField() { + return this.field != null; + } + + boolean isEmpty() { + return this.node == null && this.field == null; + } + } + + private SdkEntity resolveSymbol(String symbol) { + if (symbol == null) { + return SdkEntity.EMPTY; + } + + SdkNode node = this.resolveNode(symbol); + if (node != null) { + return SdkEntity.ofNode(node); + } + + SdkField field = this.resolveField(symbol); + return SdkEntity.ofField(field); + } + + private SdkField resolveField(String fieldId) { + return Optional.ofNullable(this.fieldById.get(fieldId)) + .orElseGet(() -> this.fieldByAlias.get(fieldId)); + } + + private SdkNode resolveNode(String nodeId) { + return Optional.ofNullable(this.nodeById.get(nodeId)) + .orElseGet(() -> this.nodeByAlias.get(nodeId)); } - // #endregion Temporary helpers ------------------------------------------------ + // #endregion Identifier Resolution ------------------------------------------------ } diff --git a/src/main/java/eu/europa/ted/efx/exceptions/ConsistencyCheckException.java b/src/main/java/eu/europa/ted/efx/exceptions/ConsistencyCheckException.java new file mode 100644 index 00000000..e156614b --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/exceptions/ConsistencyCheckException.java @@ -0,0 +1,103 @@ +/* + * Copyright 2026 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.efx.exceptions; + +/** + * Exception thrown when the toolkit encounters an internal consistency check failure. + * This indicates a bug in the toolkit implementation, not a user error. + * These are "should never happen" errors that suggest developer mistakes in the toolkit code. + */ +public class ConsistencyCheckException extends IllegalStateException { + + public enum ErrorCode { + TYPE_NOT_REGISTERED, + UNSUPPORTED_TYPE_IN_CONDITIONAL, + MISSING_TYPE_MAPPING, + MISSING_TYPE_ANNOTATION, + UNKNOWN_EXPRESSION_TYPE, + INVALID_VARIABLE_CONTEXT + } + + private static final String TYPE_NOT_REGISTERED = + "EfxDataType %s is not registered in TYPE_VARIANTS. " + + "This indicates a bug in the type system. " + + "Add the missing type to TYPE_VARIANTS with its scalar and sequence variants, " + + "ensuring subtypes appear before supertypes. " + + "Run EfxTypeLatticeTest to verify the registration."; + + private static final String UNSUPPORTED_TYPE_IN_CONDITIONAL = + "Type %s is not supported in conditional expressions. " + + "This indicates the translator is missing a handler for this type. " + + "Add an else-if branch for this type in exitConditionalExpression()."; + + private static final String MISSING_TYPE_MAPPING = + "Type %s is not mapped in %s. " + + "This indicates the translator is missing a type mapping. " + + "Add the missing type to the map."; + + private static final String MISSING_TYPE_ANNOTATION = + "TypedExpression class %s is missing @EfxDataTypeAssociation annotation. " + + "This indicates a bug in the type system. " + + "Add the annotation to specify which EfxDataType this expression represents."; + + private static final String UNKNOWN_EXPRESSION_TYPE = + "Expression type %s is not handled by the type conversion logic. " + + "This indicates a bug in the type system. " + + "The target type must be PathExpression, SequenceExpression, or ScalarExpression."; + + private static final String INVALID_VARIABLE_CONTEXT = + "Variable context is neither a field nor a node context. " + + "This indicates a bug in the translator. " + + "Ensure all variable contexts are properly classified as FieldContext or NodeContext."; + + private final ErrorCode errorCode; + + private ConsistencyCheckException(ErrorCode errorCode, String message) { + super(message); + this.errorCode = errorCode; + } + + public ErrorCode getErrorCode() { + return errorCode; + } + + public static ConsistencyCheckException typeNotRegistered(Class type) { + return new ConsistencyCheckException(ErrorCode.TYPE_NOT_REGISTERED, + String.format(TYPE_NOT_REGISTERED, type.getName())); + } + + public static ConsistencyCheckException unsupportedTypeInConditional(Class type) { + return new ConsistencyCheckException(ErrorCode.UNSUPPORTED_TYPE_IN_CONDITIONAL, + String.format(UNSUPPORTED_TYPE_IN_CONDITIONAL, type.getName())); + } + + public static ConsistencyCheckException missingTypeMapping(Class type, String mapName) { + return new ConsistencyCheckException(ErrorCode.MISSING_TYPE_MAPPING, + String.format(MISSING_TYPE_MAPPING, type.getName(), mapName)); + } + + public static ConsistencyCheckException missingTypeAnnotation(Class type) { + return new ConsistencyCheckException(ErrorCode.MISSING_TYPE_ANNOTATION, + String.format(MISSING_TYPE_ANNOTATION, type.getName())); + } + + public static ConsistencyCheckException unknownExpressionType(Class type) { + return new ConsistencyCheckException(ErrorCode.UNKNOWN_EXPRESSION_TYPE, + String.format(UNKNOWN_EXPRESSION_TYPE, type.getName())); + } + + public static ConsistencyCheckException invalidVariableContext() { + return new ConsistencyCheckException(ErrorCode.INVALID_VARIABLE_CONTEXT, INVALID_VARIABLE_CONTEXT); + } +} diff --git a/src/main/java/eu/europa/ted/efx/exceptions/InvalidArgumentException.java b/src/main/java/eu/europa/ted/efx/exceptions/InvalidArgumentException.java index 638cf5df..78411621 100644 --- a/src/main/java/eu/europa/ted/efx/exceptions/InvalidArgumentException.java +++ b/src/main/java/eu/europa/ted/efx/exceptions/InvalidArgumentException.java @@ -1,3 +1,16 @@ +/* + * 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 + * 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.efx.exceptions; import org.antlr.v4.runtime.misc.ParseCancellationException; @@ -12,17 +25,32 @@ */ @SuppressWarnings("squid:MaximumInheritanceDepth") // Necessary to integrate with ANTLR4 parser cancellation public class InvalidArgumentException extends ParseCancellationException { + + public enum ErrorCode { + ARGUMENT_NUMBER_MISMATCH, + ARGUMENT_TYPE_MISMATCH, + UNSUPPORTED_SEQUENCE_TYPE, + MISSING_ARGUMENT + } + private static final String ARGUMENT_NUMBER_MISMATCH = "Argument number mismatch in call to %s '%s'. Expected %d but got %d."; private static final String ARGUMENT_TYPE_MISMATCH = "Argument type mismatch for argument %d in call to %s '%s'. Expected %s but got %s."; private static final String UNSUPPORTED_SEQUENCE_TYPE = "Unsupported sequence type '%s' in call to %s."; private static final String MISSING_ARGUMENT = "No argument passed for parameter '%s'."; - private InvalidArgumentException(String message) { + private final ErrorCode errorCode; + + private InvalidArgumentException(ErrorCode errorCode, String message) { super(message); + this.errorCode = errorCode; + } + + public ErrorCode getErrorCode() { + return errorCode; } public static InvalidArgumentException argumentNumberMismatch(Parametrised identifier, int expectedNumber, int actualNumber) { - return new InvalidArgumentException( + return new InvalidArgumentException(ErrorCode.ARGUMENT_NUMBER_MISMATCH, String.format(ARGUMENT_NUMBER_MISMATCH, identifier.getClass().getSimpleName().toLowerCase(), identifier.name, expectedNumber, actualNumber)); } @@ -30,20 +58,20 @@ public static InvalidArgumentException argumentNumberMismatch(Parametrised ident public static InvalidArgumentException argumentNumberMismatch(Parametrised identifier, int expectedNumber) { return argumentNumberMismatch(identifier, expectedNumber, expectedNumber + 1); } - + public static InvalidArgumentException argumentTypeMismatch(int position, Parametrised identifier, Class expectedType, Class actualType) { - return new InvalidArgumentException( + return new InvalidArgumentException(ErrorCode.ARGUMENT_TYPE_MISMATCH, String.format(ARGUMENT_TYPE_MISMATCH, position + 1, identifier.getClass().getSimpleName().toLowerCase(), identifier.name, TypedExpression.getEfxDataType(expectedType).getSimpleName(), TypedExpression.getEfxDataType(actualType).getSimpleName())); } public static InvalidArgumentException unsupportedSequenceType(String type, String functionName) { - return new InvalidArgumentException(String.format(UNSUPPORTED_SEQUENCE_TYPE, type, functionName)); + return new InvalidArgumentException(ErrorCode.UNSUPPORTED_SEQUENCE_TYPE, String.format(UNSUPPORTED_SEQUENCE_TYPE, type, functionName)); } public static InvalidArgumentException missingArgument(String parameterName) { - return new InvalidArgumentException(String.format(MISSING_ARGUMENT, parameterName)); + return new InvalidArgumentException(ErrorCode.MISSING_ARGUMENT, String.format(MISSING_ARGUMENT, parameterName)); } } \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/exceptions/InvalidIdentifierException.java b/src/main/java/eu/europa/ted/efx/exceptions/InvalidIdentifierException.java index 60128c5e..2d87373f 100644 --- a/src/main/java/eu/europa/ted/efx/exceptions/InvalidIdentifierException.java +++ b/src/main/java/eu/europa/ted/efx/exceptions/InvalidIdentifierException.java @@ -1,3 +1,16 @@ +/* + * 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 + * 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.efx.exceptions; import org.antlr.v4.runtime.misc.ParseCancellationException; @@ -10,18 +23,37 @@ */ @SuppressWarnings("squid:MaximumInheritanceDepth") // Necessary to integrate with ANTLR4 parser cancellation public class InvalidIdentifierException extends ParseCancellationException { + + public enum ErrorCode { + UNDECLARED_IDENTIFIER, + IDENTIFIER_ALREADY_DECLARED, + NOT_A_CONTEXT_VARIABLE + } + private static final String UNDECLARED_IDENTIFIER = "Identifier '%s' is not declared."; private static final String IDENTIFIER_ALREADY_DECLARED = "Identifier '%s' is already declared in this scope."; + private static final String NOT_A_CONTEXT_VARIABLE = "Variable '%s' is not a context variable."; + + private final ErrorCode errorCode; - private InvalidIdentifierException(String message) { + private InvalidIdentifierException(ErrorCode errorCode, String message) { super(message); + this.errorCode = errorCode; + } + + public ErrorCode getErrorCode() { + return errorCode; } public static InvalidIdentifierException undeclaredIdentifier(String identifierName) { - return new InvalidIdentifierException(String.format(UNDECLARED_IDENTIFIER, identifierName)); + return new InvalidIdentifierException(ErrorCode.UNDECLARED_IDENTIFIER, String.format(UNDECLARED_IDENTIFIER, identifierName)); } public static InvalidIdentifierException alreadyDeclared(String identifierName) { - return new InvalidIdentifierException(String.format(IDENTIFIER_ALREADY_DECLARED, identifierName)); + return new InvalidIdentifierException(ErrorCode.IDENTIFIER_ALREADY_DECLARED, String.format(IDENTIFIER_ALREADY_DECLARED, identifierName)); + } + + public static InvalidIdentifierException notAContextVariable(String variableName) { + return new InvalidIdentifierException(ErrorCode.NOT_A_CONTEXT_VARIABLE, String.format(NOT_A_CONTEXT_VARIABLE, variableName)); } } \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/exceptions/InvalidIndentationException.java b/src/main/java/eu/europa/ted/efx/exceptions/InvalidIndentationException.java index ce788461..1f87a226 100644 --- a/src/main/java/eu/europa/ted/efx/exceptions/InvalidIndentationException.java +++ b/src/main/java/eu/europa/ted/efx/exceptions/InvalidIndentationException.java @@ -1,3 +1,16 @@ +/* + * 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 + * 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.efx.exceptions; import org.antlr.v4.runtime.misc.ParseCancellationException; @@ -9,41 +22,57 @@ */ @SuppressWarnings("squid:MaximumInheritanceDepth") // Necessary to integrate with ANTLR4 parser cancellation public class InvalidIndentationException extends ParseCancellationException { - private static final String INCONSISTENT_INDENTATION_SPACES = - "Inconsistent indentation. Expected a multiple of %d spaces."; - private static final String INDENTATION_LEVEL_SKIPPED = "Indentation level skipped."; - private static final String START_INDENT_AT_ZERO = - "Incorrect indentation. Please do not indent the first level in your template."; - private static final String MIXED_INDENTATION = - "Do not mix indentation methods. Stick with either tabs or spaces."; - private static final String NO_NESTING_ON_INVOCATIONS = - "Nesting content under a template invocation is not allowed."; - private static final String NO_INDENT_ON_TEMPLATE_DECLARATIONS = "Indentation is not allowed on template declaration lines."; - - private InvalidIndentationException(String message) { + + public enum ErrorCode { + INCONSISTENT_INDENTATION_SPACES, + INDENTATION_LEVEL_SKIPPED, + START_INDENT_AT_ZERO, + MIXED_INDENTATION, + NO_NESTING_ON_INVOCATIONS, + NO_INDENT_ON_TEMPLATE_DECLARATIONS + } + + private static final String INCONSISTENT_INDENTATION_SPACES = "Inconsistent indentation. Expected a multiple of %d spaces."; + private static final String INDENTATION_LEVEL_SKIPPED = "Indentation level skipped."; + private static final String START_INDENT_AT_ZERO = "Incorrect indentation. Please do not indent the first level in your template."; + private static final String MIXED_INDENTATION = "Do not mix indentation methods. Stick with either tabs or spaces."; + private static final String NO_NESTING_ON_INVOCATIONS = "Nesting content under a template invocation is not allowed."; + private static final String NO_INDENT_ON_TEMPLATE_DECLARATIONS = "Indentation is not allowed on template declaration lines."; + + private final ErrorCode errorCode; + + private InvalidIndentationException(ErrorCode errorCode, String message) { super(message); + this.errorCode = errorCode; + } + + public ErrorCode getErrorCode() { + return errorCode; } public static InvalidIndentationException inconsistentSpaces(int spaces) { - return new InvalidIndentationException(String.format(INCONSISTENT_INDENTATION_SPACES, spaces)); + return new InvalidIndentationException(ErrorCode.INCONSISTENT_INDENTATION_SPACES, + String.format(INCONSISTENT_INDENTATION_SPACES, spaces)); } public static InvalidIndentationException indentationLevelSkipped() { - return new InvalidIndentationException(INDENTATION_LEVEL_SKIPPED); + return new InvalidIndentationException(ErrorCode.INDENTATION_LEVEL_SKIPPED, INDENTATION_LEVEL_SKIPPED); } public static InvalidIndentationException startIndentAtZero() { - return new InvalidIndentationException(START_INDENT_AT_ZERO); + return new InvalidIndentationException(ErrorCode.START_INDENT_AT_ZERO, START_INDENT_AT_ZERO); } public static InvalidIndentationException mixedIndentation() { - return new InvalidIndentationException(MIXED_INDENTATION); + return new InvalidIndentationException(ErrorCode.MIXED_INDENTATION, MIXED_INDENTATION); } public static InvalidIndentationException noNestingOnInvocations() { - return new InvalidIndentationException(NO_NESTING_ON_INVOCATIONS); + return new InvalidIndentationException(ErrorCode.NO_NESTING_ON_INVOCATIONS, NO_NESTING_ON_INVOCATIONS); } + public static InvalidIndentationException noIndentOnTemplateDeclarations() { - return new InvalidIndentationException(NO_INDENT_ON_TEMPLATE_DECLARATIONS); + return new InvalidIndentationException(ErrorCode.NO_INDENT_ON_TEMPLATE_DECLARATIONS, + NO_INDENT_ON_TEMPLATE_DECLARATIONS); } } \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/exceptions/InvalidUsageException.java b/src/main/java/eu/europa/ted/efx/exceptions/InvalidUsageException.java index ecadaf2b..98018d37 100644 --- a/src/main/java/eu/europa/ted/efx/exceptions/InvalidUsageException.java +++ b/src/main/java/eu/europa/ted/efx/exceptions/InvalidUsageException.java @@ -1,3 +1,16 @@ +/* + * 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 + * 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.efx.exceptions; import org.antlr.v4.runtime.misc.ParseCancellationException; @@ -7,18 +20,43 @@ */ @SuppressWarnings("squid:MaximumInheritanceDepth") // Necessary to integrate with ANTLR4 parser cancellation public class InvalidUsageException extends ParseCancellationException { + + public enum ErrorCode { + SHORTHAND_REQUIRES_CODE_OR_INDICATOR, + SHORTHAND_REQUIRES_FIELD_CONTEXT, + INVALID_NOTICE_SUBTYPE_RANGE_ORDER, + INVALID_NOTICE_SUBTYPE_TOKEN + } + private static final String SHORTHAND_REQUIRES_CODE_OR_INDICATOR = "Indirect label reference shorthand #{%1$s}, requires a field of type 'code' or 'indicator'. Field %1$s is of type %2$s."; private static final String SHORTHAND_REQUIRES_FIELD_CONTEXT = "The %s shorthand syntax can only be used when a field is declared as context."; + private static final String INVALID_NOTICE_SUBTYPE_RANGE_ORDER = "Notice subtype range '%s-%s' is not in ascending order."; + private static final String INVALID_NOTICE_SUBTYPE_TOKEN = "Invalid notice subtype token '%s'. Expected format: 'X' or 'X-Y'."; + + private final ErrorCode errorCode; - private InvalidUsageException(String message) { + private InvalidUsageException(ErrorCode errorCode, String message) { super(message); + this.errorCode = errorCode; + } + + public ErrorCode getErrorCode() { + return errorCode; } public static InvalidUsageException shorthandRequiresCodeOrIndicator(String fieldName, String fieldType) { - return new InvalidUsageException(String.format(SHORTHAND_REQUIRES_CODE_OR_INDICATOR, fieldName, fieldType)); + return new InvalidUsageException(ErrorCode.SHORTHAND_REQUIRES_CODE_OR_INDICATOR, String.format(SHORTHAND_REQUIRES_CODE_OR_INDICATOR, fieldName, fieldType)); } public static InvalidUsageException shorthandRequiresFieldContext(String shorthandType) { - return new InvalidUsageException(String.format(SHORTHAND_REQUIRES_FIELD_CONTEXT, shorthandType)); + return new InvalidUsageException(ErrorCode.SHORTHAND_REQUIRES_FIELD_CONTEXT, String.format(SHORTHAND_REQUIRES_FIELD_CONTEXT, shorthandType)); + } + + public static InvalidUsageException invalidNoticeSubtypeRangeOrder(String start, String end) { + return new InvalidUsageException(ErrorCode.INVALID_NOTICE_SUBTYPE_RANGE_ORDER, String.format(INVALID_NOTICE_SUBTYPE_RANGE_ORDER, start, end)); + } + + public static InvalidUsageException invalidNoticeSubtypeToken(String token) { + return new InvalidUsageException(ErrorCode.INVALID_NOTICE_SUBTYPE_TOKEN, String.format(INVALID_NOTICE_SUBTYPE_TOKEN, token)); } } \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/exceptions/SymbolResolutionException.java b/src/main/java/eu/europa/ted/efx/exceptions/SymbolResolutionException.java index 89965662..a28ecaff 100644 --- a/src/main/java/eu/europa/ted/efx/exceptions/SymbolResolutionException.java +++ b/src/main/java/eu/europa/ted/efx/exceptions/SymbolResolutionException.java @@ -1,3 +1,16 @@ +/* + * 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 + * 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.efx.exceptions; import org.antlr.v4.runtime.misc.ParseCancellationException; @@ -10,33 +23,49 @@ */ @SuppressWarnings("squid:MaximumInheritanceDepth") // Necessary to integrate with ANTLR4 parser cancellation public class SymbolResolutionException extends ParseCancellationException { - private static final String UNKNOWN_FIELD = "Unknown field '%s'."; - private static final String UNKNOWN_NODE = "Unknown node '%s'."; + + public enum ErrorCode { + UNKNOWN_SYMBOL, + UNKNOWN_CODELIST, + NO_CODELIST_FOR_FIELD, + ROOT_NODE_NOT_FOUND, + UNKNOWN_NOTICE_SUBTYPE + } + + private static final String UNKNOWN_SYMBOL = "Unknown symbol '%s'."; private static final String UNKNOWN_CODELIST = "Unknown codelist '%s'."; - private static final String UNKNOWN_ALIAS = "Unknown field or node alias '%s'."; private static final String NO_CODELIST_FOR_FIELD = "Field '%s' is not associated with a codelist."; + private static final String ROOT_NODE_NOT_FOUND = "Could not find the root node. Check that node metadata is loaded correctly."; + private static final String UNKNOWN_NOTICE_SUBTYPE = "Unknown notice subtype '%s' in range '%s'."; - private SymbolResolutionException(String message) { - super(message); - } + private final ErrorCode errorCode; - public static SymbolResolutionException unknownField(String fieldId) { - return new SymbolResolutionException(String.format(UNKNOWN_FIELD, fieldId)); + private SymbolResolutionException(ErrorCode errorCode, String message) { + super(message); + this.errorCode = errorCode; } - public static SymbolResolutionException unknownNode(String nodeId) { - return new SymbolResolutionException(String.format(UNKNOWN_NODE, nodeId)); + public ErrorCode getErrorCode() { + return errorCode; } public static SymbolResolutionException unknownCodelist(String codelistId) { - return new SymbolResolutionException(String.format(UNKNOWN_CODELIST, codelistId)); + return new SymbolResolutionException(ErrorCode.UNKNOWN_CODELIST, String.format(UNKNOWN_CODELIST, codelistId)); } - public static SymbolResolutionException unknownAlias(String alias) { - return new SymbolResolutionException(String.format(UNKNOWN_ALIAS, alias)); + public static SymbolResolutionException unknownSymbol(String symbol) { + return new SymbolResolutionException(ErrorCode.UNKNOWN_SYMBOL, String.format(UNKNOWN_SYMBOL, symbol)); } public static SymbolResolutionException noCodelistForField(String fieldId) { - return new SymbolResolutionException(String.format(NO_CODELIST_FOR_FIELD, fieldId)); + return new SymbolResolutionException(ErrorCode.NO_CODELIST_FOR_FIELD, String.format(NO_CODELIST_FOR_FIELD, fieldId)); + } + + public static SymbolResolutionException rootNodeNotFound() { + return new SymbolResolutionException(ErrorCode.ROOT_NODE_NOT_FOUND, ROOT_NODE_NOT_FOUND); + } + + public static SymbolResolutionException unknownNoticeSubtype(String noticeSubtype, String rangeString) { + return new SymbolResolutionException(ErrorCode.UNKNOWN_NOTICE_SUBTYPE, String.format(UNKNOWN_NOTICE_SUBTYPE, noticeSubtype, rangeString)); } } \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/exceptions/TypeMismatchException.java b/src/main/java/eu/europa/ted/efx/exceptions/TypeMismatchException.java index d83d95f8..1eeb6e16 100644 --- a/src/main/java/eu/europa/ted/efx/exceptions/TypeMismatchException.java +++ b/src/main/java/eu/europa/ted/efx/exceptions/TypeMismatchException.java @@ -1,3 +1,16 @@ +/* + * 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 + * 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.efx.exceptions; import org.antlr.v4.runtime.misc.ParseCancellationException; @@ -13,11 +26,28 @@ */ @SuppressWarnings("squid:MaximumInheritanceDepth") // Necessary to integrate with ANTLR4 parser cancellation public class TypeMismatchException extends ParseCancellationException { - private static final String TYPE_MISMATCH_CANNOT_CONVERT = "Type mismatch. Expected %s instead of %s."; - private static final String TYPE_MISMATCH_CANNOT_COMPARE = "Type mismatch. Cannot compare values of different types: %s and %s"; - private TypeMismatchException(String message) { + public enum ErrorCode { + CANNOT_CONVERT, + CANNOT_COMPARE, + EXPECTED_SEQUENCE, + EXPECTED_FIELD_CONTEXT + } + + private static final String CANNOT_CONVERT = "Type mismatch. Expected %s instead of %s."; + private static final String CANNOT_COMPARE = "Type mismatch. Cannot compare values of different types: %s and %s"; + private static final String EXPECTED_SEQUENCE = "Type mismatch. Field '%s' may return multiple values from context '%s', but is used as a scalar. Use a sequence expression or change the context."; + private static final String EXPECTED_FIELD_CONTEXT = "Type mismatch. Context variable '$%s' refers to node '%s', but is used as a value. Only field context variables can be used in value expressions."; + + private final ErrorCode errorCode; + + private TypeMismatchException(ErrorCode errorCode, String message) { super(message); + this.errorCode = errorCode; + } + + public ErrorCode getErrorCode() { + return errorCode; } public static TypeMismatchException cannotConvert(Class expectedType, @@ -27,17 +57,26 @@ public static TypeMismatchException cannotConvert(Class var actual = actualType.asSubclass(TypedExpression.class); var expected = expectedType.asSubclass(TypedExpression.class); - return new TypeMismatchException(String.format(TYPE_MISMATCH_CANNOT_CONVERT, + return new TypeMismatchException(ErrorCode.CANNOT_CONVERT, String.format(CANNOT_CONVERT, TypedExpression.getEfxDataType(expected).getSimpleName(), TypedExpression.getEfxDataType(actual).getSimpleName())); } - return new TypeMismatchException(String.format(TYPE_MISMATCH_CANNOT_CONVERT, + return new TypeMismatchException(ErrorCode.CANNOT_CONVERT, String.format(CANNOT_CONVERT, expectedType.getSimpleName(), actualType.getSimpleName())); } public static TypeMismatchException cannotCompare(Expression left, Expression right) { - return new TypeMismatchException(String.format(TYPE_MISMATCH_CANNOT_COMPARE, + return new TypeMismatchException(ErrorCode.CANNOT_COMPARE, String.format(CANNOT_COMPARE, left.getClass().getSimpleName(), right.getClass().getSimpleName())); } + + public static TypeMismatchException fieldMayRepeat(String fieldId, String contextSymbol) { + return new TypeMismatchException(ErrorCode.EXPECTED_SEQUENCE, String.format(EXPECTED_SEQUENCE, fieldId, + contextSymbol != null ? contextSymbol : "root")); + } + + public static TypeMismatchException nodesHaveNoValue(String variableName, String nodeId) { + return new TypeMismatchException(ErrorCode.EXPECTED_FIELD_CONTEXT, String.format(EXPECTED_FIELD_CONTEXT, variableName, nodeId)); + } } diff --git a/src/main/java/eu/europa/ted/efx/interfaces/MarkupGenerator.java b/src/main/java/eu/europa/ted/efx/interfaces/MarkupGenerator.java index a3c3a3d8..757b8720 100644 --- a/src/main/java/eu/europa/ted/efx/interfaces/MarkupGenerator.java +++ b/src/main/java/eu/europa/ted/efx/interfaces/MarkupGenerator.java @@ -19,7 +19,8 @@ import org.apache.commons.lang3.tuple.Pair; import eu.europa.ted.efx.model.expressions.Expression; -import eu.europa.ted.efx.model.expressions.path.PathExpression; +import eu.europa.ted.efx.model.expressions.PathExpression; +import eu.europa.ted.efx.model.expressions.TypedExpression; import eu.europa.ted.efx.model.expressions.scalar.NumericExpression; import eu.europa.ted.efx.model.expressions.scalar.StringExpression; import eu.europa.ted.efx.model.templates.Conditional; @@ -77,13 +78,33 @@ default public Markup composeOutputFile(final List content, final List type, final String name, final Expression initialiser); + @Deprecated(since = "2.0.0-alpha.6", forRemoval = true) + default Markup renderVariableDeclaration(final Class type, final String name, final Expression initialiser) { + assert initialiser instanceof TypedExpression : "Initialiser must be a TypedExpression"; + return renderVariableDeclaration(name, (TypedExpression) initialiser); + } /** * Renders the markup necessary to declare and initialise a dictionary . @@ -99,19 +120,36 @@ default public Markup composeOutputFile(final List content, final List> parameters, final TypedExpression expression); + /** * Renders the Markup necessary to make the function available at runtime. * - * @param type A sub-class of EfxDataType that represents the return type - * of the function. + * @deprecated Use {@link #renderFunctionDeclaration(String, Map, TypedExpression)} instead. + * This method is being deprecated as of version 2.0.0-alpha.6 and + * will be removed before version 2.0.0 is released. + * + * @param type The return type of the function. * @param name The name of the function. - * @param parameters The function parameters as a map of parameter names to - * their corresponding EfxDataType. - * @param expression The function body (the expression that must be evaluated - * when the function is invoked). + * @param parameters The parameters of the function as a map from parameter name to its EFX data type. + * @param expression The function body expression. * @return A Markup object that declares the function. */ - Markup renderFunctionDeclaration(final Class type, final String name, final Map> parameters, final Expression expression); + @Deprecated(since = "2.0.0-alpha.6", forRemoval = true) + default Markup renderFunctionDeclaration(final Class type, final String name, final Map> parameters, final Expression expression) { + assert expression instanceof TypedExpression : "Expression must be a TypedExpression"; + return renderFunctionDeclaration(name, parameters, (TypedExpression) expression); + } + /** * Given an expression (which will eventually, at runtime, evaluate to the value @@ -144,7 +182,7 @@ default public Markup renderVariableExpression(final Expression variableExpressi * @return the template code that renders the label in the target template * language. */ - Markup renderLabelFromKey(final StringExpression key, TranslatorContext translatorContext); + Markup renderLabelFromKey(final StringExpression key, final TranslatorContext translatorContext); @Deprecated(since = "2.0.0-alpha.6", forRemoval = true) default public Markup renderLabelFromKey(final StringExpression key) { @@ -164,7 +202,7 @@ default public Markup renderLabelFromKey(final StringExpression key) { * @return the template code that renders the label in the target template * language. */ - Markup renderLabelFromKey(final StringExpression key, final NumericExpression quantity, TranslatorContext translatorContext); + Markup renderLabelFromKey(final StringExpression key, final NumericExpression quantity, final TranslatorContext translatorContext); @Deprecated(since = "2.0.0-alpha.6", forRemoval = true) default public Markup renderLabelFromKey(final StringExpression key, final NumericExpression quantity) { @@ -183,7 +221,7 @@ default public Markup renderLabelFromKey(final StringExpression key, final Numer * @return the template code that renders the label in the target template * language. */ - Markup renderLabelFromExpression(final Expression expression, TranslatorContext translatorContext); + Markup renderLabelFromExpression(final Expression expression, final TranslatorContext translatorContext); @Deprecated(since = "2.0.0-alpha.6", forRemoval = true) default public Markup renderLabelFromExpression(final Expression expression) { @@ -204,7 +242,7 @@ default public Markup renderLabelFromExpression(final Expression expression) { * @return the template code that renders the label in the target template * language. */ - Markup renderLabelFromExpression(final Expression expression, final NumericExpression quantity, TranslatorContext translatorContext); + Markup renderLabelFromExpression(final Expression expression, final NumericExpression quantity, final TranslatorContext translatorContext); @Deprecated(since = "2.0.0-alpha.6", forRemoval = true) default public Markup renderLabelFromExpression(final Expression expression, final NumericExpression quantity) { @@ -222,7 +260,7 @@ default public Markup renderLabelFromExpression(final Expression expression, fin * @param translatorContext additional context information provided by the template translator. * @return the template code that adds this text in the target template. */ - Markup renderFreeText(final String freeText, TranslatorContext translatorContext); + Markup renderFreeText(final String freeText, final TranslatorContext translatorContext); @Deprecated(since = "2.0.0-alpha.6", forRemoval = true) default public Markup renderFreeText(final String freeText) { @@ -238,7 +276,7 @@ default public Markup renderFreeText(final String freeText) { * @param translatorContext additional context information provided by the template translator. * @return the template code that renders the hyperlink in the target template language. */ - Markup renderHyperlink(final Markup label, final StringExpression url, TranslatorContext translatorContext); + Markup renderHyperlink(final Markup label, final StringExpression url, final TranslatorContext translatorContext); /** @@ -250,7 +288,7 @@ default public Markup renderFreeText(final String freeText) { * * @return the markup that represents a line break in the target template. */ - Markup renderLineBreak(TranslatorContext translatorContext); + Markup renderLineBreak(final TranslatorContext translatorContext); @Deprecated(since = "2.0.0-alpha.6", forRemoval = true) default public Markup renderLineBreak() { @@ -259,17 +297,16 @@ default public Markup renderLineBreak() { /** * Given a fragment name (identifier) and some pre-rendered content, this method - * returns the code - * that encapsulates it in the target template - * * @deprecated This method is deprecated and will be removed in future versions. - * Use {@link #composeFragmentDefinition(String, String, Set, Markup, Markup, Set)} instead. - * We are keeping the method temporarily to prevent build errors. - * This method is being deprecated as of version 2.0.0-alpha.6 and - * will be removed before version 2.0.0 is released - * The default implementation provided here only throws - * UnsupportedOperationException to prevent accidental use of this - * method. The EfxTemplateTranslator will not call this method. - * + * returns the code that encapsulates it in the target template. + * + * @deprecated This method is deprecated and will be removed in future versions. + * Use {@link #composeFragmentDefinition(String, String, Set, Markup, Markup, Set, + * TranslatorContext)} instead. We are keeping the method temporarily to prevent + * build errors. This method is being deprecated as of version 2.0.0-alpha.6 and + * will be removed before version 2.0.0 is released. The default implementation + * provided here only throws UnsupportedOperationException to prevent accidental + * use of this method. The EfxTemplateTranslator will not call this method. + * * @param name the name of the fragment. * @param number the outline number of the fragment. * @param content the content of the fragment. @@ -285,20 +322,19 @@ default Markup composeFragmentDefinition(final String name, String number, Marku /** * Given a fragment name (identifier) and some pre-rendered content, this method - * returns the code - * that encapsulates it in the target template - * - * @param name the name of the fragment. - * @param number the outline number of the fragment. - * @param conditionals the conditionals of the fragment. - * @param content the content of the fragment. - * @param children the children of the fragment. - * @param parameters the parameters of the fragment. - * @param translatorContext additional context information provided by the template translator. - * @return the code that encapsulates the fragment in the target template. + * returns the code that encapsulates it in the target template. + * + * @param name the name of the fragment. + * @param number the outline number of the fragment. + * @param conditionals the conditionals of the fragment. + * @param content the content of the fragment. + * @param children the children of the fragment. + * @param parameters the parameters of the fragment. + * @param translatorContext additional context information provided by the template translator. + * @return the code that encapsulates the fragment in the target template. */ - Markup composeFragmentDefinition(final String name, String number, Set conditionals, Markup content, Markup children, - Set parameters, TranslatorContext translatorContext); + Markup composeFragmentDefinition(final String name, final String number, final Set conditionals, final Markup content, final Markup children, + final Set parameters, final TranslatorContext translatorContext); @Deprecated(since = "2.0.0-alpha.6", forRemoval = true) default public Markup composeFragmentDefinition(final String name, String number, Set conditionals, @@ -325,7 +361,7 @@ default public Markup composeFragmentDefinition(final String name, String number * @param translatorContext additional context information provided by the template translator. * @return the code that invokes (uses) the fragment. */ - Markup renderFragmentInvocation(final String name, final Set arguments, TranslatorContext translatorContext); + Markup renderFragmentInvocation(final String name, final Set arguments, final TranslatorContext translatorContext); /** * Given a fragment name (identifier), and an evaluation context, this method @@ -361,7 +397,7 @@ default public Markup renderFragmentInvocation(final String name, final Set T composeList(List T composeConditionalExpression(BooleanExpress public T composeForExpression( IteratorListExpression iterators, ScalarExpression expression, Class targetListType); - public IteratorExpression composeIteratorExpression(Expression variableDeclarationExpression, SequenceExpression sourceList); + public IteratorExpression composeIteratorExpression(Expression variableDeclarationExpression, SequenceExpression sourceList); public IteratorListExpression composeIteratorList(List iterators); @@ -250,9 +250,21 @@ public PathExpression composeFieldInExternalReference(final PathExpression exter */ public PathExpression joinPaths(PathExpression first, PathExpression second); + /** + * Makes the given absolute path relative to the given context path. + * + * This is target-language-specific because different target languages (XPath, JavaScript, etc.) + * may have different path contextualization semantics. + * + * @param absolutePath The absolute path to contextualize. + * @param contextPath The context path to make the result relative to. + * @return The path relative to the given context. + */ + public PathExpression contextualizePath(PathExpression absolutePath, PathExpression contextPath); + /** * Gets a piece of text and returns it inside quotes as expected by the target language. - * + * * @param value The text to be quoted. * @return The quoted text. */ @@ -285,7 +297,7 @@ public NumericExpression composeNumericOperation(NumericExpression leftOperand, /** * Returns the numeric literal passed in target language script. The passed literal is in EFX. - * + * * @param efxLiteral The numeric literal in EFX. * @return The numeric literal in the target language. */ @@ -294,7 +306,7 @@ public NumericExpression composeNumericOperation(NumericExpression leftOperand, /** * Returns the string literal in the target language. Note that the string literal passed as a * parameter is already between quotes in EFX. - * + * * @param efxLiteral The string literal in EFX. * @return The string literal in the target language. */ diff --git a/src/main/java/eu/europa/ted/efx/interfaces/SymbolResolver.java b/src/main/java/eu/europa/ted/efx/interfaces/SymbolResolver.java index f56132fd..9cbe03aa 100644 --- a/src/main/java/eu/europa/ted/efx/interfaces/SymbolResolver.java +++ b/src/main/java/eu/europa/ted/efx/interfaces/SymbolResolver.java @@ -15,7 +15,7 @@ import java.util.List; -import eu.europa.ted.efx.model.expressions.path.PathExpression; +import eu.europa.ted.efx.model.expressions.PathExpression; /** * A SymbolResolver is a mechanism used by EFX translators to resolve symbols. @@ -48,27 +48,63 @@ public interface SymbolResolver { * JsonPath. If you intend to use a function call to retrieve the data from the data source then * that is what you should return as path. In general keep in mind that the path is used as target * language script. - * + * * @param fieldId The identifier of the field to look for. * @param contextPath The path relative to which we expect to find the return value. * @return The path to the given field relative to the given contextPath. + * @deprecated Use {@link #getRelativePathOfField(String, String)} instead. */ + @Deprecated(since = "2.0.0-alpha.6", forRemoval = true) public PathExpression getRelativePathOfField(final String fieldId, final PathExpression contextPath); + /** + * Gets the path that can be used to locate the given field in the data source, relative to the + * given context (node or field). + * + * @param fieldId The identifier of the field to look for. + * @param contextId The identifier of the context node or field. If a field ID is provided, + * its parent node is used as the context. + * @return The path to the given field relative to the given context. + */ + public PathExpression getRelativePathOfField(final String fieldId, final String contextId); + /** * Gets the path that can be used to locate the given node in the data source, relative to another * given path. - * + * * See {@link getRelativePathOfField} for a description of the concept of "path". - * + * * @param nodeId The identifier of the node to look for. * @param contextPath The path relative to which we expect to find the return value. * @return The path to the given node relative to the given context path. + * @deprecated Use {@link #getRelativePathOfNode(String, String)} instead. */ + @Deprecated(since = "2.0.0-alpha.6", forRemoval = true) public PathExpression getRelativePathOfNode(final String nodeId, final PathExpression contextPath); + /** + * Gets the path that can be used to locate the given node in the data source, relative to the + * given context (node or field). + * + * @param nodeId The identifier of the node to look for. + * @param contextId The identifier of the context node or field. If a field ID is provided, + * its parent node is used as the context. + * @return The path to the given node relative to the given context. + */ + public PathExpression getRelativePathOfNode(final String nodeId, final String contextId); + + /** + * Converts an absolute path to a relative path based on the given context. + * + * @param absolutePath The absolute path to convert. + * @param contextPath The context path to make the result relative to. + * @return The path relative to the given context. + * @deprecated Use {@link ScriptGenerator#contextualizePath(PathExpression, PathExpression)} instead. + * Path contextualization is target-language-specific and belongs on ScriptGenerator. + */ + @Deprecated(forRemoval = true) public PathExpression getRelativePath(PathExpression absolutePath, PathExpression contextPath); /** @@ -159,8 +195,70 @@ public PathExpression getRelativePathOfNode(final String nodeId, * @return List of notice type IDs (e.g., ["1", "2", ..., "40", "CEI", "E1", ..., "X02"]) */ public List getAllNoticeSubtypeIds(); - + + /** + * Resolves a field alias to its canonical field identifier. + * + * Returns null if the alias is not recognized as a field alias. This allows callers to implement + * fallback logic (e.g., trying {@link #getNodeIdFromAlias} next). The translator throws + * {@code SymbolResolutionException.unknownAlias()} only when both field and node lookups fail. + * + * @param alias The alias to resolve. + * @return The canonical field ID, or null if the alias is not a known field alias. + */ public String getFieldIdFromAlias(final String alias); + /** + * Resolves a node alias to its canonical node identifier. + * + * Returns null if the alias is not recognized as a node alias. This allows callers to implement + * fallback logic. The translator throws {@code SymbolResolutionException.unknownAlias()} only + * when both field and node lookups fail. + * + * @param alias The alias to resolve. + * @return The canonical node ID, or null if the alias is not a known node alias. + */ public String getNodeIdFromAlias(final String alias); + + /** + * Determines if a field reference would return multiple values when evaluated from a given + * context. + * + * A field is considered repeatable from a context if: + * 1. The field itself is marked as repeatable, OR + * 2. Any node between the field's parent and the context (exclusive) is repeatable + * + * @param fieldId The identifier of the field to check. + * @param contextNodeId The identifier of the context node, or null for root context. + * @return true if the field would return multiple values from the given context. + */ + public boolean isFieldRepeatableFromContext(final String fieldId, final String contextNodeId); + + /** + * Determines if a node reference would return multiple values when evaluated from a given + * context. + * + * A node is considered repeatable from a context if: + * 1. The node itself is marked as repeatable, OR + * 2. Any ancestor node between the node and the context (exclusive) is repeatable + * + * @param nodeId The identifier of the node to check. + * @param contextNodeId The identifier of the context node, or null for root context. + * @return true if the node would return multiple values from the given context. + */ + public boolean isNodeRepeatableFromContext(final String nodeId, final String contextNodeId); + + /** + * Gets the identifier of the root node. + * + * @return The root node ID (e.g., "ND-Root"). + */ + public String getRootNodeId(); + + /** + * Gets the absolute path to the root node. + * + * @return The absolute path of the root node as a PathExpression. + */ + public PathExpression getRootPath(); } diff --git a/src/main/java/eu/europa/ted/efx/interfaces/TypeChecker.java b/src/main/java/eu/europa/ted/efx/interfaces/TypeChecker.java new file mode 100644 index 00000000..86d12104 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/interfaces/TypeChecker.java @@ -0,0 +1,42 @@ +/* + * Copyright 2026 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.efx.interfaces; + +import eu.europa.ted.efx.model.expressions.TypedExpression; +import eu.europa.ted.efx.sdk1.TypeCheckerV1; +import eu.europa.ted.efx.sdk2.TypeCheckerV2; + +/** + * Determines whether one EFX expression type can be converted to another. + * + * Used by the {@link eu.europa.ted.efx.model.CallStack} to validate type compatibility during + * EFX expression evaluation. Different SDK versions have different type checking rules: + * {@link eu.europa.ted.efx.sdk1.TypeCheckerV1} (lenient) and + * {@link eu.europa.ted.efx.sdk2.TypeCheckerV2} (strict, requiring concrete types). + */ +public interface TypeChecker { + + TypeChecker V1 = TypeCheckerV1.INSTANCE; + TypeChecker V2 = TypeCheckerV2.INSTANCE; + + /** + * Checks if an expression of type {@code from} can be converted to type {@code to}. + * + * @param from the source expression type + * @param to the target expression type + * @return {@code true} if the conversion is allowed, {@code false} otherwise + */ + boolean canConvert(Class from, + Class to); +} diff --git a/src/main/java/eu/europa/ted/efx/model/CallStack.java b/src/main/java/eu/europa/ted/efx/model/CallStack.java index 77907b5a..592c2e4b 100644 --- a/src/main/java/eu/europa/ted/efx/model/CallStack.java +++ b/src/main/java/eu/europa/ted/efx/model/CallStack.java @@ -1,3 +1,16 @@ +/* + * 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.efx.model; import java.util.HashMap; @@ -13,6 +26,7 @@ import eu.europa.ted.efx.exceptions.InvalidIdentifierException; import eu.europa.ted.efx.exceptions.TypeMismatchException; +import eu.europa.ted.efx.interfaces.TypeChecker; import eu.europa.ted.efx.model.expressions.TypedExpression; import eu.europa.ted.efx.model.types.EfxDataType; import eu.europa.ted.efx.model.variables.ParsedParameter; @@ -33,6 +47,11 @@ public class CallStack { private static final String STACK_UNDERFLOW = "Stack underflow. Return values were available in the dropped frame, but no stack frame is left to consume them."; + /** + * The type checker used for type conversion checks. + */ + private final TypeChecker typeChecker; + /** * Stack frames are means of controlling the scope of variables and parameters. * Certain @@ -63,7 +82,7 @@ void declareIdentifier(Identifier identifier) { * Returns the object at the top of the stack and removes it from the stack. The * object must be * of the expected type. - * + * * @param expectedType The type that the returned object is expected to have. * @return The object removed from the top of the stack. */ @@ -76,7 +95,7 @@ synchronized T pop(Class expectedType) { if (TypedExpression.class.isAssignableFrom(actualType) && TypedExpression.class.isAssignableFrom(expectedType)) { var actual = actualType.asSubclass(TypedExpression.class); var expected = expectedType.asSubclass(TypedExpression.class); - if (TypedExpression.canConvert(actual, expected)) { + if (typeChecker.canConvert(actual, expected)) { return expectedType.cast(TypedExpression.from((TypedExpression) this.pop(), expected)); } throw TypeMismatchException.cannotConvert(expected, actual); @@ -94,7 +113,7 @@ synchronized T peek(Class expectedType) { if (TypedExpression.class.isAssignableFrom(actualType) && TypedExpression.class.isAssignableFrom(expectedType)) { var actual = actualType.asSubclass(TypedExpression.class); var expected = expectedType.asSubclass(TypedExpression.class); - if (TypedExpression.canConvert(actual, expected)) { + if (typeChecker.canConvert(actual, expected)) { return expectedType.cast(TypedExpression.from((TypedExpression) this.peek(), expected)); } } @@ -123,13 +142,23 @@ public void clear() { Map globalIdentifierRegistry = new LinkedHashMap<>(); /** - * Default and only constructor. Adds a global scope to the stack. + * Creates a CallStack with a specific type checker. + * + * @param typeChecker The type checker to use for type conversion checks. */ - public CallStack() { + public CallStack(TypeChecker typeChecker) { + this.typeChecker = typeChecker; this.frames = new Stack<>(); this.frames.push(new StackFrame()); // The global scope } + /** + * Default constructor. Uses TypeCheckerV2 for strict type checking. + */ + public CallStack() { + this(TypeChecker.V2); + } + /** * Creates a new stack frame and pushes it on top of the call stack. * diff --git a/src/main/java/eu/europa/ted/efx/model/Context.java b/src/main/java/eu/europa/ted/efx/model/Context.java index 0403f2f6..2d199cfa 100644 --- a/src/main/java/eu/europa/ted/efx/model/Context.java +++ b/src/main/java/eu/europa/ted/efx/model/Context.java @@ -1,6 +1,19 @@ +/* + * 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.efx.model; -import eu.europa.ted.efx.model.expressions.path.PathExpression; +import eu.europa.ted.efx.model.expressions.PathExpression; import eu.europa.ted.efx.model.variables.Variable; /** diff --git a/src/main/java/eu/europa/ted/efx/model/ContextStack.java b/src/main/java/eu/europa/ted/efx/model/ContextStack.java index 4d8a781e..4fc73aa5 100644 --- a/src/main/java/eu/europa/ted/efx/model/ContextStack.java +++ b/src/main/java/eu/europa/ted/efx/model/ContextStack.java @@ -1,3 +1,16 @@ +/* + * 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.efx.model; import java.util.HashMap; @@ -7,7 +20,7 @@ import eu.europa.ted.efx.interfaces.SymbolResolver; import eu.europa.ted.efx.model.Context.FieldContext; import eu.europa.ted.efx.model.Context.NodeContext; -import eu.europa.ted.efx.model.expressions.path.PathExpression; +import eu.europa.ted.efx.model.expressions.PathExpression; /** * Used to keep track of the current evaluation context. Extends Stack<Context> to provide @@ -44,7 +57,7 @@ public FieldContext pushFieldContext(final String fieldId) { this.push(context); return context; } - PathExpression relativePath = symbols.getRelativePathOfField(fieldId, this.absolutePath()); + PathExpression relativePath = symbols.getRelativePathOfField(fieldId, this.symbol()); FieldContext context = new FieldContext(fieldId, absolutePath, relativePath); this.push(context); return context; @@ -64,7 +77,7 @@ public NodeContext pushNodeContext(final String nodeId) { this.push(context); return context; } - PathExpression relativePath = symbols.getRelativePathOfNode(nodeId, this.absolutePath()); + PathExpression relativePath = symbols.getRelativePathOfNode(nodeId, this.symbol()); NodeContext context = new NodeContext(nodeId, absolutePath, relativePath); this.push(context); return context; @@ -137,7 +150,7 @@ public PathExpression absolutePath() { /** * Returns the relative path of the context that is currently at the top of the stack. Does not * remove the context from the stack. - * + * * @return the relative path of the context that is currently at the top of the stack. */ public PathExpression relativePath() { @@ -147,4 +160,17 @@ public PathExpression relativePath() { return this.peek().relativePath(); } + + /** + * Returns the parent context (second from top of the stack) without removing it. + * This is useful for the ".." context shortcut which refers to the grandparent context. + * + * @return the parent context, or null if there are fewer than 2 contexts on the stack. + */ + public Context peekParentContext() { + if (this.size() < 2) { + return null; + } + return this.get(this.size() - 2); + } } diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/Expression.java b/src/main/java/eu/europa/ted/efx/model/expressions/Expression.java index 75d6dd54..54c6d62f 100644 --- a/src/main/java/eu/europa/ted/efx/model/expressions/Expression.java +++ b/src/main/java/eu/europa/ted/efx/model/expressions/Expression.java @@ -1,3 +1,16 @@ +/* + * Copyright 2023 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.efx.model.expressions; import java.lang.reflect.Constructor; @@ -7,34 +20,36 @@ import eu.europa.ted.efx.model.ParsedEntity; +/** + * Root interface for all expression AST nodes in the EFX type system. + * + * An {@link Expression} wraps a target-language script fragment (e.g., XPath) produced during + * EFX-to-target-language translation. The script is accessed via {@link #getScript()}. + * + * Expressions form a type hierarchy enabling compile-time type safety during translation. + * Use {@link TypedExpression} subinterfaces for type-specific operations. + * + * @see TypedExpression for expressions with associated EFX data types + * @see LiteralExpression for constant values known at translation time + * @see PathExpression for references to document elements + */ public interface Expression extends ParsedEntity { public String getScript(); - public boolean isLiteral(); - static T instantiate(String script, Class type) { - return Expression.instantiate(script, false, type); - } - - static T from(Expression source, Class returnType) { - return Expression.instantiate(source.getScript(), source.isLiteral(), returnType); - } - - static T instantiate(String script, Boolean isLiteral, Class type) { try { - if (Boolean.TRUE.equals(isLiteral)) { - Constructor constructor = type.getConstructor(String.class, Boolean.class); - return constructor.newInstance(script, isLiteral); - } else { - Constructor constructor = type.getConstructor(String.class); - return constructor.newInstance(script); - } + Constructor constructor = type.getConstructor(String.class); + return constructor.newInstance(script); } catch (Exception e) { throw new ParseCancellationException(e); } } + static T from(Expression source, Class returnType) { + return Expression.instantiate(source.getScript(), returnType); + } + static T empty(Class type) { return instantiate("", type); } @@ -45,25 +60,14 @@ static T empty(Class type) { public abstract class Impl implements Expression { private final String script; - private final boolean isLiteral; @Override public String getScript() { return this.script; } - @Override - public boolean isLiteral() { - return this.isLiteral; - } - protected Impl(final String script) { - this(script, false); - } - - protected Impl(final String script, final Boolean isLiteral) { this.script = script; - this.isLiteral = isLiteral; } public final Boolean isEmpty() { @@ -81,12 +85,13 @@ public boolean equals(Object obj) { } Expression other = (Expression) obj; - return Objects.equals(script, other.getScript()) && isLiteral == other.isLiteral(); + return Objects.equals(script, other.getScript()) + && (this instanceof LiteralExpression) == (other instanceof LiteralExpression); } - + @Override public int hashCode() { - return Objects.hash(script, isLiteral); + return Objects.hash(script, this instanceof LiteralExpression); } } -} \ No newline at end of file +} diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/LiteralExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/LiteralExpression.java new file mode 100644 index 00000000..8f7e6336 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/LiteralExpression.java @@ -0,0 +1,46 @@ +/* + * Copyright 2026 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.efx.model.expressions; + +import eu.europa.ted.efx.model.expressions.scalar.ScalarLiteral; +import eu.europa.ted.efx.model.expressions.sequence.SequenceLiteral; + +/** + * A {@link TypedExpression} representing a constant value known at translation time. + * + * Unlike a {@link PathExpression} which generates a script that resolves to a value at runtime, + * a literal expression generates a script that embeds the constant value itself. + * + * {@link ScalarLiteral} represents single-value literals, while {@link SequenceLiteral} + * represents sequences of literals. + * + * @see PathExpression for expressions that reference document data + */ +public interface LiteralExpression extends TypedExpression { + + /** + * Creates an object of the given type, by using the given + * {@link TypedExpression} as a source. + * + * @param The type of the returned object. + * @param source The source {@link TypedExpression} to copy. + * @param returnType The type of object to be returned. + * + * @return An object of the given type, having the same property values as the + * source. + */ + static T from(TypedExpression source, Class returnType) { + return Expression.from(source, returnType); + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/PathExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/PathExpression.java new file mode 100644 index 00000000..16e87c42 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/PathExpression.java @@ -0,0 +1,63 @@ +/* + * Copyright 2026 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.efx.model.expressions; + +import eu.europa.ted.efx.model.expressions.scalar.ScalarPath; +import eu.europa.ted.efx.model.expressions.sequence.SequencePath; + +/** + * A {@link TypedExpression} representing a reference to an eForms Field or Node in a document. + * + * The generated script resolves to a value at runtime (similar to an XPath expression). + * + * {@link ScalarPath} represents references that resolve to a single value, while + * {@link SequencePath} represents references that may resolve to multiple values. + * Path expressions can convert between scalar and sequence representations via + * {@link #asScalar()} and {@link #asSequence()}. + */ +public interface PathExpression extends TypedExpression { + + /** + * Returns a scalar version of this path expression. + * If already scalar, returns this. If a sequence, creates the corresponding + * scalar path expression with the same script. + * + * @return A {@link ScalarPath} with the same script and primitive type. + */ + ScalarPath asScalar(); + + /** + * Returns a sequence version of this path expression. + * If already a sequence, returns this. If scalar, creates the corresponding + * sequence path expression with the same script. + * + * @return A {@link SequencePath} with the same script and primitive type. + */ + SequencePath asSequence(); + + /** + * Creates an object of the given type, by using the given + * {@link TypedExpression} as a source. + * + * @param The type of the returned object. + * @param source The source {@link TypedExpression} to copy. + * @param returnType The type of object to be returned. + * + * @return An object of the given type, having the same property values as the + * source. + */ + static T from(TypedExpression source, Class returnType) { + return Expression.from(source, returnType); + } +} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/TypedExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/TypedExpression.java index 1b21095b..292fc001 100644 --- a/src/main/java/eu/europa/ted/efx/model/expressions/TypedExpression.java +++ b/src/main/java/eu/europa/ted/efx/model/expressions/TypedExpression.java @@ -1,96 +1,91 @@ +/* + * Copyright 2023 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.efx.model.expressions; -import eu.europa.ted.efx.model.expressions.path.PathExpression; +import eu.europa.ted.efx.exceptions.ConsistencyCheckException; import eu.europa.ted.efx.model.expressions.scalar.ScalarExpression; import eu.europa.ted.efx.model.expressions.sequence.SequenceExpression; import eu.europa.ted.efx.model.types.EfxDataType; import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; -import eu.europa.ted.efx.model.types.EfxExpressionType; -import eu.europa.ted.efx.model.types.EfxExpressionTypeAssociation; - +import eu.europa.ted.efx.model.types.EfxTypeLattice; + +/** + * An {@link Expression} with an associated {@link EfxDataType}. + * + * Each concrete implementation is annotated with {@link EfxDataTypeAssociation} to declare its + * type in the EFX type system. This enables compile-time type checking and type-safe conversions + * during EFX translation. + * + * @see EfxDataType for the complete type hierarchy + * @see EfxDataTypeAssociation for the annotation linking expressions to types + */ public interface TypedExpression extends Expression { - public Class getExpressionType(); - public Class getDataType(); public boolean is(Class dataType); - public boolean is(Class referenceType, Class dataType); - - static Class getEfxDataType( - Class clazz, Class dataType1) { - EfxDataTypeAssociation annotation = clazz.getAnnotation(EfxDataTypeAssociation.class); - if (annotation == null) { - return EfxDataType.ANY.asSubclass(dataType1); // throw new IllegalArgumentException("Missing @EfxDataTypeAssociation annotation"); - } - return annotation.dataType().asSubclass(dataType1); - } - static Class getEfxDataType(Class clazz) { EfxDataTypeAssociation annotation = clazz.getAnnotation(EfxDataTypeAssociation.class); if (annotation == null) { - throw new IllegalArgumentException("Missing @EfxDataTypeAssociation annotation"); + throw ConsistencyCheckException.missingTypeAnnotation(clazz); } return annotation.dataType(); } public static T from(TypedExpression source, Class targetType) { + // When target is PathExpression, delegate to PathExpression.from if (PathExpression.class.isAssignableFrom(targetType)) { return targetType.cast(PathExpression.from(source, targetType.asSubclass(PathExpression.class))); - } else if (SequenceExpression.class.isAssignableFrom(targetType)) { - return targetType.cast(SequenceExpression.from(source, targetType.asSubclass(SequenceExpression.class))); - } else if (ScalarExpression.class.isAssignableFrom(targetType)) { - return targetType.cast(ScalarExpression.from(source, targetType.asSubclass(ScalarExpression.class))); - } else { - throw new RuntimeException("Unknown expression type: " + targetType); } - } - public static TypedExpression instantiate(String script, - Class expressionType, Class dataType) { - if (PathExpression.class.isAssignableFrom(expressionType)) { - return PathExpression.instantiate(script, dataType); - } else if (SequenceExpression.class.isAssignableFrom(expressionType)) { - return SequenceExpression.instantiate(script, dataType); - } else if (ScalarExpression.class.isAssignableFrom(expressionType)) { - return ScalarExpression.instantiate(script, dataType); - } else { - throw new RuntimeException("Unknown expression type: " + expressionType); + // When converting PathExpression to SequenceExpression, use the appropriate concrete sequence type + if (source instanceof PathExpression && SequenceExpression.class.isAssignableFrom(targetType)) { + Class primitiveType = EfxTypeLattice.toPrimitive(source.getDataType()); + Class concreteType = SequenceExpression.fromEfxDataType.get(primitiveType); + if (concreteType != null) { + return targetType.cast(Expression.from(source, concreteType)); + } } - } - public static boolean canConvert(Class from, Class to) { - var fromExpressionType = from.getAnnotation(EfxExpressionTypeAssociation.class).expressionType(); - var fromDataType = from.getAnnotation(EfxDataTypeAssociation.class).dataType(); - var toExpressionType = to.getAnnotation(EfxExpressionTypeAssociation.class).expressionType(); - var toDataType = to.getAnnotation(EfxDataTypeAssociation.class).dataType(); + // When converting PathExpression to ScalarExpression, use the appropriate concrete scalar type + if (source instanceof PathExpression && ScalarExpression.class.isAssignableFrom(targetType)) { + Class primitiveType = EfxTypeLattice.toPrimitive(source.getDataType()); + Class concreteType = ScalarExpression.fromEfxDataType.get(primitiveType); + if (concreteType != null) { + return targetType.cast(Expression.from(source, concreteType)); + } + } - return toExpressionType.isAssignableFrom(fromExpressionType) && toDataType.isAssignableFrom(fromDataType); + if (SequenceExpression.class.isAssignableFrom(targetType)) { + return targetType.cast(SequenceExpression.from(source, targetType.asSubclass(SequenceExpression.class))); + } else if (ScalarExpression.class.isAssignableFrom(targetType)) { + return targetType.cast(ScalarExpression.from(source, targetType.asSubclass(ScalarExpression.class))); + } else { + throw ConsistencyCheckException.unknownExpressionType(targetType); + } } public abstract class Impl extends Expression.Impl implements TypedExpression { - private Class expressionType; private Class dataType; - protected Impl(final String script, Class expressionType, - Class dataType) { - this(script, false, expressionType, dataType); - } - - protected Impl(final String script, final Boolean isLiteral, - Class expressionType, Class dataType) { - super(script, isLiteral); - this.expressionType = expressionType; + protected Impl(final String script, Class dataType) { + super(script); this.dataType = dataType; } - @Override - public Class getExpressionType() { - return this.expressionType; - } - @Override public Class getDataType() { return this.dataType; @@ -100,10 +95,5 @@ public Class getDataType() { public boolean is(Class dataType) { return dataType.isAssignableFrom(this.dataType); } - - @Override - public boolean is(Class referenceType, Class dataType) { - return referenceType.isAssignableFrom(this.expressionType) && dataType.isAssignableFrom(this.dataType); - } } } \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/path/BooleanPathExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/path/BooleanPathExpression.java deleted file mode 100644 index edf55b56..00000000 --- a/src/main/java/eu/europa/ted/efx/model/expressions/path/BooleanPathExpression.java +++ /dev/null @@ -1,12 +0,0 @@ -package eu.europa.ted.efx.model.expressions.path; - -import eu.europa.ted.efx.model.types.EfxDataType; -import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; - -@EfxDataTypeAssociation(dataType = EfxDataType.Boolean.class) -public class BooleanPathExpression extends PathExpression.Impl { - - public BooleanPathExpression(final String script) { - super(script, EfxDataType.Boolean.class); - } -} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/path/DatePathExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/path/DatePathExpression.java deleted file mode 100644 index e7a8c761..00000000 --- a/src/main/java/eu/europa/ted/efx/model/expressions/path/DatePathExpression.java +++ /dev/null @@ -1,12 +0,0 @@ -package eu.europa.ted.efx.model.expressions.path; - -import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; -import eu.europa.ted.efx.model.types.EfxDataType; - -@EfxDataTypeAssociation(dataType = EfxDataType.Date.class) -public class DatePathExpression extends PathExpression.Impl { - - public DatePathExpression(final String script) { - super(script, EfxDataType.Date.class); - } -} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/path/DurationPathExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/path/DurationPathExpression.java deleted file mode 100644 index ea87912a..00000000 --- a/src/main/java/eu/europa/ted/efx/model/expressions/path/DurationPathExpression.java +++ /dev/null @@ -1,12 +0,0 @@ -package eu.europa.ted.efx.model.expressions.path; - -import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; -import eu.europa.ted.efx.model.types.EfxDataType; - -@EfxDataTypeAssociation(dataType = EfxDataType.Duration.class) -public class DurationPathExpression extends PathExpression.Impl { - - public DurationPathExpression(final String script) { - super(script, EfxDataType.Duration.class); - } -} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/path/MultilingualStringPathExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/path/MultilingualStringPathExpression.java deleted file mode 100644 index 84c0f5f4..00000000 --- a/src/main/java/eu/europa/ted/efx/model/expressions/path/MultilingualStringPathExpression.java +++ /dev/null @@ -1,12 +0,0 @@ -package eu.europa.ted.efx.model.expressions.path; - -import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; -import eu.europa.ted.efx.model.types.EfxDataType; - -@EfxDataTypeAssociation(dataType = EfxDataType.MultilingualString.class) -public class MultilingualStringPathExpression extends StringPathExpression { - - public MultilingualStringPathExpression(final String script) { - super(script, EfxDataType.MultilingualString.class); - } -} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/path/NodePathExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/path/NodePathExpression.java deleted file mode 100644 index b81591f6..00000000 --- a/src/main/java/eu/europa/ted/efx/model/expressions/path/NodePathExpression.java +++ /dev/null @@ -1,16 +0,0 @@ -package eu.europa.ted.efx.model.expressions.path; - -import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; -import eu.europa.ted.efx.model.types.EfxDataType; - -@EfxDataTypeAssociation(dataType = EfxDataType.Node.class) -public class NodePathExpression extends PathExpression.Impl { - - public NodePathExpression(final String script) { - super(script, EfxDataType.Node.class); - } - - public static NodePathExpression empty() { - return new NodePathExpression(""); - } -} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/path/NumericPathExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/path/NumericPathExpression.java deleted file mode 100644 index c404c935..00000000 --- a/src/main/java/eu/europa/ted/efx/model/expressions/path/NumericPathExpression.java +++ /dev/null @@ -1,12 +0,0 @@ -package eu.europa.ted.efx.model.expressions.path; - -import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; -import eu.europa.ted.efx.model.types.EfxDataType; - -@EfxDataTypeAssociation(dataType = EfxDataType.Number.class) -public class NumericPathExpression extends PathExpression.Impl { - - public NumericPathExpression(final String script) { - super(script, EfxDataType.Number.class); - } -} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/path/PathExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/path/PathExpression.java deleted file mode 100644 index ae3f06e0..00000000 --- a/src/main/java/eu/europa/ted/efx/model/expressions/path/PathExpression.java +++ /dev/null @@ -1,128 +0,0 @@ -package eu.europa.ted.efx.model.expressions.path; - -import static java.util.Map.entry; - -import java.util.Map; - -import eu.europa.ted.efx.model.expressions.Expression; -import eu.europa.ted.efx.model.expressions.TypedExpression; -import eu.europa.ted.efx.model.expressions.scalar.ScalarExpression; -import eu.europa.ted.efx.model.expressions.sequence.SequenceExpression; -import eu.europa.ted.efx.model.types.EfxDataType; -import eu.europa.ted.efx.model.types.EfxExpressionType; -import eu.europa.ted.efx.model.types.EfxExpressionTypeAssociation; -import eu.europa.ted.efx.model.types.FieldTypes; - -/** - * A {@link PathExpression} is a {@link TypedExpression} that represents a - * pointer (a path) to an element in a structured document. Typically the - * document will be an XML document but it could also be a JSON - * object or any other structured data source. - * - * {@link PathExpression} objects are used to resolve eForms fields and Nodes. - * To develop an intuition on the behaviour of a {@link PathExpression}, you can - * think of it as being similar to an XPath expression. - * - * The {@link #getScript()} method should return an expression in the target - * language that resolves to a value. - */ -public interface PathExpression extends ScalarExpression, SequenceExpression, EfxExpressionType.Path { - - /** - * Maps {@link FieldTypes}, to concrete java classes that implement - * {@link PathExpression}. - */ - Map> fromFieldType = Map.ofEntries( - entry(FieldTypes.ID, StringPathExpression.class), // - entry(FieldTypes.ID_REF, StringPathExpression.class), // - entry(FieldTypes.TEXT, StringPathExpression.class), // - entry(FieldTypes.TEXT_MULTILINGUAL, MultilingualStringPathExpression.class), // - entry(FieldTypes.INDICATOR, BooleanPathExpression.class), // - entry(FieldTypes.AMOUNT, NumericPathExpression.class), // - entry(FieldTypes.NUMBER, NumericPathExpression.class), // - entry(FieldTypes.MEASURE, DurationPathExpression.class), // - entry(FieldTypes.CODE, StringPathExpression.class), - entry(FieldTypes.INTERNAL_CODE, StringPathExpression.class), // - entry(FieldTypes.INTEGER, NumericPathExpression.class), // - entry(FieldTypes.DATE, DatePathExpression.class), // - entry(FieldTypes.ZONED_DATE, DatePathExpression.class), // - entry(FieldTypes.TIME, TimePathExpression.class), // - entry(FieldTypes.ZONED_TIME, TimePathExpression.class), // - entry(FieldTypes.URL, StringPathExpression.class), // - entry(FieldTypes.PHONE, StringPathExpression.class), // - entry(FieldTypes.EMAIL, StringPathExpression.class)); - - /** - * Maps subclasses of {@link EfxDataType}, to concrete java classes that - * implement {@link PathExpression}. - */ - Map, Class> fromEfxDataType = Map.ofEntries( - entry(EfxDataType.String.class, StringPathExpression.class), // - entry(EfxDataType.MultilingualString.class, MultilingualStringPathExpression.class), // - entry(EfxDataType.Boolean.class, BooleanPathExpression.class), // - entry(EfxDataType.Number.class, NumericPathExpression.class), // - entry(EfxDataType.Date.class, DatePathExpression.class), // - entry(EfxDataType.Time.class, TimePathExpression.class), // - entry(EfxDataType.Duration.class, DurationPathExpression.class), // - entry(EfxDataType.Node.class, NodePathExpression.class) // - ); - - /** - * Creates an object that implements {@link PathExpression} and conforms to the - * given field type. - * - * @param script The target language script that resolves to a value. - * @param fieldType The type of field (or node) that the returned - * {@link PathExpression} points to. - * - * @return An object that implements {@link PathExpression} and conforms to the - * given field type. - */ - static PathExpression instantiate(String script, FieldTypes fieldType) { - return Expression.instantiate(script, fromFieldType.get(fieldType)); - } - - /** - * Creates an object that implements {@link PathExpression} and conforms to the - * given {@link EfxDataType}. - * - * @param script The target language script that resolves to a value. - * @param efxDataType The {@link EfxDataType} of the field (or node) that the - * returned {@link PathExpression} points to. - * - * @return An object that implements {@link PathExpression} and conforms to the - * given {@link EfxDataType}. - */ - static PathExpression instantiate(String script, Class efxDataType) { - return Expression.instantiate(script, fromEfxDataType.get(efxDataType)); - } - - /** - * Creates an object of the given type, by using the given - * {@link TypedExpression} as a source. - * - * @param The type of the returned object. - * @param source The source {@link TypedExpression} to copy. - * @param returnType The type of object to be returned. - * - * @return An object of the given type, having the same property values as the - * source. - */ - static T from(TypedExpression source, Class returnType) { - return Expression.from(source, returnType); - } - - /** - * A base class for {@link PathExpression} implementations. - * - * @param the EFX data type for the expression. - */ - @EfxExpressionTypeAssociation(expressionType = EfxExpressionType.Path.class) - public abstract class Impl extends TypedExpression.Impl - implements PathExpression { - - protected Impl(final String script, Class dataType) { - super(script, EfxExpressionType.Path.class, dataType); - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/path/StringPathExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/path/StringPathExpression.java deleted file mode 100644 index 4573b01c..00000000 --- a/src/main/java/eu/europa/ted/efx/model/expressions/path/StringPathExpression.java +++ /dev/null @@ -1,16 +0,0 @@ -package eu.europa.ted.efx.model.expressions.path; - -import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; -import eu.europa.ted.efx.model.types.EfxDataType; - -@EfxDataTypeAssociation(dataType = EfxDataType.String.class) -public class StringPathExpression extends PathExpression.Impl { - - public StringPathExpression(final String script) { - super(script, EfxDataType.String.class); - } - - protected StringPathExpression(final String script, Class type) { - super(script, type); - } -} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/path/TimePathExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/path/TimePathExpression.java deleted file mode 100644 index 30acecfc..00000000 --- a/src/main/java/eu/europa/ted/efx/model/expressions/path/TimePathExpression.java +++ /dev/null @@ -1,12 +0,0 @@ -package eu.europa.ted.efx.model.expressions.path; - -import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; -import eu.europa.ted.efx.model.types.EfxDataType; - -@EfxDataTypeAssociation(dataType = EfxDataType.Time.class) -public class TimePathExpression extends PathExpression.Impl { - - public TimePathExpression(final String script) { - super(script, EfxDataType.Time.class); - } -} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/BooleanExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/BooleanExpression.java index 61ab75b3..3a0cf331 100644 --- a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/BooleanExpression.java +++ b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/BooleanExpression.java @@ -1,20 +1,35 @@ +/* + * Copyright 2023 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.efx.model.expressions.scalar; import eu.europa.ted.efx.model.types.EfxDataType; import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; /** - * Represents a boolean expression, value or literal in the target language. + * A boolean-typed scalar AST node in the expression tree built during EFX translation. + * + * Wraps a target-language script fragment that evaluates to a boolean value. */ -@EfxDataTypeAssociation(dataType = EfxDataType.Boolean.class) -public class BooleanExpression extends ScalarExpression.Impl { +@EfxDataTypeAssociation(dataType = EfxDataType.BooleanScalar.class) +public class BooleanExpression extends ScalarExpression.Impl { public BooleanExpression(final String script) { - super(script, EfxDataType.Boolean.class); + super(script, EfxDataType.BooleanScalar.class); } - public BooleanExpression(final String script, final Boolean isLiteral) { - super(script, isLiteral, EfxDataType.Boolean.class); + protected BooleanExpression(final String script, Class type) { + super(script, type); } public static BooleanExpression empty() { diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/BooleanLiteral.java b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/BooleanLiteral.java new file mode 100644 index 00000000..ae1c6dde --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/BooleanLiteral.java @@ -0,0 +1,28 @@ +/* + * Copyright 2026 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.efx.model.expressions.scalar; + +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; + +/** + * A boolean-typed literal AST node representing a constant boolean value known at translation time. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.BooleanScalar.class) +public class BooleanLiteral extends BooleanExpression implements ScalarLiteral { + + public BooleanLiteral(final String script) { + super(script); + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/BooleanPath.java b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/BooleanPath.java new file mode 100644 index 00000000..d35d1c9d --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/BooleanPath.java @@ -0,0 +1,36 @@ +/* + * Copyright 2026 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.efx.model.expressions.scalar; + +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; + +/** + * A boolean-typed path AST node referencing an eForms Field that resolves to a boolean value. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.BooleanScalar.class) +public class BooleanPath extends ScalarPath.Impl { + + public BooleanPath(final String script) { + super(script, EfxDataType.BooleanScalar.class); + } + + protected BooleanPath(final String script, Class type) { + super(script, type); + } + + public static BooleanPath empty() { + return new BooleanPath(""); + } +} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/DateExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/DateExpression.java index 8975b7fe..7202e588 100644 --- a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/DateExpression.java +++ b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/DateExpression.java @@ -1,20 +1,35 @@ +/* + * Copyright 2023 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.efx.model.expressions.scalar; import eu.europa.ted.efx.model.types.EfxDataType; import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; /** - * Represents a date expression or value in the target language. + * A date-typed scalar AST node in the expression tree built during EFX translation. + * + * Wraps a target-language script fragment that evaluates to a date value. */ -@EfxDataTypeAssociation(dataType = EfxDataType.Date.class) -public class DateExpression extends ScalarExpression.Impl { +@EfxDataTypeAssociation(dataType = EfxDataType.DateScalar.class) +public class DateExpression extends ScalarExpression.Impl { public DateExpression(final String script) { - super(script, EfxDataType.Date.class); + super(script, EfxDataType.DateScalar.class); } - public DateExpression(final String script, final Boolean isLiteral) { - super(script, isLiteral, EfxDataType.Date.class); + protected DateExpression(final String script, Class type) { + super(script, type); } public static DateExpression empty() { diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/DateLiteral.java b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/DateLiteral.java new file mode 100644 index 00000000..32ff14af --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/DateLiteral.java @@ -0,0 +1,28 @@ +/* + * Copyright 2026 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.efx.model.expressions.scalar; + +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; + +/** + * A date-typed literal AST node representing a constant date value known at translation time. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.DateScalar.class) +public class DateLiteral extends DateExpression implements ScalarLiteral { + + public DateLiteral(final String script) { + super(script); + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/DatePath.java b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/DatePath.java new file mode 100644 index 00000000..0bd789fe --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/DatePath.java @@ -0,0 +1,36 @@ +/* + * Copyright 2026 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.efx.model.expressions.scalar; + +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; + +/** + * A date-typed path AST node referencing an eForms Field that resolves to a date value. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.DateScalar.class) +public class DatePath extends ScalarPath.Impl { + + public DatePath(final String script) { + super(script, EfxDataType.DateScalar.class); + } + + protected DatePath(final String script, Class type) { + super(script, type); + } + + public static DatePath empty() { + return new DatePath(""); + } +} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/DurationExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/DurationExpression.java index d3c6b04a..ae14bcf9 100644 --- a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/DurationExpression.java +++ b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/DurationExpression.java @@ -1,20 +1,35 @@ +/* + * Copyright 2023 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.efx.model.expressions.scalar; import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; import eu.europa.ted.efx.model.types.EfxDataType; /** - * Represents a duration expression, value or literal in the target language. + * A duration-typed scalar AST node in the expression tree built during EFX translation. + * + * Wraps a target-language script fragment that evaluates to a duration value. */ -@EfxDataTypeAssociation(dataType = EfxDataType.Duration.class) -public class DurationExpression extends ScalarExpression.Impl { +@EfxDataTypeAssociation(dataType = EfxDataType.DurationScalar.class) +public class DurationExpression extends ScalarExpression.Impl { public DurationExpression(final String script) { - super(script, EfxDataType.Duration.class); + super(script, EfxDataType.DurationScalar.class); } - public DurationExpression(final String script, final Boolean isLiteral) { - super(script, isLiteral, EfxDataType.Duration.class); + protected DurationExpression(final String script, Class type) { + super(script, type); } public static DurationExpression empty() { diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/DurationLiteral.java b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/DurationLiteral.java new file mode 100644 index 00000000..ed598cdf --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/DurationLiteral.java @@ -0,0 +1,28 @@ +/* + * Copyright 2026 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.efx.model.expressions.scalar; + +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; + +/** + * A duration-typed literal AST node representing a constant duration value known at translation time. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.DurationScalar.class) +public class DurationLiteral extends DurationExpression implements ScalarLiteral { + + public DurationLiteral(final String script) { + super(script); + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/DurationPath.java b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/DurationPath.java new file mode 100644 index 00000000..2e2953f9 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/DurationPath.java @@ -0,0 +1,36 @@ +/* + * Copyright 2026 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.efx.model.expressions.scalar; + +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; + +/** + * A duration-typed path AST node referencing an eForms Field that resolves to a duration value. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.DurationScalar.class) +public class DurationPath extends ScalarPath.Impl { + + public DurationPath(final String script) { + super(script, EfxDataType.DurationScalar.class); + } + + protected DurationPath(final String script, Class type) { + super(script, type); + } + + public static DurationPath empty() { + return new DurationPath(""); + } +} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/MultilingualStringExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/MultilingualStringExpression.java index ba9c18b7..ec7595ac 100644 --- a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/MultilingualStringExpression.java +++ b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/MultilingualStringExpression.java @@ -1,16 +1,30 @@ +/* + * Copyright 2023 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.efx.model.expressions.scalar; import eu.europa.ted.efx.model.types.EfxDataType; import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; -@EfxDataTypeAssociation(dataType = EfxDataType.MultilingualString.class) +/** + * A multilingual-string-typed scalar AST node, extending {@link StringExpression}. + * + * Represents strings that may have multiple language versions in eForms documents. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.MultilingualStringScalar.class) public class MultilingualStringExpression extends StringExpression { public MultilingualStringExpression(final String script) { - super(script, false, EfxDataType.MultilingualString.class); - } - - public MultilingualStringExpression(final String script, final Boolean isLiteral) { - super(script, isLiteral, EfxDataType.MultilingualString.class); + super(script, EfxDataType.MultilingualStringScalar.class); } } \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/MultilingualStringPath.java b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/MultilingualStringPath.java new file mode 100644 index 00000000..7f805079 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/MultilingualStringPath.java @@ -0,0 +1,30 @@ +/* + * Copyright 2026 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.efx.model.expressions.scalar; + +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; + +/** + * A multilingual-string-typed path AST node, extending {@link StringPath}. + * + * References an eForms Field that may have multiple language versions. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.MultilingualStringScalar.class) +public class MultilingualStringPath extends StringPath { + + public MultilingualStringPath(final String script) { + super(script, EfxDataType.MultilingualStringScalar.class); + } +} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/NodePath.java b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/NodePath.java new file mode 100644 index 00000000..da489c14 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/NodePath.java @@ -0,0 +1,38 @@ +/* + * Copyright 2026 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.efx.model.expressions.scalar; + +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; + +/** + * A path AST node referencing an eForms Node in a structured document. + * + * Unlike eForms Fields, eForms Nodes have no value and are used to set evaluation context. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.NodeScalar.class) +public class NodePath extends ScalarPath.Impl { + + public NodePath(final String script) { + super(script, EfxDataType.NodeScalar.class); + } + + protected NodePath(final String script, Class type) { + super(script, type); + } + + public static NodePath empty() { + return new NodePath(""); + } +} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/NumericExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/NumericExpression.java index 2be90118..bde9ef02 100644 --- a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/NumericExpression.java +++ b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/NumericExpression.java @@ -1,20 +1,35 @@ +/* + * Copyright 2023 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.efx.model.expressions.scalar; import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; import eu.europa.ted.efx.model.types.EfxDataType; /** - * Represents a numeric expression, value or literal in the target language. + * A numeric-typed scalar AST node in the expression tree built during EFX translation. + * + * Wraps a target-language script fragment that evaluates to a numeric value. */ -@EfxDataTypeAssociation(dataType = EfxDataType.Number.class) -public class NumericExpression extends ScalarExpression.Impl { +@EfxDataTypeAssociation(dataType = EfxDataType.NumberScalar.class) +public class NumericExpression extends ScalarExpression.Impl { public NumericExpression(final String script) { - super(script, EfxDataType.Number.class); + super(script, EfxDataType.NumberScalar.class); } - public NumericExpression(final String script, final Boolean isLiteral) { - super(script, isLiteral, EfxDataType.Number.class); + protected NumericExpression(final String script, Class type) { + super(script, type); } public static NumericExpression empty() { diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/NumericLiteral.java b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/NumericLiteral.java new file mode 100644 index 00000000..e96cd430 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/NumericLiteral.java @@ -0,0 +1,28 @@ +/* + * Copyright 2026 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.efx.model.expressions.scalar; + +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; + +/** + * A numeric-typed literal AST node representing a constant numeric value known at translation time. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.NumberScalar.class) +public class NumericLiteral extends NumericExpression implements ScalarLiteral { + + public NumericLiteral(final String script) { + super(script); + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/NumericPath.java b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/NumericPath.java new file mode 100644 index 00000000..4275d790 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/NumericPath.java @@ -0,0 +1,36 @@ +/* + * Copyright 2026 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.efx.model.expressions.scalar; + +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; + +/** + * A numeric-typed path AST node referencing an eForms Field that resolves to a numeric value. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.NumberScalar.class) +public class NumericPath extends ScalarPath.Impl { + + public NumericPath(final String script) { + super(script, EfxDataType.NumberScalar.class); + } + + protected NumericPath(final String script, Class type) { + super(script, type); + } + + public static NumericPath empty() { + return new NumericPath(""); + } +} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/ScalarExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/ScalarExpression.java index 6cc4bdbc..7d5d2d96 100644 --- a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/ScalarExpression.java +++ b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/ScalarExpression.java @@ -1,3 +1,16 @@ +/* + * Copyright 2023 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.efx.model.expressions.scalar; import static java.util.Map.entry; @@ -7,11 +20,18 @@ import eu.europa.ted.efx.model.expressions.Expression; import eu.europa.ted.efx.model.expressions.TypedExpression; import eu.europa.ted.efx.model.types.EfxDataType; -import eu.europa.ted.efx.model.types.EfxExpressionType; -import eu.europa.ted.efx.model.types.EfxExpressionTypeAssociation; +import eu.europa.ted.efx.model.types.EfxTypeLattice; import eu.europa.ted.efx.model.types.FieldTypes; -public interface ScalarExpression extends TypedExpression, EfxExpressionType.Scalar { +/** + * A {@link TypedExpression} representing a single value (as opposed to a sequence). + * + * Concrete implementations are typed by primitive: {@link BooleanExpression}, {@link StringExpression}, + * {@link NumericExpression}, {@link DateExpression}, {@link TimeExpression}, {@link DurationExpression}. + * + * @see eu.europa.ted.efx.model.expressions.sequence.SequenceExpression for multi-value expressions + */ +public interface ScalarExpression extends TypedExpression { /** * Maps {@link FieldTypes} to their corresponding {@link ScalarExpression} @@ -37,10 +57,10 @@ public interface ScalarExpression extends TypedExpression, EfxExpressionType.Sca entry(FieldTypes.EMAIL, StringExpression.class)); /** - * Maps {@link EfxDataType} to their corresponding {@link ScalarExpression} + * Maps primitive {@link EfxDataType} to their corresponding {@link ScalarExpression} * class. */ - Map, Class> fromEfxDataType = Map + Map, Class> fromEfxDataType = Map .ofEntries( entry(EfxDataType.String.class, StringExpression.class), // entry(EfxDataType.MultilingualString.class, MultilingualStringExpression.class), // @@ -79,24 +99,19 @@ static T from(TypedExpression source, Class retu * given {@link EfxDataType}. */ static ScalarExpression instantiate(String script, Class efxDataType) { - return Expression.instantiate(script, fromEfxDataType.get(efxDataType)); + return Expression.instantiate(script, fromEfxDataType.get(EfxTypeLattice.toPrimitive(efxDataType))); } /** * A base class for {@link ScalarExpression} implementations. - * + * * @param the EFX data type for the expression. */ - @EfxExpressionTypeAssociation(expressionType = EfxExpressionType.Scalar.class) public abstract class Impl extends TypedExpression.Impl implements ScalarExpression { - protected Impl(String script, Class dataType) { - super(script, EfxExpressionType.Scalar.class, dataType); - } - - protected Impl(String script, Boolean isLiteral, Class dataType) { - super(script, isLiteral, EfxExpressionType.Scalar.class, dataType); + protected Impl(String script, Class dataType) { + super(script, dataType); } } } \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/ScalarLiteral.java b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/ScalarLiteral.java new file mode 100644 index 00000000..92753829 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/ScalarLiteral.java @@ -0,0 +1,62 @@ +/* + * Copyright 2026 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.efx.model.expressions.scalar; + +import static java.util.Map.entry; + +import java.util.Map; + +import eu.europa.ted.efx.model.expressions.Expression; +import eu.europa.ted.efx.model.expressions.LiteralExpression; +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxTypeLattice; + +/** + * A {@link ScalarExpression} that is also a {@link LiteralExpression}. + * + * Represents a single constant value known at translation time, embedded directly in the + * generated script. + * + * @see eu.europa.ted.efx.model.expressions.sequence.SequenceLiteral for sequences of literals + */ +public interface ScalarLiteral extends ScalarExpression, LiteralExpression { + + /** + * Maps primitive {@link EfxDataType} to concrete scalar literal expression classes. + */ + Map, Class> fromEfxDataType = Map.ofEntries( + entry(EfxDataType.String.class, StringLiteral.class), + entry(EfxDataType.MultilingualString.class, StringLiteral.class), + entry(EfxDataType.Boolean.class, BooleanLiteral.class), + entry(EfxDataType.Number.class, NumericLiteral.class), + entry(EfxDataType.Date.class, DateLiteral.class), + entry(EfxDataType.Time.class, TimeLiteral.class), + entry(EfxDataType.Duration.class, DurationLiteral.class) + ); + + /** + * Creates a {@link ScalarLiteral} for the given {@link EfxDataType}. + * + * @param The type of the returned object. + * @param script The target language script representing the literal value. + * @param efxDataType The {@link EfxDataType} of the literal value. + * + * @return A {@link ScalarLiteral} for the given data type. + */ + @SuppressWarnings("unchecked") + static T instantiate(String script, Class efxDataType) { + Class type = fromEfxDataType.get(EfxTypeLattice.toPrimitive(efxDataType)); + return (T) Expression.instantiate(script, type); + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/ScalarPath.java b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/ScalarPath.java new file mode 100644 index 00000000..d9d7506f --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/ScalarPath.java @@ -0,0 +1,140 @@ +/* + * Copyright 2026 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.efx.model.expressions.scalar; + +import static java.util.Map.entry; + +import java.util.Map; + +import eu.europa.ted.efx.model.expressions.Expression; +import eu.europa.ted.efx.model.expressions.PathExpression; +import eu.europa.ted.efx.model.expressions.sequence.SequencePath; +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxTypeLattice; +import eu.europa.ted.efx.model.types.FieldTypes; + +/** + * A {@link ScalarExpression} that is also a {@link PathExpression}. + * + * References a single value in a document (non-repeatable from the current context). + * Can convert to {@link SequencePath} via {@link #asSequence()}. + * + * @see SequencePath for paths that may resolve to multiple values + */ +public interface ScalarPath extends ScalarExpression, PathExpression { + + /** + * Maps {@link FieldTypes} to concrete scalar path expression classes. + */ + Map> fromFieldType = Map.ofEntries( + entry(FieldTypes.ID, StringPath.class), // + entry(FieldTypes.ID_REF, StringPath.class), // + entry(FieldTypes.TEXT, StringPath.class), // + entry(FieldTypes.TEXT_MULTILINGUAL, MultilingualStringPath.class), // + entry(FieldTypes.INDICATOR, BooleanPath.class), // + entry(FieldTypes.AMOUNT, NumericPath.class), // + entry(FieldTypes.NUMBER, NumericPath.class), // + entry(FieldTypes.MEASURE, DurationPath.class), // + entry(FieldTypes.CODE, StringPath.class), + entry(FieldTypes.INTERNAL_CODE, StringPath.class), // + entry(FieldTypes.INTEGER, NumericPath.class), // + entry(FieldTypes.DATE, DatePath.class), // + entry(FieldTypes.ZONED_DATE, DatePath.class), // + entry(FieldTypes.TIME, TimePath.class), // + entry(FieldTypes.ZONED_TIME, TimePath.class), // + entry(FieldTypes.URL, StringPath.class), // + entry(FieldTypes.PHONE, StringPath.class), // + entry(FieldTypes.EMAIL, StringPath.class)); + + /** + * Maps primitive {@link EfxDataType} to concrete scalar path expression classes. + */ + Map, Class> fromEfxDataType = Map.ofEntries( + entry(EfxDataType.String.class, StringPath.class), // + entry(EfxDataType.MultilingualString.class, MultilingualStringPath.class), // + entry(EfxDataType.Boolean.class, BooleanPath.class), // + entry(EfxDataType.Number.class, NumericPath.class), // + entry(EfxDataType.Date.class, DatePath.class), // + entry(EfxDataType.Time.class, TimePath.class), // + entry(EfxDataType.Duration.class, DurationPath.class), // + entry(EfxDataType.Node.class, NodePath.class) // + ); + + /** + * Creates a {@link ScalarPath} for the given field type. + * + * @param The type of the returned object. + * @param script The target language script that resolves to a value. + * @param fieldType The type of field that the returned expression points to. + * + * @return A {@link ScalarPath} for the given field type. + */ + @SuppressWarnings("unchecked") + static T instantiate(String script, FieldTypes fieldType) { + Class type = fromFieldType.get(fieldType); + return (T) Expression.instantiate(script, type); + } + + /** + * Creates an empty {@link ScalarPath} for the given field type. + * + * @param The type of the returned object. + * @param fieldType The type of field that the returned expression points to. + * @return An empty {@link ScalarPath} for the given field type. + */ + @SuppressWarnings("unchecked") + static T empty(FieldTypes fieldType) { + Class type = fromFieldType.get(fieldType); + return (T) Expression.instantiate("", type); + } + + /** + * Creates a {@link ScalarPath} for the given {@link EfxDataType}. + * + * @param The type of the returned object. + * @param script The target language script that resolves to a value. + * @param efxDataType The {@link EfxDataType} of the field that the returned expression points to. + * + * @return A {@link ScalarPath} for the given data type. + */ + @SuppressWarnings("unchecked") + static T instantiate(String script, Class efxDataType) { + Class type = fromEfxDataType.get(EfxTypeLattice.toPrimitive(efxDataType)); + return (T) Expression.instantiate(script, type); + } + + /** + * A base class for {@link ScalarPath} implementations. + * + * @param the EFX data type for the expression. + */ + public abstract class Impl extends ScalarExpression.Impl + implements ScalarPath { + + protected Impl(final String script, Class dataType) { + super(script, dataType); + } + + @Override + public ScalarPath asScalar() { + return this; + } + + @Override + public SequencePath asSequence() { + Class sequenceType = EfxTypeLattice.toSequence(getDataType()); + return SequencePath.instantiate(getScript(), sequenceType); + } + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/StringExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/StringExpression.java index 4690e728..0d56cfa1 100644 --- a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/StringExpression.java +++ b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/StringExpression.java @@ -1,24 +1,35 @@ +/* + * Copyright 2023 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.efx.model.expressions.scalar; import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; import eu.europa.ted.efx.model.types.EfxDataType; /** - * Represents a string expression, value or literal in the target language. + * A string-typed scalar AST node in the expression tree built during EFX translation. + * + * Wraps a target-language script fragment that evaluates to a string value. */ -@EfxDataTypeAssociation(dataType = EfxDataType.String.class) -public class StringExpression extends ScalarExpression.Impl { +@EfxDataTypeAssociation(dataType = EfxDataType.StringScalar.class) +public class StringExpression extends ScalarExpression.Impl { public StringExpression(final String script) { - this(script, false); - } - - public StringExpression(final String script, final Boolean isLiteral) { - super(script, isLiteral, EfxDataType.String.class); + super(script, EfxDataType.StringScalar.class); } - public StringExpression(final String script, final Boolean isLiteral, Class type) { - super(script, isLiteral, type); + protected StringExpression(final String script, Class type) { + super(script, type); } public static StringExpression empty() { diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/StringLiteral.java b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/StringLiteral.java new file mode 100644 index 00000000..d672755c --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/StringLiteral.java @@ -0,0 +1,28 @@ +/* + * Copyright 2026 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.efx.model.expressions.scalar; + +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; + +/** + * A string-typed literal AST node representing a constant string value known at translation time. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.StringScalar.class) +public class StringLiteral extends StringExpression implements ScalarLiteral { + + public StringLiteral(final String script) { + super(script); + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/StringPath.java b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/StringPath.java new file mode 100644 index 00000000..49ac6487 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/StringPath.java @@ -0,0 +1,36 @@ +/* + * Copyright 2026 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.efx.model.expressions.scalar; + +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; + +/** + * A string-typed path AST node referencing an eForms Field that resolves to a string value. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.StringScalar.class) +public class StringPath extends ScalarPath.Impl { + + public StringPath(final String script) { + super(script, EfxDataType.StringScalar.class); + } + + protected StringPath(final String script, Class type) { + super(script, type); + } + + public static StringPath empty() { + return new StringPath(""); + } +} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/TimeExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/TimeExpression.java index 6916d7b8..3cbb2d3c 100644 --- a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/TimeExpression.java +++ b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/TimeExpression.java @@ -1,20 +1,35 @@ +/* + * Copyright 2023 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.efx.model.expressions.scalar; import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; import eu.europa.ted.efx.model.types.EfxDataType; /** - * Represents a time expression, value or literal in the target language. + * A time-typed scalar AST node in the expression tree built during EFX translation. + * + * Wraps a target-language script fragment that evaluates to a time value. */ -@EfxDataTypeAssociation(dataType = EfxDataType.Time.class) -public class TimeExpression extends ScalarExpression.Impl { +@EfxDataTypeAssociation(dataType = EfxDataType.TimeScalar.class) +public class TimeExpression extends ScalarExpression.Impl { public TimeExpression(final String script) { - super(script, EfxDataType.Time.class); + super(script, EfxDataType.TimeScalar.class); } - public TimeExpression(final String script, final Boolean isLiteral) { - super(script, isLiteral, EfxDataType.Time.class); + protected TimeExpression(final String script, Class type) { + super(script, type); } public static TimeExpression empty() { diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/TimeLiteral.java b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/TimeLiteral.java new file mode 100644 index 00000000..5371be43 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/TimeLiteral.java @@ -0,0 +1,28 @@ +/* + * Copyright 2026 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.efx.model.expressions.scalar; + +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; + +/** + * A time-typed literal AST node representing a constant time value known at translation time. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.TimeScalar.class) +public class TimeLiteral extends TimeExpression implements ScalarLiteral { + + public TimeLiteral(final String script) { + super(script); + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/TimePath.java b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/TimePath.java new file mode 100644 index 00000000..74599bcb --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/TimePath.java @@ -0,0 +1,36 @@ +/* + * Copyright 2026 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.efx.model.expressions.scalar; + +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; + +/** + * A time-typed path AST node referencing an eForms Field that resolves to a time value. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.TimeScalar.class) +public class TimePath extends ScalarPath.Impl { + + public TimePath(final String script) { + super(script, EfxDataType.TimeScalar.class); + } + + protected TimePath(final String script, Class type) { + super(script, type); + } + + public static TimePath empty() { + return new TimePath(""); + } +} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/BooleanSequenceExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/BooleanSequenceExpression.java index 66bce7ba..c85e5db9 100644 --- a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/BooleanSequenceExpression.java +++ b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/BooleanSequenceExpression.java @@ -1,19 +1,38 @@ +/* + * Copyright 2023 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.efx.model.expressions.sequence; import eu.europa.ted.efx.model.types.EfxDataType; import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; /** - * Used to represent a list of booleans in the target language. + * A boolean-typed sequence AST node in the expression tree built during EFX translation. + * + * Wraps a target-language script fragment that evaluates to a sequence of boolean values. */ -@EfxDataTypeAssociation(dataType = EfxDataType.Boolean.class) -public class BooleanSequenceExpression extends SequenceExpression.Impl { +@EfxDataTypeAssociation(dataType = EfxDataType.BooleanSequence.class) +public class BooleanSequenceExpression extends SequenceExpression.Impl { public BooleanSequenceExpression(final String script) { - super(script, EfxDataType.Boolean.class); + super(script, EfxDataType.BooleanSequence.class); + } + + protected BooleanSequenceExpression(final String script, Class type) { + super(script, type); } - public BooleanSequenceExpression(final String script, final Boolean isLiteral) { - super(script, isLiteral,EfxDataType.Boolean.class); + public static BooleanSequenceExpression empty() { + return new BooleanSequenceExpression(""); } } \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/BooleanSequenceLiteral.java b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/BooleanSequenceLiteral.java new file mode 100644 index 00000000..ca510aec --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/BooleanSequenceLiteral.java @@ -0,0 +1,28 @@ +/* + * Copyright 2026 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.efx.model.expressions.sequence; + +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; + +/** + * A boolean-typed sequence literal AST node representing constant boolean values known at translation time. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.BooleanSequence.class) +public class BooleanSequenceLiteral extends BooleanSequenceExpression implements SequenceLiteral { + + public BooleanSequenceLiteral(final String script) { + super(script); + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/BooleanSequencePath.java b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/BooleanSequencePath.java new file mode 100644 index 00000000..5115b8d7 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/BooleanSequencePath.java @@ -0,0 +1,36 @@ +/* + * Copyright 2026 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.efx.model.expressions.sequence; + +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; + +/** + * A boolean-typed sequence path AST node referencing an eForms Field that resolves to multiple boolean values. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.BooleanSequence.class) +public class BooleanSequencePath extends SequencePath.Impl { + + public BooleanSequencePath(final String script) { + super(script, EfxDataType.BooleanSequence.class); + } + + protected BooleanSequencePath(final String script, Class type) { + super(script, type); + } + + public static BooleanSequencePath empty() { + return new BooleanSequencePath(""); + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/DateSequenceExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/DateSequenceExpression.java index b9be9954..7fa43f57 100644 --- a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/DateSequenceExpression.java +++ b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/DateSequenceExpression.java @@ -1,19 +1,38 @@ +/* + * Copyright 2023 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.efx.model.expressions.sequence; import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; import eu.europa.ted.efx.model.types.EfxDataType; /** - * Used to represent a list of dates in the target language. + * A date-typed sequence AST node in the expression tree built during EFX translation. + * + * Wraps a target-language script fragment that evaluates to a sequence of date values. */ -@EfxDataTypeAssociation(dataType = EfxDataType.Date.class) -public class DateSequenceExpression extends SequenceExpression.Impl { +@EfxDataTypeAssociation(dataType = EfxDataType.DateSequence.class) +public class DateSequenceExpression extends SequenceExpression.Impl { public DateSequenceExpression(final String script) { - super(script, EfxDataType.Date.class); + super(script, EfxDataType.DateSequence.class); + } + + protected DateSequenceExpression(final String script, Class type) { + super(script, type); } - public DateSequenceExpression(final String script, final Boolean isLiteral) { - super(script, isLiteral, EfxDataType.Date.class); + public static DateSequenceExpression empty() { + return new DateSequenceExpression(""); } } \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/DateSequenceLiteral.java b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/DateSequenceLiteral.java new file mode 100644 index 00000000..62717641 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/DateSequenceLiteral.java @@ -0,0 +1,28 @@ +/* + * Copyright 2026 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.efx.model.expressions.sequence; + +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; + +/** + * A date-typed sequence literal AST node representing constant date values known at translation time. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.DateSequence.class) +public class DateSequenceLiteral extends DateSequenceExpression implements SequenceLiteral { + + public DateSequenceLiteral(final String script) { + super(script); + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/DateSequencePath.java b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/DateSequencePath.java new file mode 100644 index 00000000..cbd4880e --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/DateSequencePath.java @@ -0,0 +1,36 @@ +/* + * Copyright 2026 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.efx.model.expressions.sequence; + +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; + +/** + * A date-typed sequence path AST node referencing an eForms Field that resolves to multiple date values. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.DateSequence.class) +public class DateSequencePath extends SequencePath.Impl { + + public DateSequencePath(final String script) { + super(script, EfxDataType.DateSequence.class); + } + + protected DateSequencePath(final String script, Class type) { + super(script, type); + } + + public static DateSequencePath empty() { + return new DateSequencePath(""); + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/DurationSequenceExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/DurationSequenceExpression.java index 94f6318c..7239fd24 100644 --- a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/DurationSequenceExpression.java +++ b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/DurationSequenceExpression.java @@ -1,19 +1,38 @@ +/* + * Copyright 2023 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.efx.model.expressions.sequence; import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; import eu.europa.ted.efx.model.types.EfxDataType; /** - * Used to represent a list of durations in the target language. + * A duration-typed sequence AST node in the expression tree built during EFX translation. + * + * Wraps a target-language script fragment that evaluates to a sequence of duration values. */ -@EfxDataTypeAssociation(dataType = EfxDataType.Duration.class) -public class DurationSequenceExpression extends SequenceExpression.Impl { +@EfxDataTypeAssociation(dataType = EfxDataType.DurationSequence.class) +public class DurationSequenceExpression extends SequenceExpression.Impl { public DurationSequenceExpression(final String script) { - super(script, EfxDataType.Duration.class); + super(script, EfxDataType.DurationSequence.class); + } + + protected DurationSequenceExpression(final String script, Class type) { + super(script, type); } - public DurationSequenceExpression(final String script, final Boolean isLiteral) { - super(script, isLiteral, EfxDataType.Duration.class); + public static DurationSequenceExpression empty() { + return new DurationSequenceExpression(""); } } \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/DurationSequenceLiteral.java b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/DurationSequenceLiteral.java new file mode 100644 index 00000000..33adc0b2 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/DurationSequenceLiteral.java @@ -0,0 +1,28 @@ +/* + * Copyright 2026 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.efx.model.expressions.sequence; + +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; + +/** + * A duration-typed sequence literal AST node representing constant duration values known at translation time. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.DurationSequence.class) +public class DurationSequenceLiteral extends DurationSequenceExpression implements SequenceLiteral { + + public DurationSequenceLiteral(final String script) { + super(script); + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/DurationSequencePath.java b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/DurationSequencePath.java new file mode 100644 index 00000000..e163d8fc --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/DurationSequencePath.java @@ -0,0 +1,36 @@ +/* + * Copyright 2026 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.efx.model.expressions.sequence; + +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; + +/** + * A duration-typed sequence path AST node referencing an eForms Field that resolves to multiple duration values. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.DurationSequence.class) +public class DurationSequencePath extends SequencePath.Impl { + + public DurationSequencePath(final String script) { + super(script, EfxDataType.DurationSequence.class); + } + + protected DurationSequencePath(final String script, Class type) { + super(script, type); + } + + public static DurationSequencePath empty() { + return new DurationSequencePath(""); + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/MultilingualStringSequenceExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/MultilingualStringSequenceExpression.java index 1aed6a5e..25192573 100644 --- a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/MultilingualStringSequenceExpression.java +++ b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/MultilingualStringSequenceExpression.java @@ -1,12 +1,30 @@ +/* + * Copyright 2023 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.efx.model.expressions.sequence; import eu.europa.ted.efx.model.types.EfxDataType; import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; -@EfxDataTypeAssociation(dataType = EfxDataType.MultilingualString.class) +/** + * A multilingual-string-typed sequence AST node, extending {@link StringSequenceExpression}. + * + * Represents sequences of strings that may have multiple language versions in eForms documents. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.MultilingualStringSequence.class) public class MultilingualStringSequenceExpression extends StringSequenceExpression { public MultilingualStringSequenceExpression(final String script) { - super(script, false, EfxDataType.MultilingualString.class); + super(script, EfxDataType.MultilingualStringSequence.class); } } \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/MultilingualStringSequencePath.java b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/MultilingualStringSequencePath.java new file mode 100644 index 00000000..e7605124 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/MultilingualStringSequencePath.java @@ -0,0 +1,30 @@ +/* + * Copyright 2026 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.efx.model.expressions.sequence; + +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; + +/** + * A multilingual-string-typed sequence path AST node, extending {@link StringSequencePath}. + * + * References an eForms Field that may have multiple language versions, resolving to multiple values. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.MultilingualStringSequence.class) +public class MultilingualStringSequencePath extends StringSequencePath { + + public MultilingualStringSequencePath(final String script) { + super(script, EfxDataType.MultilingualStringSequence.class); + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/NodeSequencePath.java b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/NodeSequencePath.java new file mode 100644 index 00000000..9a81b82d --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/NodeSequencePath.java @@ -0,0 +1,38 @@ +/* + * Copyright 2026 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.efx.model.expressions.sequence; + +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; + +/** + * A sequence path AST node referencing multiple eForms Nodes in a structured document. + * + * Unlike eForms Fields, eForms Nodes have no value and are used to set evaluation context. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.NodeSequence.class) +public class NodeSequencePath extends SequencePath.Impl { + + public NodeSequencePath(final String script) { + super(script, EfxDataType.NodeSequence.class); + } + + protected NodeSequencePath(final String script, Class type) { + super(script, type); + } + + public static NodeSequencePath empty() { + return new NodeSequencePath(""); + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/NumericSequenceExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/NumericSequenceExpression.java index 9844e8a2..a9e935f6 100644 --- a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/NumericSequenceExpression.java +++ b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/NumericSequenceExpression.java @@ -1,19 +1,38 @@ +/* + * Copyright 2023 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.efx.model.expressions.sequence; import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; import eu.europa.ted.efx.model.types.EfxDataType; /** - * Used to represent a list of numbers in the target language. + * A numeric-typed sequence AST node in the expression tree built during EFX translation. + * + * Wraps a target-language script fragment that evaluates to a sequence of numeric values. */ -@EfxDataTypeAssociation(dataType = EfxDataType.Number.class) -public class NumericSequenceExpression extends SequenceExpression.Impl { +@EfxDataTypeAssociation(dataType = EfxDataType.NumberSequence.class) +public class NumericSequenceExpression extends SequenceExpression.Impl { public NumericSequenceExpression(final String script) { - super(script, EfxDataType.Number.class); + super(script, EfxDataType.NumberSequence.class); + } + + protected NumericSequenceExpression(final String script, Class type) { + super(script, type); } - public NumericSequenceExpression(final String script, final Boolean isLiteral) { - super(script, isLiteral, EfxDataType.Number.class); + public static NumericSequenceExpression empty() { + return new NumericSequenceExpression(""); } } \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/NumericSequenceLiteral.java b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/NumericSequenceLiteral.java new file mode 100644 index 00000000..dffffb9d --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/NumericSequenceLiteral.java @@ -0,0 +1,28 @@ +/* + * Copyright 2026 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.efx.model.expressions.sequence; + +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; + +/** + * A numeric-typed sequence literal AST node representing constant numeric values known at translation time. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.NumberSequence.class) +public class NumericSequenceLiteral extends NumericSequenceExpression implements SequenceLiteral { + + public NumericSequenceLiteral(final String script) { + super(script); + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/NumericSequencePath.java b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/NumericSequencePath.java new file mode 100644 index 00000000..69707e1a --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/NumericSequencePath.java @@ -0,0 +1,36 @@ +/* + * Copyright 2026 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.efx.model.expressions.sequence; + +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; + +/** + * A numeric-typed sequence path AST node referencing an eForms Field that resolves to multiple numeric values. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.NumberSequence.class) +public class NumericSequencePath extends SequencePath.Impl { + + public NumericSequencePath(final String script) { + super(script, EfxDataType.NumberSequence.class); + } + + protected NumericSequencePath(final String script, Class type) { + super(script, type); + } + + public static NumericSequencePath empty() { + return new NumericSequencePath(""); + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/SequenceExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/SequenceExpression.java index 7908c689..7adcc7e4 100644 --- a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/SequenceExpression.java +++ b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/SequenceExpression.java @@ -1,3 +1,16 @@ +/* + * Copyright 2023 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.efx.model.expressions.sequence; import static java.util.Map.entry; @@ -7,11 +20,19 @@ import eu.europa.ted.efx.model.expressions.Expression; import eu.europa.ted.efx.model.expressions.TypedExpression; import eu.europa.ted.efx.model.types.EfxDataType; -import eu.europa.ted.efx.model.types.EfxExpressionType; -import eu.europa.ted.efx.model.types.EfxExpressionTypeAssociation; +import eu.europa.ted.efx.model.types.EfxTypeLattice; import eu.europa.ted.efx.model.types.FieldTypes; -public interface SequenceExpression extends TypedExpression, EfxExpressionType.Sequence { +/** + * A {@link TypedExpression} representing multiple values (as opposed to a single value). + * + * Concrete implementations are typed by primitive: {@link BooleanSequenceExpression}, + * {@link StringSequenceExpression}, {@link NumericSequenceExpression}, {@link DateSequenceExpression}, + * {@link TimeSequenceExpression}, {@link DurationSequenceExpression}. + * + * @see eu.europa.ted.efx.model.expressions.scalar.ScalarExpression for single-value expressions + */ +public interface SequenceExpression extends TypedExpression { /** * Maps {@link FieldTypes} to the corresponding {@link SequenceExpression}. @@ -37,9 +58,9 @@ public interface SequenceExpression extends TypedExpression, EfxExpressionType.S entry(FieldTypes.EMAIL, StringSequenceExpression.class)); /** - * Maps {@link EfxDataType} to the corresponding {@link SequenceExpression}. + * Maps primitive {@link EfxDataType} to the corresponding {@link SequenceExpression}. */ - Map, Class> fromEfxDataType = Map + Map, Class> fromEfxDataType = Map .ofEntries( entry(EfxDataType.String.class, StringSequenceExpression.class), // entry(EfxDataType.MultilingualString.class, MultilingualStringSequenceExpression.class), // @@ -77,24 +98,19 @@ static T from(TypedExpression source, Class efxDataType) { - return Expression.instantiate(script, fromEfxDataType.get(efxDataType)); + return Expression.instantiate(script, fromEfxDataType.get(EfxTypeLattice.toPrimitive(efxDataType))); } /** * A base class for {@link SequenceExpression} implementations. - * + * * @param the data type of the sequence expression result */ - @EfxExpressionTypeAssociation(expressionType = EfxExpressionType.Sequence.class) public abstract class Impl extends TypedExpression.Impl implements SequenceExpression { protected Impl(final String script, Class dataType) { - this(script, false, dataType); - } - - protected Impl(final String script, final Boolean isLiteral, Class dataType) { - super(script, isLiteral, EfxExpressionType.Sequence.class, dataType); + super(script, dataType); } } } \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/SequenceLiteral.java b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/SequenceLiteral.java new file mode 100644 index 00000000..47c39f6f --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/SequenceLiteral.java @@ -0,0 +1,63 @@ +/* + * Copyright 2026 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.efx.model.expressions.sequence; + +import static java.util.Map.entry; + +import java.util.Map; + +import eu.europa.ted.efx.model.expressions.Expression; +import eu.europa.ted.efx.model.expressions.LiteralExpression; +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxTypeLattice; + +/** + * A {@link SequenceExpression} that is also a {@link LiteralExpression}. + * + * Represents a sequence of constant values known at translation time, embedded directly in the + * generated script. + * + * @see eu.europa.ted.efx.model.expressions.scalar.ScalarLiteral for single-value literals + */ +public interface SequenceLiteral extends SequenceExpression, LiteralExpression { + + /** + * Maps primitive {@link EfxDataType} to concrete sequence literal expression classes. + */ + Map, Class> fromEfxDataType = Map.ofEntries( + entry(EfxDataType.String.class, StringSequenceLiteral.class), + entry(EfxDataType.MultilingualString.class, StringSequenceLiteral.class), + entry(EfxDataType.Boolean.class, BooleanSequenceLiteral.class), + entry(EfxDataType.Number.class, NumericSequenceLiteral.class), + entry(EfxDataType.Date.class, DateSequenceLiteral.class), + entry(EfxDataType.Time.class, TimeSequenceLiteral.class), + entry(EfxDataType.Duration.class, DurationSequenceLiteral.class) + ); + + /** + * Creates a {@link SequenceLiteral} for the given {@link EfxDataType}. + * + * @param The type of the returned object. + * @param script The target language script representing the literal sequence. + * @param efxDataType The {@link EfxDataType} of the literal sequence values. + * + * @return A {@link SequenceLiteral} for the given data type. + */ + @SuppressWarnings("unchecked") + static T instantiate(String script, Class efxDataType) { + Class primitiveType = EfxTypeLattice.toPrimitive(efxDataType); + Class type = fromEfxDataType.get(primitiveType); + return (T) Expression.instantiate(script, type); + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/SequencePath.java b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/SequencePath.java new file mode 100644 index 00000000..3c496bef --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/SequencePath.java @@ -0,0 +1,130 @@ +/* + * Copyright 2026 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.efx.model.expressions.sequence; + +import static java.util.Map.entry; + +import java.util.Map; + +import eu.europa.ted.efx.model.expressions.Expression; +import eu.europa.ted.efx.model.expressions.PathExpression; +import eu.europa.ted.efx.model.expressions.scalar.ScalarPath; +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxTypeLattice; +import eu.europa.ted.efx.model.types.FieldTypes; + +/** + * A {@link SequenceExpression} that is also a {@link PathExpression}. + * + * References multiple values in a document (repeatable from the current context). + * Can convert to {@link ScalarPath} via {@link #asScalar()}. + * + * @see ScalarPath for paths that resolve to a single value + */ +public interface SequencePath extends SequenceExpression, PathExpression { + + /** + * Maps {@link FieldTypes} to concrete sequence path expression classes. + */ + Map> fromFieldType = Map.ofEntries( + entry(FieldTypes.ID, StringSequencePath.class), // + entry(FieldTypes.ID_REF, StringSequencePath.class), // + entry(FieldTypes.TEXT, StringSequencePath.class), // + entry(FieldTypes.TEXT_MULTILINGUAL, MultilingualStringSequencePath.class), // + entry(FieldTypes.INDICATOR, BooleanSequencePath.class), // + entry(FieldTypes.AMOUNT, NumericSequencePath.class), // + entry(FieldTypes.NUMBER, NumericSequencePath.class), // + entry(FieldTypes.MEASURE, DurationSequencePath.class), // + entry(FieldTypes.CODE, StringSequencePath.class), + entry(FieldTypes.INTERNAL_CODE, StringSequencePath.class), // + entry(FieldTypes.INTEGER, NumericSequencePath.class), // + entry(FieldTypes.DATE, DateSequencePath.class), // + entry(FieldTypes.ZONED_DATE, DateSequencePath.class), // + entry(FieldTypes.TIME, TimeSequencePath.class), // + entry(FieldTypes.ZONED_TIME, TimeSequencePath.class), // + entry(FieldTypes.URL, StringSequencePath.class), // + entry(FieldTypes.PHONE, StringSequencePath.class), // + entry(FieldTypes.EMAIL, StringSequencePath.class)); + + /** + * Maps primitive {@link EfxDataType} to concrete sequence path expression classes. + */ + Map, Class> fromEfxDataType = Map.ofEntries( + entry(EfxDataType.String.class, StringSequencePath.class), // + entry(EfxDataType.MultilingualString.class, MultilingualStringSequencePath.class), // + entry(EfxDataType.Boolean.class, BooleanSequencePath.class), // + entry(EfxDataType.Number.class, NumericSequencePath.class), // + entry(EfxDataType.Date.class, DateSequencePath.class), // + entry(EfxDataType.Time.class, TimeSequencePath.class), // + entry(EfxDataType.Duration.class, DurationSequencePath.class), // + entry(EfxDataType.Node.class, NodeSequencePath.class) // + ); + + /** + * Creates a {@link SequencePath} for the given field type. + * Use this when the field is repeatable from the current context. + * + * @param The type of the returned object. + * @param script The target language script that resolves to a sequence. + * @param fieldType The type of field that the returned expression points to. + * + * @return A {@link SequencePath} for the given field type. + */ + @SuppressWarnings("unchecked") + static T instantiate(String script, FieldTypes fieldType) { + Class type = fromFieldType.get(fieldType); + return (T) Expression.instantiate(script, type); + } + + /** + * Creates a {@link SequencePath} for the given {@link EfxDataType}. + * Use this when the field is repeatable from the current context. + * + * @param The type of the returned object. + * @param script The target language script that resolves to a sequence. + * @param efxDataType The {@link EfxDataType} of the field that the returned expression points to. + * + * @return A {@link SequencePath} for the given data type. + */ + @SuppressWarnings("unchecked") + static T instantiate(String script, Class efxDataType) { + Class primitiveType = EfxTypeLattice.toPrimitive(efxDataType); + Class type = fromEfxDataType.get(primitiveType); + return (T) Expression.instantiate(script, type); + } + + /** + * A base class for {@link SequencePath} implementations. + * + * @param the EFX data type for the expression. + */ + public abstract class Impl extends SequenceExpression.Impl + implements SequencePath { + + protected Impl(final String script, Class dataType) { + super(script, dataType); + } + + @Override + public ScalarPath asScalar() { + Class scalarType = EfxTypeLattice.toScalar(getDataType()); + return ScalarPath.instantiate(getScript(), scalarType); + } + + @Override + public SequencePath asSequence() { + return this; + } + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/StringSequenceExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/StringSequenceExpression.java index 176d9f97..fe9fc6bd 100644 --- a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/StringSequenceExpression.java +++ b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/StringSequenceExpression.java @@ -1,23 +1,38 @@ +/* + * Copyright 2023 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.efx.model.expressions.sequence; import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; import eu.europa.ted.efx.model.types.EfxDataType; /** - * Used to represent a list of strings in the target language. + * A string-typed sequence AST node in the expression tree built during EFX translation. + * + * Wraps a target-language script fragment that evaluates to a sequence of string values. */ -@EfxDataTypeAssociation(dataType = EfxDataType.String.class) -public class StringSequenceExpression extends SequenceExpression.Impl { +@EfxDataTypeAssociation(dataType = EfxDataType.StringSequence.class) +public class StringSequenceExpression extends SequenceExpression.Impl { public StringSequenceExpression(final String script) { - this(script, false); + super(script, EfxDataType.StringSequence.class); } - public StringSequenceExpression(final String script, final Boolean isLiteral) { - super(script, isLiteral, EfxDataType.String.class); + protected StringSequenceExpression(final String script, Class type) { + super(script, type); } - protected StringSequenceExpression(final String script, final Boolean isLiteral, Class type) { - super(script, isLiteral, type); + public static StringSequenceExpression empty() { + return new StringSequenceExpression(""); } } \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/StringSequenceLiteral.java b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/StringSequenceLiteral.java new file mode 100644 index 00000000..5778bd4e --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/StringSequenceLiteral.java @@ -0,0 +1,28 @@ +/* + * Copyright 2026 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.efx.model.expressions.sequence; + +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; + +/** + * A string-typed sequence literal AST node representing constant string values known at translation time. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.StringSequence.class) +public class StringSequenceLiteral extends StringSequenceExpression implements SequenceLiteral { + + public StringSequenceLiteral(final String script) { + super(script); + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/StringSequencePath.java b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/StringSequencePath.java new file mode 100644 index 00000000..dffa4bca --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/StringSequencePath.java @@ -0,0 +1,36 @@ +/* + * Copyright 2026 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.efx.model.expressions.sequence; + +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; + +/** + * A string-typed sequence path AST node referencing an eForms Field that resolves to multiple string values. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.StringSequence.class) +public class StringSequencePath extends SequencePath.Impl { + + public StringSequencePath(final String script) { + super(script, EfxDataType.StringSequence.class); + } + + protected StringSequencePath(final String script, Class type) { + super(script, type); + } + + public static StringSequencePath empty() { + return new StringSequencePath(""); + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/TimeSequenceExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/TimeSequenceExpression.java index c8aebc50..8d7041ca 100644 --- a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/TimeSequenceExpression.java +++ b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/TimeSequenceExpression.java @@ -1,18 +1,37 @@ +/* + * Copyright 2023 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.efx.model.expressions.sequence; import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; import eu.europa.ted.efx.model.types.EfxDataType; /** - * Used to represent a list of times in the target language. + * A time-typed sequence AST node in the expression tree built during EFX translation. + * + * Wraps a target-language script fragment that evaluates to a sequence of time values. */ -@EfxDataTypeAssociation(dataType = EfxDataType.Time.class) -public class TimeSequenceExpression extends SequenceExpression.Impl { +@EfxDataTypeAssociation(dataType = EfxDataType.TimeSequence.class) +public class TimeSequenceExpression extends SequenceExpression.Impl { public TimeSequenceExpression(final String script) { - super(script, EfxDataType.Time.class); + super(script, EfxDataType.TimeSequence.class); + } + + protected TimeSequenceExpression(final String script, Class type) { + super(script, type); } - public TimeSequenceExpression(final String script, final Boolean isLiteral) { - super(script, isLiteral, EfxDataType.Time.class); + public static TimeSequenceExpression empty() { + return new TimeSequenceExpression(""); } } \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/TimeSequenceLiteral.java b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/TimeSequenceLiteral.java new file mode 100644 index 00000000..7a9f59b7 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/TimeSequenceLiteral.java @@ -0,0 +1,28 @@ +/* + * Copyright 2026 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.efx.model.expressions.sequence; + +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; + +/** + * A time-typed sequence literal AST node representing constant time values known at translation time. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.TimeSequence.class) +public class TimeSequenceLiteral extends TimeSequenceExpression implements SequenceLiteral { + + public TimeSequenceLiteral(final String script) { + super(script); + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/sequence/TimeSequencePath.java b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/TimeSequencePath.java new file mode 100644 index 00000000..14227873 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/sequence/TimeSequencePath.java @@ -0,0 +1,36 @@ +/* + * Copyright 2026 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.efx.model.expressions.sequence; + +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; + +/** + * A time-typed sequence path AST node referencing an eForms Field that resolves to multiple time values. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.TimeSequence.class) +public class TimeSequencePath extends SequencePath.Impl { + + public TimeSequencePath(final String script) { + super(script, EfxDataType.TimeSequence.class); + } + + protected TimeSequencePath(final String script, Class type) { + super(script, type); + } + + public static TimeSequencePath empty() { + return new TimeSequencePath(""); + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/rules/NoticeSubtypeRange.java b/src/main/java/eu/europa/ted/efx/model/rules/NoticeSubtypeRange.java index 5fd9ccbe..bbfd56c3 100644 --- a/src/main/java/eu/europa/ted/efx/model/rules/NoticeSubtypeRange.java +++ b/src/main/java/eu/europa/ted/efx/model/rules/NoticeSubtypeRange.java @@ -16,6 +16,8 @@ import java.util.ArrayList; import java.util.List; +import eu.europa.ted.efx.exceptions.InvalidUsageException; +import eu.europa.ted.efx.exceptions.SymbolResolutionException; import eu.europa.ted.efx.model.ParsedEntity; public class NoticeSubtypeRange implements ParsedEntity, Iterable { @@ -47,9 +49,7 @@ public NoticeSubtypeRange(String rangeString, List validNoticeSubtypes) case 1: { int idx = validNoticeSubtypes.indexOf(parts[0]); if (idx < 0) { - throw new IllegalArgumentException( - String.format("Invalid notice type ID '%s' in compressed list '%s'", - parts[0], rangeString)); + throw SymbolResolutionException.unknownNoticeSubtype(parts[0], rangeString); } noticeSubtypes.add(validNoticeSubtypes.get(idx)); break; @@ -57,20 +57,14 @@ public NoticeSubtypeRange(String rangeString, List validNoticeSubtypes) case 2: { int startIdx = validNoticeSubtypes.indexOf(parts[0]); if (startIdx < 0) { - throw new IllegalArgumentException( - String.format("Invalid notice subtype '%s' in range '%s-%s'.", - parts[0], parts[0], parts[1])); + throw SymbolResolutionException.unknownNoticeSubtype(parts[0], parts[0] + "-" + parts[1]); } int endIdx = validNoticeSubtypes.indexOf(parts[1]); if (endIdx < 0) { - throw new IllegalArgumentException( - String.format("Invalid notice subtype '%s' in range '%s-%s'.", - parts[1], parts[0], parts[1])); + throw SymbolResolutionException.unknownNoticeSubtype(parts[1], parts[0] + "-" + parts[1]); } if (startIdx > endIdx) { - throw new IllegalArgumentException( - String.format("Notice subtype range '%s-%s' is not in ascending order.", parts[0], - parts[1])); + throw InvalidUsageException.invalidNoticeSubtypeRangeOrder(parts[0], parts[1]); } for (int i = startIdx; i <= endIdx; i++) { @@ -79,9 +73,7 @@ public NoticeSubtypeRange(String rangeString, List validNoticeSubtypes) break; } default: - throw new IllegalArgumentException( - String.format("Invalid notice subtype token '%s'.", - item)); + throw InvalidUsageException.invalidNoticeSubtypeToken(item); } } diff --git a/src/main/java/eu/europa/ted/efx/model/types/EfxDataType.java b/src/main/java/eu/europa/ted/efx/model/types/EfxDataType.java index 32cc5a18..3a7f9545 100644 --- a/src/main/java/eu/europa/ted/efx/model/types/EfxDataType.java +++ b/src/main/java/eu/europa/ted/efx/model/types/EfxDataType.java @@ -1,16 +1,82 @@ +/* + * Copyright 2023 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.efx.model.types; +/** + * Marker interfaces defining the EFX type system as a 2D lattice. + * + * The type system has two dimensions: + * - Primitive type (String, Boolean, Number, Date, Time, Duration, Node) + * - Cardinality (Scalar for single values, Sequence for multiple values) + * + * Concrete types combine both dimensions (e.g., StringScalar, BooleanSequence). + * Expression classes use {@link EfxDataTypeAssociation} to declare their type. + * + * @see EfxTypeLattice for type conversion operations + */ public interface EfxDataType { - Class ANY = EfxDataType.class; - Class UNDEFINED = Undefined.class; + Class VOID = EfxDataType.Void.class; - interface Boolean extends EfxDataType {} - interface String extends EfxDataType {} + /** + * Cardinality markers for EFX types (scalar vs sequence). + * Nested within EfxDataType but NOT extending it, to prevent + * accidental misuse where primitive types are expected. + */ + interface Cardinality { + /** Marker for single-value types */ + interface Scalar extends Cardinality {} + /** Marker for multi-value types */ + interface Sequence extends Cardinality {} + } + + /** + * Type category markers that extend EfxDataType. + * Used for type-safe return types in EfxTypeLattice. + */ + interface Primitive extends EfxDataType {} + interface ConcreteScalar extends EfxDataType {} + interface ConcreteSequence extends EfxDataType {} + + // EFX primitive types (extend Primitive marker) + interface Boolean extends Primitive {} + interface String extends Primitive {} interface MultilingualString extends String {} - interface Number extends EfxDataType {} - interface Date extends EfxDataType {} - interface Duration extends EfxDataType {} - interface Time extends EfxDataType {} - interface Node extends EfxDataType {} - interface Undefined extends EfxDataType {} + interface Number extends Primitive {} + interface Date extends Primitive {} + interface Time extends Primitive {} + interface Duration extends Primitive {} + interface Node extends Primitive {} + /** Unit type for templates/procedures that produce output but don't return a value. */ + interface Void extends EfxDataType {} + + // Concrete scalar types (primitive + Cardinality.Scalar + ConcreteScalar) + interface BooleanScalar extends Boolean, Cardinality.Scalar, ConcreteScalar {} + interface StringScalar extends String, Cardinality.Scalar, ConcreteScalar {} + interface MultilingualStringScalar extends StringScalar, MultilingualString {} + interface NumberScalar extends Number, Cardinality.Scalar, ConcreteScalar {} + interface DateScalar extends Date, Cardinality.Scalar, ConcreteScalar {} + interface TimeScalar extends Time, Cardinality.Scalar, ConcreteScalar {} + interface DurationScalar extends Duration, Cardinality.Scalar, ConcreteScalar {} + interface NodeScalar extends Node, Cardinality.Scalar, ConcreteScalar {} + + // Concrete sequence types (primitive + Cardinality.Sequence + ConcreteSequence) + interface BooleanSequence extends Boolean, Cardinality.Sequence, ConcreteSequence {} + interface StringSequence extends String, Cardinality.Sequence, ConcreteSequence {} + interface MultilingualStringSequence extends StringSequence, MultilingualString {} + interface NumberSequence extends Number, Cardinality.Sequence, ConcreteSequence {} + interface DateSequence extends Date, Cardinality.Sequence, ConcreteSequence {} + interface TimeSequence extends Time, Cardinality.Sequence, ConcreteSequence {} + interface DurationSequence extends Duration, Cardinality.Sequence, ConcreteSequence {} + interface NodeSequence extends Node, Cardinality.Sequence, ConcreteSequence {} } diff --git a/src/main/java/eu/europa/ted/efx/model/types/EfxDataTypeAssociation.java b/src/main/java/eu/europa/ted/efx/model/types/EfxDataTypeAssociation.java index 2fd4fd7b..e4c7e205 100644 --- a/src/main/java/eu/europa/ted/efx/model/types/EfxDataTypeAssociation.java +++ b/src/main/java/eu/europa/ted/efx/model/types/EfxDataTypeAssociation.java @@ -6,6 +6,12 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Runtime annotation linking an expression class to its {@link EfxDataType}. + * + * Applied to concrete expression classes to declare their type in the EFX type system, + * enabling type introspection and type-safe operations during translation. + */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Inherited diff --git a/src/main/java/eu/europa/ted/efx/model/types/EfxExpressionType.java b/src/main/java/eu/europa/ted/efx/model/types/EfxExpressionType.java deleted file mode 100644 index 1f2a7f29..00000000 --- a/src/main/java/eu/europa/ted/efx/model/types/EfxExpressionType.java +++ /dev/null @@ -1,7 +0,0 @@ -package eu.europa.ted.efx.model.types; - -public interface EfxExpressionType { - public interface Scalar extends EfxExpressionType {} - public interface Sequence extends EfxExpressionType {} - public interface Path extends EfxExpressionType.Scalar, EfxExpressionType.Sequence {} -} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/types/EfxExpressionTypeAssociation.java b/src/main/java/eu/europa/ted/efx/model/types/EfxExpressionTypeAssociation.java deleted file mode 100644 index a6496c6c..00000000 --- a/src/main/java/eu/europa/ted/efx/model/types/EfxExpressionTypeAssociation.java +++ /dev/null @@ -1,14 +0,0 @@ -package eu.europa.ted.efx.model.types; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Inherited; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -@Inherited -public @interface EfxExpressionTypeAssociation { - Class expressionType(); -} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/types/EfxTypeLattice.java b/src/main/java/eu/europa/ted/efx/model/types/EfxTypeLattice.java new file mode 100644 index 00000000..d8e6a10b --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/types/EfxTypeLattice.java @@ -0,0 +1,182 @@ +/* + * Copyright 2026 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.efx.model.types; + +import java.util.Arrays; +import java.util.List; + +import eu.europa.ted.efx.exceptions.ConsistencyCheckException; + +/** + * Provides type conversion operations for the EFX type system. + * + * The EFX type system is a two-dimensional lattice with primitive types + * (String, Boolean, Number, Date, Time, Duration, Node) and cardinalities + * (Scalar for single values, Sequence for multiple values). + * + * @see #toPrimitive(Class) Get the primitive type (e.g., String from StringScalar) + * @see #toScalar(Class) Get the scalar variant (e.g., StringScalar from String) + * @see #toSequence(Class) Get the sequence variant (e.g., StringSequence from String) + */ +public final class EfxTypeLattice { + + private EfxTypeLattice() { + // Utility class - prevent instantiation + } + + /** + * Groups a primitive type with its scalar and sequence variants. + */ + private static final class TypeVariants { + final Class primitive; + final Class scalar; + final Class sequence; + + TypeVariants(Class primitive, + Class scalar, + Class sequence) { + this.primitive = primitive; + this.scalar = scalar; + this.sequence = sequence; + } + } + + /** + * Registered type variants. Order matters: specific types must come before + * their supertypes (e.g., MultilingualString before String) for correct + * subtype matching. + */ + private static final List TYPE_VARIANTS = Arrays.asList( + new TypeVariants(EfxDataType.MultilingualString.class, + EfxDataType.MultilingualStringScalar.class, + EfxDataType.MultilingualStringSequence.class), + new TypeVariants(EfxDataType.String.class, + EfxDataType.StringScalar.class, + EfxDataType.StringSequence.class), + new TypeVariants(EfxDataType.Number.class, + EfxDataType.NumberScalar.class, + EfxDataType.NumberSequence.class), + new TypeVariants(EfxDataType.Boolean.class, + EfxDataType.BooleanScalar.class, + EfxDataType.BooleanSequence.class), + new TypeVariants(EfxDataType.Date.class, + EfxDataType.DateScalar.class, + EfxDataType.DateSequence.class), + new TypeVariants(EfxDataType.Time.class, + EfxDataType.TimeScalar.class, + EfxDataType.TimeSequence.class), + new TypeVariants(EfxDataType.Duration.class, + EfxDataType.DurationScalar.class, + EfxDataType.DurationSequence.class), + new TypeVariants(EfxDataType.Node.class, + EfxDataType.NodeScalar.class, + EfxDataType.NodeSequence.class) + ); + + /** + * Returns the primitive type for the given EFX data type. + * + * @param type any EFX data type (primitive, scalar, or sequence) + * @return the corresponding primitive type + * @throws IllegalArgumentException if type is null + * @throws ConsistencyCheckException if type is not registered in TYPE_VARIANTS + */ + public static Class toPrimitive(Class type) { + if (type == null) { + throw new IllegalArgumentException("type cannot be null"); + } + for (TypeVariants variants : TYPE_VARIANTS) { + if (variants.primitive.isAssignableFrom(type)) { + return variants.primitive; + } + } + throw ConsistencyCheckException.typeNotRegistered(type); + } + + /** + * Returns the scalar variant for the given EFX data type. + * + * @param type any EFX data type (primitive, scalar, or sequence) + * @return the corresponding scalar type + * @throws IllegalArgumentException if type is null + * @throws ConsistencyCheckException if type is not registered in TYPE_VARIANTS + */ + public static Class toScalar(Class type) { + if (type == null) { + throw new IllegalArgumentException("type cannot be null"); + } + for (TypeVariants variants : TYPE_VARIANTS) { + if (variants.primitive.isAssignableFrom(type)) { + return variants.scalar; + } + } + throw ConsistencyCheckException.typeNotRegistered(type); + } + + /** + * Returns the sequence variant for the given EFX data type. + * + * @param type any EFX data type (primitive, scalar, or sequence) + * @return the corresponding sequence type + * @throws IllegalArgumentException if type is null + * @throws ConsistencyCheckException if type is not registered in TYPE_VARIANTS + */ + public static Class toSequence(Class type) { + if (type == null) { + throw new IllegalArgumentException("type cannot be null"); + } + for (TypeVariants variants : TYPE_VARIANTS) { + if (variants.primitive.isAssignableFrom(type)) { + return variants.sequence; + } + } + throw ConsistencyCheckException.typeNotRegistered(type); + } + + /** + * Returns true if the given type has scalar cardinality. + * + * @param type the EFX data type to check + * @return true if the type implements {@link EfxDataType.Cardinality.Scalar} + */ + public static boolean isScalar(Class type) { + return type != null && EfxDataType.Cardinality.Scalar.class.isAssignableFrom(type); + } + + /** + * Returns true if the given type has sequence cardinality. + * + * @param type the EFX data type to check + * @return true if the type implements {@link EfxDataType.Cardinality.Sequence} + */ + public static boolean isSequence(Class type) { + return type != null && EfxDataType.Cardinality.Sequence.class.isAssignableFrom(type); + } + + /** + * Returns true if the given type is registered in TYPE_VARIANTS. + * Used by tests to verify all concrete types are registered. + * + * @param type the type to check + * @return true if the type is registered + */ + public static boolean isRegistered(Class type) { + for (TypeVariants variants : TYPE_VARIANTS) { + if (variants.scalar.equals(type) || variants.sequence.equals(type)) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/types/FieldTypes.java b/src/main/java/eu/europa/ted/efx/model/types/FieldTypes.java index 52e47c81..c999f8cb 100644 --- a/src/main/java/eu/europa/ted/efx/model/types/FieldTypes.java +++ b/src/main/java/eu/europa/ted/efx/model/types/FieldTypes.java @@ -3,6 +3,13 @@ import java.util.HashMap; import java.util.Map; +/** + * Enumeration of eForms SDK field types. + * + * Maps the field type strings from the SDK (e.g., "text", "indicator", "date") + * to enum constants. Expression classes use this to determine the appropriate + * EFX type for a given field. + */ public enum FieldTypes { ID("id"), // ID_REF("id-ref"), // diff --git a/src/main/java/eu/europa/ted/efx/model/variables/Dictionary.java b/src/main/java/eu/europa/ted/efx/model/variables/Dictionary.java index bc026c0b..95263fbe 100644 --- a/src/main/java/eu/europa/ted/efx/model/variables/Dictionary.java +++ b/src/main/java/eu/europa/ted/efx/model/variables/Dictionary.java @@ -1,9 +1,28 @@ +/* + * 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 + * 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.efx.model.variables; +import eu.europa.ted.efx.model.expressions.PathExpression; import eu.europa.ted.efx.model.expressions.TypedExpression; -import eu.europa.ted.efx.model.expressions.path.PathExpression; import eu.europa.ted.efx.model.expressions.scalar.StringExpression; +/** + * A dictionary variable declared in EFX source code. + * + * Maps string keys to values retrieved from a path expression. The dictionary stores the key + * expression, the path expression for values, and the expression type of the path. + */ public class Dictionary extends Identifier { public final StringExpression keyExpression; public final PathExpression pathExpression; diff --git a/src/main/java/eu/europa/ted/efx/model/variables/Function.java b/src/main/java/eu/europa/ted/efx/model/variables/Function.java index d58d87ba..c73a4751 100644 --- a/src/main/java/eu/europa/ted/efx/model/variables/Function.java +++ b/src/main/java/eu/europa/ted/efx/model/variables/Function.java @@ -1,15 +1,32 @@ +/* + * 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 + * 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.efx.model.variables; import eu.europa.ted.efx.model.expressions.TypedExpression; -import eu.europa.ted.efx.model.types.EfxDataType; +/** + * A user-defined function declared in EFX source code. + * + * Extends {@link Parametrised} to add the function body expression, whose type determines + * the function's return type. + */ public class Function extends Parametrised { public final TypedExpression expression; - public Function(String name, Class returnType, ParsedParameters parameters, - TypedExpression expression) { - super(name, returnType, parameters); + public Function(String name, ParsedParameters parameters, TypedExpression expression) { + super(name, expression.getDataType(), parameters); this.expression = expression; } diff --git a/src/main/java/eu/europa/ted/efx/model/variables/Parametrised.java b/src/main/java/eu/europa/ted/efx/model/variables/Parametrised.java index 60ad206e..b74e442a 100644 --- a/src/main/java/eu/europa/ted/efx/model/variables/Parametrised.java +++ b/src/main/java/eu/europa/ted/efx/model/variables/Parametrised.java @@ -2,6 +2,12 @@ import eu.europa.ted.efx.model.types.EfxDataType; +/** + * An identifier that accepts parameters. + * + * Base class for parameterized identifiers like functions and templates. Extends {@link Identifier} + * to add parsed parameter information. + */ public class Parametrised extends Identifier { public final ParsedParameters parameters; @@ -19,8 +25,8 @@ public boolean equals(Object o) { return false; if (!super.equals(o)) return false; - Parametrised function = (Function) o; - return parameters != null ? parameters.equals(function.parameters) : function.parameters == null; + Parametrised other = (Parametrised) o; + return parameters != null ? parameters.equals(other.parameters) : other.parameters == null; } @Override diff --git a/src/main/java/eu/europa/ted/efx/model/variables/ParsedParameters.java b/src/main/java/eu/europa/ted/efx/model/variables/ParsedParameters.java index f4cb3231..cf785cbe 100644 --- a/src/main/java/eu/europa/ted/efx/model/variables/ParsedParameters.java +++ b/src/main/java/eu/europa/ted/efx/model/variables/ParsedParameters.java @@ -11,6 +11,12 @@ import java.util.Set; import java.util.stream.Collectors; +/** + * An ordered collection of parsed parameters for a function or template. + * + * Preserves parameter order (important for positional arguments) and provides conversions + * to Set and Map representations. + */ public class ParsedParameters extends LinkedList implements ParsedEntity { public ParsedParameters() { diff --git a/src/main/java/eu/europa/ted/efx/model/variables/StrictArguments.java b/src/main/java/eu/europa/ted/efx/model/variables/StrictArguments.java index f68cb4b5..092d5340 100644 --- a/src/main/java/eu/europa/ted/efx/model/variables/StrictArguments.java +++ b/src/main/java/eu/europa/ted/efx/model/variables/StrictArguments.java @@ -1,8 +1,22 @@ +/* + * 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 + * 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.efx.model.variables; import java.util.Objects; import eu.europa.ted.efx.exceptions.InvalidArgumentException; +import eu.europa.ted.efx.interfaces.TypeChecker; import eu.europa.ted.efx.model.expressions.TypedExpression; public class StrictArguments extends ParsedArguments { @@ -22,7 +36,7 @@ public boolean addArgument(TypedExpression argument) { var actualType = argument.getClass(); var expectedType = this.identifier.parameters.get(position).getParameterType(); - if (!TypedExpression.canConvert(actualType, expectedType)) { + if (!TypeChecker.V2.canConvert(actualType, expectedType)) { throw InvalidArgumentException.argumentTypeMismatch(position, this.identifier, expectedType, actualType); } return this.add(new ParsedArgument(this.identifier.parameters.get(position), argument)); diff --git a/src/main/java/eu/europa/ted/efx/model/variables/Template.java b/src/main/java/eu/europa/ted/efx/model/variables/Template.java index da95d45e..ea662a48 100644 --- a/src/main/java/eu/europa/ted/efx/model/variables/Template.java +++ b/src/main/java/eu/europa/ted/efx/model/variables/Template.java @@ -5,6 +5,6 @@ public class Template extends Parametrised { public Template(String name, ParsedParameters parameters) { - super(name, EfxDataType.UNDEFINED, parameters); + super(name, EfxDataType.VOID, parameters); } } \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/variables/Variable.java b/src/main/java/eu/europa/ted/efx/model/variables/Variable.java index 882841f1..37128ee9 100644 --- a/src/main/java/eu/europa/ted/efx/model/variables/Variable.java +++ b/src/main/java/eu/europa/ted/efx/model/variables/Variable.java @@ -1,16 +1,40 @@ +/* + * Copyright 2023 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.efx.model.variables; import eu.europa.ted.efx.model.expressions.DeclarationExpression; import eu.europa.ted.efx.model.expressions.Expression; import eu.europa.ted.efx.model.expressions.TypedExpression; +import eu.europa.ted.efx.model.types.EfxTypeLattice; +/** + * A variable declared in EFX source code. + * + * Tracks three expressions: the initialization expression (determines the variable's type), + * the reference expression (target-language code for accessing the variable), and the + * declaration expression (target-language code for declaring the variable). + * + * For iterator variables, the initialization expression may be a sequence while the reference + * expression is scalar (since the iterator yields one element at a time). + */ public class Variable extends Identifier { public final Expression declarationExpression; public final TypedExpression initializationExpression; public final TypedExpression referenceExpression; public Variable(String variableName, TypedExpression initializationExpression, TypedExpression referenceExpression) { - this(variableName, DeclarationExpression.empty(), initializationExpression, referenceExpression); + this(variableName, DeclarationExpression.empty(), initializationExpression, referenceExpression); } public Variable(String variableName, Expression declarationExpression, TypedExpression initializationExpression, TypedExpression referenceExpression) { @@ -18,7 +42,10 @@ public Variable(String variableName, Expression declarationExpression, TypedExpr this.declarationExpression = declarationExpression; this.initializationExpression = initializationExpression; this.referenceExpression = referenceExpression; - assert referenceExpression.getDataType() == initializationExpression.getDataType(); + // Compare primitive types (without cardinality) since iterator variables have sequence initializers but scalar references + // Use isAssignableFrom to allow compatible types (e.g., MultilingualString is assignable to String) + assert EfxTypeLattice.toPrimitive(referenceExpression.getDataType()) + .isAssignableFrom(EfxTypeLattice.toPrimitive(initializationExpression.getDataType())); } @Override diff --git a/src/main/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1.java b/src/main/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1.java index ad2c0bc6..1484dd70 100644 --- a/src/main/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1.java +++ b/src/main/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1.java @@ -1,3 +1,16 @@ +/* + * 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.efx.sdk1; import java.util.ArrayList; @@ -21,27 +34,30 @@ import eu.europa.ted.eforms.sdk.component.SdkComponentType; import eu.europa.ted.efx.exceptions.InvalidArgumentException; import eu.europa.ted.efx.exceptions.TypeMismatchException; +import eu.europa.ted.efx.exceptions.ConsistencyCheckException; import eu.europa.ted.efx.interfaces.EfxExpressionTranslator; import eu.europa.ted.efx.interfaces.ScriptGenerator; import eu.europa.ted.efx.interfaces.SymbolResolver; +import eu.europa.ted.efx.interfaces.TypeChecker; import eu.europa.ted.efx.model.CallStack; import eu.europa.ted.efx.model.Context; import eu.europa.ted.efx.model.Context.FieldContext; import eu.europa.ted.efx.model.Context.NodeContext; import eu.europa.ted.efx.model.ContextStack; import eu.europa.ted.efx.model.expressions.Expression; +import eu.europa.ted.efx.model.expressions.PathExpression; import eu.europa.ted.efx.model.expressions.TypedExpression; import eu.europa.ted.efx.model.expressions.iteration.IteratorExpression; import eu.europa.ted.efx.model.expressions.iteration.IteratorListExpression; -import eu.europa.ted.efx.model.expressions.path.NodePathExpression; -import eu.europa.ted.efx.model.expressions.path.PathExpression; -import eu.europa.ted.efx.model.expressions.path.StringPathExpression; import eu.europa.ted.efx.model.expressions.scalar.BooleanExpression; import eu.europa.ted.efx.model.expressions.scalar.DateExpression; import eu.europa.ted.efx.model.expressions.scalar.DurationExpression; +import eu.europa.ted.efx.model.expressions.scalar.NodePath; import eu.europa.ted.efx.model.expressions.scalar.NumericExpression; import eu.europa.ted.efx.model.expressions.scalar.ScalarExpression; +import eu.europa.ted.efx.model.expressions.scalar.ScalarPath; import eu.europa.ted.efx.model.expressions.scalar.StringExpression; +import eu.europa.ted.efx.model.expressions.scalar.StringPath; import eu.europa.ted.efx.model.expressions.scalar.TimeExpression; import eu.europa.ted.efx.model.expressions.sequence.BooleanSequenceExpression; import eu.europa.ted.efx.model.expressions.sequence.DateSequenceExpression; @@ -51,6 +67,7 @@ import eu.europa.ted.efx.model.expressions.sequence.StringSequenceExpression; import eu.europa.ted.efx.model.expressions.sequence.TimeSequenceExpression; import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxTypeLattice; import eu.europa.ted.efx.model.types.FieldTypes; import eu.europa.ted.efx.model.variables.ParsedParameter; import eu.europa.ted.efx.model.variables.Variable; @@ -84,7 +101,7 @@ public class EfxExpressionTranslatorV1 extends EfxBaseListener * The stack is used by the methods of this listener to pass data to each other as the parse tree * is being walked. */ - protected CallStack stack = new CallStack(); + protected CallStack stack = new CallStack(TypeChecker.V1); /** * The context stack is used to keep track of context switching in nested expressions. @@ -608,7 +625,7 @@ private void exitList(int listSize, public void exitUntypedConditionalExpression(UntypedConditionalExpressionContext ctx) { var topOfStack = this.stack.peek(); assert topOfStack instanceof TypedExpression : "Expected a TypedExpression at the top of the stack."; - final Class typeWhenFalse = ((TypedExpression)topOfStack).getDataType(); + final Class typeWhenFalse = EfxTypeLattice.toPrimitive(((TypedExpression)topOfStack).getDataType()); if (typeWhenFalse == EfxDataType.Boolean.class) { this.exitConditionalBooleanExpression(); } else if (typeWhenFalse == EfxDataType.Number.class) { @@ -622,7 +639,7 @@ public void exitUntypedConditionalExpression(UntypedConditionalExpressionContext } else if (typeWhenFalse == EfxDataType.Duration.class) { this.exitConditionalDurationExpression(); } else { - throw new IllegalStateException("Unknown type " + typeWhenFalse); + throw ConsistencyCheckException.unsupportedTypeInConditional(typeWhenFalse); } } @@ -742,7 +759,8 @@ public void exitDurationIteratorExpression(DurationIteratorExpressionContext ctx public void exitContextIteratorExpression(ContextIteratorExpressionContext ctx) { PathExpression path = this.stack.pop(PathExpression.class); - var variableType = path.getClass(); + // Iterator variable holds the current item (scalar), not the sequence + var variableType = path.asScalar().getClass(); var variableName = getVariableName(ctx.contextVariableDeclaration()); Variable variable = new Variable(variableName, this.script.composeVariableDeclaration(variableName, variableType), @@ -750,18 +768,18 @@ public void exitContextIteratorExpression(ContextIteratorExpressionContext ctx) this.script.composeVariableReference(variableName, variableType)); this.stack.declareIdentifier(variable); - this.stack.push(this.script.composeIteratorExpression(variable.declarationExpression, path)); + this.stack.push(this.script.composeIteratorExpression(variable.declarationExpression, path.asSequence())); if (ctx.fieldContext() != null) { final String contextFieldId = getFieldId(ctx.fieldContext()); this.efxContext.declareContextVariable(variable.name, new FieldContext(contextFieldId, this.symbols.getAbsolutePathOfField(contextFieldId), - this.symbols.getRelativePathOfField(contextFieldId, this.efxContext.absolutePath()))); + this.symbols.getRelativePathOfField(contextFieldId, this.efxContext.symbol()))); } else if (ctx.nodeContext() != null) { final String contextNodeId = getNodeId(ctx.nodeContext()); this.efxContext.declareContextVariable(variable.name, new NodeContext(contextNodeId, this.symbols.getAbsolutePathOfNode(contextNodeId), - this.symbols.getRelativePathOfNode(contextNodeId, this.efxContext.absolutePath()))); + this.symbols.getRelativePathOfNode(contextNodeId, this.efxContext.symbol()))); } } @@ -908,13 +926,13 @@ public void exitDurationLiteral(DurationLiteralContext ctx) { @Override public void exitSimpleNodeReference(SimpleNodeReferenceContext ctx) { this.stack.push( - this.symbols.getRelativePathOfNode(ctx.NodeId().getText(), this.efxContext.absolutePath())); + this.symbols.getRelativePathOfNode(ctx.NodeId().getText(), this.efxContext.symbol())); } @Override public void exitSimpleFieldReference(EfxParser.SimpleFieldReferenceContext ctx) { this.stack.push( - symbols.getRelativePathOfField(ctx.FieldId().getText(), this.efxContext.absolutePath())); + symbols.getRelativePathOfField(ctx.FieldId().getText(), this.efxContext.symbol())); } @Override @@ -952,7 +970,7 @@ public void exitAbsoluteNodeReference(AbsoluteNodeReferenceContext ctx) { public void exitNodeReferenceWithPredicate(NodeReferenceWithPredicateContext ctx) { if (ctx.predicate() != null) { BooleanExpression predicate = this.stack.pop(BooleanExpression.class); - PathExpression nodeReference = this.stack.pop(NodePathExpression.class); + PathExpression nodeReference = this.stack.pop(NodePath.class); this.stack.push(this.script.composeNodeReferenceWithPredicate(nodeReference, predicate)); } } @@ -1043,10 +1061,10 @@ public void exitScalarFromFieldReference(ScalarFromFieldReferenceContext ctx) { String fieldId = getFieldId(ctx.fieldReference()); if (this.symbols.isAttributeField(fieldId)) { this.stack.push(this.script.composeFieldAttributeReference( - this.symbols.getRelativePath( + this.script.contextualizePath( this.symbols.getAbsolutePathOfFieldWithoutTheAttribute(fieldId), this.efxContext.peek().absolutePath()), this.symbols.getAttributeNameFromAttributeField(fieldId), - PathExpression.fromFieldType.get(FieldTypes.fromString(this.symbols.getTypeOfField(fieldId))))); + ScalarPath.fromFieldType.get(FieldTypes.fromString(this.symbols.getTypeOfField(fieldId))))); } else { this.stack.push(this.script.composeFieldValueReference(path)); } @@ -1058,10 +1076,10 @@ public void exitSequenceFromFieldReference(SequenceFromFieldReferenceContext ctx String fieldId = getFieldId(ctx.fieldReference()); if (this.symbols.isAttributeField(fieldId)) { this.stack.push(this.script.composeFieldAttributeReference( - this.symbols.getRelativePath( + this.script.contextualizePath( this.symbols.getAbsolutePathOfFieldWithoutTheAttribute(fieldId), this.efxContext.peek().absolutePath()), this.symbols.getAttributeNameFromAttributeField(fieldId), - PathExpression.fromFieldType.get(FieldTypes.fromString(this.symbols.getTypeOfField(fieldId))))); + ScalarPath.fromFieldType.get(FieldTypes.fromString(this.symbols.getTypeOfField(fieldId))))); } else { this.stack.push(this.script.composeFieldValueReference(path)); } @@ -1070,13 +1088,13 @@ public void exitSequenceFromFieldReference(SequenceFromFieldReferenceContext ctx @Override public void exitScalarFromAttributeReference(ScalarFromAttributeReferenceContext ctx) { this.stack.push(this.script.composeFieldAttributeReference(this.stack.pop(PathExpression.class), - ctx.attributeReference().Identifier().getText(), StringPathExpression.class)); + ctx.attributeReference().Identifier().getText(), StringPath.class)); } @Override public void exitSequenceFromAttributeReference(SequenceFromAttributeReferenceContext ctx) { this.stack.push(this.script.composeFieldAttributeReference(this.stack.pop(PathExpression.class), - ctx.attributeReference().Identifier().getText(), StringPathExpression.class)); + ctx.attributeReference().Identifier().getText(), StringPath.class)); } // #endregion Value References ---------------------------------------------- @@ -1094,7 +1112,7 @@ public void exitContextFieldSpecifier(ContextFieldSpecifierContext ctx) { final String contextFieldId = getFieldId(ctx.fieldContext()); this.efxContext .push(new FieldContext(contextFieldId, this.symbols.getAbsolutePathOfField(contextFieldId), - this.symbols.getRelativePathOfField(contextFieldId, this.efxContext.absolutePath()))); + this.symbols.getRelativePathOfField(contextFieldId, this.efxContext.symbol()))); } @@ -1123,7 +1141,7 @@ public void exitContextNodeSpecifier(ContextNodeSpecifierContext ctx) { final String contextNodeId = getNodeId(ctx.node); this.efxContext .push(new NodeContext(contextNodeId, this.symbols.getAbsolutePathOfNode(contextNodeId), - this.symbols.getRelativePathOfNode(contextNodeId, this.efxContext.absolutePath()))); + this.symbols.getRelativePathOfNode(contextNodeId, this.efxContext.symbol()))); } /** @@ -1146,13 +1164,13 @@ public void exitContextVariableSpecifier(ContextVariableSpecifierContext ctx) { if (variableContext.isFieldContext()) { this.efxContext.push(new FieldContext(variableContext.symbol(), this.symbols.getAbsolutePathOfField(variableContext.symbol()), this.symbols - .getRelativePathOfField(variableContext.symbol(), this.efxContext.absolutePath()))); + .getRelativePathOfField(variableContext.symbol(), this.efxContext.symbol()))); } else if (variableContext.isNodeContext()) { this.efxContext.push(new NodeContext(variableContext.symbol(), this.symbols.getAbsolutePathOfNode(variableContext.symbol()), this.symbols - .getRelativePathOfNode(variableContext.symbol(), this.efxContext.absolutePath()))); + .getRelativePathOfNode(variableContext.symbol(), this.efxContext.symbol()))); } else { - throw new IllegalStateException("Variable context is neither a field nor a node context."); + throw ConsistencyCheckException.invalidVariableContext(); } } diff --git a/src/main/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1.java b/src/main/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1.java index 1392d123..bad88c44 100644 --- a/src/main/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1.java +++ b/src/main/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1.java @@ -1,3 +1,16 @@ +/* + * 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.efx.sdk1; import java.io.IOException; @@ -28,11 +41,11 @@ import eu.europa.ted.efx.model.Context.FieldContext; import eu.europa.ted.efx.model.Context.NodeContext; import eu.europa.ted.efx.model.expressions.Expression; +import eu.europa.ted.efx.model.expressions.PathExpression; import eu.europa.ted.efx.model.expressions.TypedExpression; -import eu.europa.ted.efx.model.expressions.path.PathExpression; -import eu.europa.ted.efx.model.expressions.path.StringPathExpression; import eu.europa.ted.efx.model.expressions.scalar.DateExpression; import eu.europa.ted.efx.model.expressions.scalar.StringExpression; +import eu.europa.ted.efx.model.expressions.scalar.StringPath; import eu.europa.ted.efx.model.expressions.sequence.DateSequenceExpression; import eu.europa.ted.efx.model.expressions.sequence.StringSequenceExpression; import eu.europa.ted.efx.model.expressions.sequence.TimeSequenceExpression; @@ -359,10 +372,10 @@ private void shorthandIndirectLabelReference(final String fieldId) { final String fieldType = this.symbols.getTypeOfField(fieldId); final PathExpression valueReference = this.symbols.isAttributeField(fieldId) ? this.script.composeFieldAttributeReference( - this.symbols.getRelativePath(this.symbols.getAbsolutePathOfFieldWithoutTheAttribute(fieldId), currentContext.absolutePath()), - this.symbols.getAttributeNameFromAttributeField(fieldId), StringPathExpression.class) + this.script.contextualizePath(this.symbols.getAbsolutePathOfFieldWithoutTheAttribute(fieldId), currentContext.absolutePath()), + this.symbols.getAttributeNameFromAttributeField(fieldId), StringPath.class) : this.script.composeFieldValueReference( - this.symbols.getRelativePathOfField(fieldId, currentContext.absolutePath())); + this.symbols.getRelativePathOfField(fieldId, currentContext.symbol())); Variable loopVariable = new Variable("item", this.script.composeVariableDeclaration("item", StringExpression.class), StringExpression.empty(), this.script.composeVariableReference("item", StringExpression.class)); @@ -373,7 +386,7 @@ private void shorthandIndirectLabelReference(final String fieldId) { this.script.composeForExpression( this.script.composeIteratorList( List.of( - this.script.composeIteratorExpression(loopVariable.declarationExpression, valueReference))), + this.script.composeIteratorExpression(loopVariable.declarationExpression, valueReference.asSequence()))), this.script.composeStringConcatenation( List.of(this.script.getStringLiteralFromUnquotedString(ASSET_TYPE_INDICATOR), this.script.getStringLiteralFromUnquotedString("|"), @@ -392,7 +405,7 @@ private void shorthandIndirectLabelReference(final String fieldId) { this.script.composeForExpression( this.script.composeIteratorList( List.of( - this.script.composeIteratorExpression(loopVariable.declarationExpression, valueReference))), + this.script.composeIteratorExpression(loopVariable.declarationExpression, valueReference.asSequence()))), this.script.composeStringConcatenation(List.of( this.script.getStringLiteralFromUnquotedString(ASSET_TYPE_CODE), this.script.getStringLiteralFromUnquotedString("|"), @@ -535,7 +548,7 @@ public void exitShorthandFieldValueReferenceFromContextField( throw InvalidUsageException.shorthandRequiresFieldContext("$value"); } this.stack.push(this.script.composeFieldValueReference( - this.symbols.getRelativePathOfField(this.efxContext.symbol(), this.efxContext.absolutePath()))); + this.symbols.getRelativePathOfField(this.efxContext.symbol(), this.efxContext.symbol()))); } // #endregion Expression Blocks ${...} -------------------------------------- @@ -643,13 +656,13 @@ private Context relativizeContext(Context childContext, Context parentContext) { if (childContext.isFieldContext()) { return new FieldContext(childContext.symbol(), childContext.absolutePath(), - this.symbols.getRelativePath(childContext.absolutePath(), parentContextAbsolutePath), childContext.variable()); + this.script.contextualizePath(childContext.absolutePath(), parentContextAbsolutePath), childContext.variable()); } assert childContext.isNodeContext() : "Child context should be either a FieldContext NodeContext."; return new NodeContext(childContext.symbol(), childContext.absolutePath(), - this.symbols.getRelativePath(childContext.absolutePath(), parentContextAbsolutePath)); + this.script.contextualizePath(childContext.absolutePath(), parentContextAbsolutePath)); } // #endregion Template lines ----------------------------------------------- diff --git a/src/main/java/eu/europa/ted/efx/sdk1/TypeCheckerV1.java b/src/main/java/eu/europa/ted/efx/sdk1/TypeCheckerV1.java new file mode 100644 index 00000000..159d8e5a --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/sdk1/TypeCheckerV1.java @@ -0,0 +1,94 @@ +/* + * Copyright 2026 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.efx.sdk1; + +import eu.europa.ted.efx.interfaces.TypeChecker; +import eu.europa.ted.efx.model.expressions.PathExpression; +import eu.europa.ted.efx.model.expressions.TypedExpression; +import eu.europa.ted.efx.model.expressions.scalar.ScalarExpression; +import eu.europa.ted.efx.model.expressions.sequence.SequenceExpression; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; +import eu.europa.ted.efx.model.types.EfxTypeLattice; + +/** + * V1-compatible type checker that handles abstract base types (Sequence, Scalar). + * Replicates V1 behavior where EfxExpressionType.Path extended both Scalar and Sequence, + * allowing PathExpressions to convert to either target type. + */ +public class TypeCheckerV1 implements TypeChecker { + + public static final TypeCheckerV1 INSTANCE = new TypeCheckerV1(); + + private TypeCheckerV1() {} + + @Override + public boolean canConvert(Class from, + Class to) { + // Direct class compatibility + if (to.isAssignableFrom(from)) { + return true; + } + + // Handle abstract base types FIRST (before annotation check, since these have no annotations) + // PathExpression can convert to ANY Scalar or Sequence target + if (PathExpression.class.isAssignableFrom(from)) { + if (to == SequenceExpression.class || to == ScalarExpression.class) { + return true; + } + } + + // Get source annotation (required for remaining checks) + var fromAnnotation = from.getAnnotation(EfxDataTypeAssociation.class); + if (fromAnnotation == null) { + return false; + } + var fromType = fromAnnotation.dataType(); + + // Handle abstract target types (ScalarExpression/SequenceExpression have no annotations) + if (to == SequenceExpression.class) { + return EfxTypeLattice.isSequence(fromType) || EfxTypeLattice.isScalar(fromType); + } + if (to == ScalarExpression.class) { + return EfxTypeLattice.isScalar(fromType); + } + + // For concrete target types, get the annotation + var toAnnotation = to.getAnnotation(EfxDataTypeAssociation.class); + if (toAnnotation == null) { + return false; + } + var toType = toAnnotation.dataType(); + + // PathExpression to concrete type - check primitive compatibility + if (PathExpression.class.isAssignableFrom(from)) { + var fromPrimitive = EfxTypeLattice.toPrimitive(fromType); + var toPrimitive = EfxTypeLattice.toPrimitive(toType); + return toPrimitive.isAssignableFrom(fromPrimitive); + } + + // Direct type compatibility + if (toType.isAssignableFrom(fromType)) { + return true; + } + + // Scalar can promote to Sequence + if (EfxTypeLattice.isScalar(fromType) && EfxTypeLattice.isSequence(toType)) { + var fromPrimitive = EfxTypeLattice.toPrimitive(fromType); + var toPrimitive = EfxTypeLattice.toPrimitive(toType); + return toPrimitive.isAssignableFrom(fromPrimitive); + } + + return false; + } +} diff --git a/src/main/java/eu/europa/ted/efx/sdk1/entity/SdkCodelistV1.java b/src/main/java/eu/europa/ted/efx/sdk1/entity/SdkCodelistV1.java deleted file mode 100644 index c8ba0ec7..00000000 --- a/src/main/java/eu/europa/ted/efx/sdk1/entity/SdkCodelistV1.java +++ /dev/null @@ -1,21 +0,0 @@ -package eu.europa.ted.efx.sdk1.entity; - -import java.util.List; -import java.util.Optional; -import eu.europa.ted.eforms.sdk.component.SdkComponent; -import eu.europa.ted.eforms.sdk.component.SdkComponentType; -import eu.europa.ted.eforms.sdk.entity.SdkCodelist; - -/** - * Representation of an SdkCodelist for usage in the symbols map. - * - * @author rouschr - */ -@SdkComponent(versions = {"1"}, componentType = SdkComponentType.CODELIST) -public class SdkCodelistV1 extends SdkCodelist { - - public SdkCodelistV1(final String codelistId, final String codelistVersion, - final List codes, final Optional parentId) { - super(codelistId, codelistVersion, codes, parentId); - } -} diff --git a/src/main/java/eu/europa/ted/efx/sdk1/entity/SdkFieldV1.java b/src/main/java/eu/europa/ted/efx/sdk1/entity/SdkFieldV1.java deleted file mode 100644 index db807f73..00000000 --- a/src/main/java/eu/europa/ted/efx/sdk1/entity/SdkFieldV1.java +++ /dev/null @@ -1,47 +0,0 @@ -package eu.europa.ted.efx.sdk1.entity; - -import java.util.Map; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.JsonNode; -import eu.europa.ted.eforms.sdk.component.SdkComponent; -import eu.europa.ted.eforms.sdk.component.SdkComponentType; -import eu.europa.ted.eforms.sdk.entity.SdkField; - -@SdkComponent(versions = {"1"}, componentType = SdkComponentType.FIELD) -public class SdkFieldV1 extends SdkField { - - public SdkFieldV1(final String id, final String type, final String parentNodeId, - final String xpathAbsolute, - final String xpathRelative, final String codelistId) { - super(id, type, parentNodeId, xpathAbsolute, xpathRelative, codelistId); - } - - public SdkFieldV1(final JsonNode field) { - super(field); - } - - @JsonCreator - public SdkFieldV1( - @JsonProperty("id") final String id, - @JsonProperty("type") final String type, - @JsonProperty("parentNodeId") final String parentNodeId, - @JsonProperty("xpathAbsolute") final String xpathAbsolute, - @JsonProperty("xpathRelative") final String xpathRelative, - @JsonProperty("codeList") final Map> codelist) { - this(id, type, parentNodeId, xpathAbsolute, xpathRelative, getCodelistId(codelist)); - } - - protected static String getCodelistId(Map> codelist) { - if (codelist == null) { - return null; - } - - Map value = codelist.get("value"); - if (value == null) { - return null; - } - - return value.get("id"); - } -} diff --git a/src/main/java/eu/europa/ted/efx/sdk1/entity/SdkNodeV1.java b/src/main/java/eu/europa/ted/efx/sdk1/entity/SdkNodeV1.java deleted file mode 100644 index 502ea999..00000000 --- a/src/main/java/eu/europa/ted/efx/sdk1/entity/SdkNodeV1.java +++ /dev/null @@ -1,22 +0,0 @@ -package eu.europa.ted.efx.sdk1.entity; - -import com.fasterxml.jackson.databind.JsonNode; -import eu.europa.ted.eforms.sdk.component.SdkComponent; -import eu.europa.ted.eforms.sdk.component.SdkComponentType; -import eu.europa.ted.eforms.sdk.entity.SdkNode; - -/** - * A node is something like a section. Nodes can be parents of other nodes or parents of fields. - */ -@SdkComponent(versions = {"1"}, componentType = SdkComponentType.NODE) -public class SdkNodeV1 extends SdkNode { - - public SdkNodeV1(String id, String parentId, String xpathAbsolute, String xpathRelative, - boolean repeatable) { - super(id, parentId, xpathAbsolute, xpathRelative, repeatable); - } - - public SdkNodeV1(JsonNode node) { - super(node); - } -} diff --git a/src/main/java/eu/europa/ted/efx/sdk1/entity/SdkNoticeSubtypeV1.java b/src/main/java/eu/europa/ted/efx/sdk1/entity/SdkNoticeSubtypeV1.java deleted file mode 100644 index b8f2c689..00000000 --- a/src/main/java/eu/europa/ted/efx/sdk1/entity/SdkNoticeSubtypeV1.java +++ /dev/null @@ -1,21 +0,0 @@ -package eu.europa.ted.efx.sdk1.entity; - -import com.fasterxml.jackson.databind.JsonNode; -import eu.europa.ted.eforms.sdk.component.SdkComponent; -import eu.europa.ted.eforms.sdk.component.SdkComponentType; -import eu.europa.ted.eforms.sdk.entity.SdkNoticeSubtype; - -/** - * Represents a notice subtype from the SDK's notice-types.json file. - */ -@SdkComponent(versions = {"1"}, componentType = SdkComponentType.NOTICE_TYPE) -public class SdkNoticeSubtypeV1 extends SdkNoticeSubtype { - - public SdkNoticeSubtypeV1(String subTypeId, String documentType, String type) { - super(subTypeId, documentType, type); - } - - public SdkNoticeSubtypeV1(JsonNode json) { - super(json); - } -} diff --git a/src/main/java/eu/europa/ted/efx/sdk1/xpath/XPathScriptGeneratorV1.java b/src/main/java/eu/europa/ted/efx/sdk1/xpath/XPathScriptGeneratorV1.java index 8dce460c..5ce7c9fb 100644 --- a/src/main/java/eu/europa/ted/efx/sdk1/xpath/XPathScriptGeneratorV1.java +++ b/src/main/java/eu/europa/ted/efx/sdk1/xpath/XPathScriptGeneratorV1.java @@ -1,3 +1,16 @@ +/* + * Copyright 2023 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.efx.sdk1.xpath; import eu.europa.ted.eforms.sdk.component.SdkComponent; @@ -5,7 +18,8 @@ import eu.europa.ted.eforms.xpath.XPathInfo; import eu.europa.ted.eforms.xpath.XPathProcessor; import eu.europa.ted.efx.interfaces.TranslatorOptions; -import eu.europa.ted.efx.model.expressions.path.PathExpression; +import eu.europa.ted.efx.model.expressions.Expression; +import eu.europa.ted.efx.model.expressions.PathExpression; import eu.europa.ted.efx.model.types.EfxDataType; import eu.europa.ted.efx.xpath.XPathScriptGenerator; @@ -43,7 +57,7 @@ public XPathScriptGeneratorV1(TranslatorOptions translatorOptions) { public PathExpression composeFieldValueReference(PathExpression fieldReference) { XPathInfo xpathInfo = XPathProcessor.parse(fieldReference.getScript()); if (fieldReference.is(EfxDataType.MultilingualString.class) && !xpathInfo.hasPredicate("@languageID")) { - return PathExpression.instantiate("efx:preferred-language-text(" + fieldReference.getScript() + ")", fieldReference.getDataType()); + return Expression.instantiate("efx:preferred-language-text(" + fieldReference.getScript() + ")", fieldReference.getClass()); } return super.composeFieldValueReference(fieldReference); } diff --git a/src/main/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2.java b/src/main/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2.java index 4aeeba67..e439e85f 100644 --- a/src/main/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2.java +++ b/src/main/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2.java @@ -1,3 +1,16 @@ +/* + * 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.efx.sdk2; import static java.util.Map.entry; @@ -24,7 +37,10 @@ import eu.europa.ted.eforms.sdk.component.SdkComponent; import eu.europa.ted.eforms.sdk.component.SdkComponentType; import eu.europa.ted.efx.exceptions.InvalidArgumentException; +import eu.europa.ted.efx.exceptions.InvalidIdentifierException; import eu.europa.ted.efx.exceptions.SymbolResolutionException; +import eu.europa.ted.efx.exceptions.TypeMismatchException; +import eu.europa.ted.efx.exceptions.ConsistencyCheckException; import eu.europa.ted.efx.interfaces.EfxExpressionTranslator; import eu.europa.ted.efx.interfaces.ScriptGenerator; import eu.europa.ted.efx.interfaces.SymbolResolver; @@ -34,19 +50,19 @@ import eu.europa.ted.efx.model.Context.NodeContext; import eu.europa.ted.efx.model.ContextStack; import eu.europa.ted.efx.model.expressions.Expression; +import eu.europa.ted.efx.model.expressions.PathExpression; import eu.europa.ted.efx.model.expressions.TypedExpression; import eu.europa.ted.efx.model.expressions.iteration.IteratorExpression; import eu.europa.ted.efx.model.expressions.iteration.IteratorListExpression; -import eu.europa.ted.efx.model.expressions.path.MultilingualStringPathExpression; -import eu.europa.ted.efx.model.expressions.path.NodePathExpression; -import eu.europa.ted.efx.model.expressions.path.PathExpression; -import eu.europa.ted.efx.model.expressions.path.StringPathExpression; import eu.europa.ted.efx.model.expressions.scalar.BooleanExpression; import eu.europa.ted.efx.model.expressions.scalar.DateExpression; import eu.europa.ted.efx.model.expressions.scalar.DurationExpression; +import eu.europa.ted.efx.model.expressions.scalar.MultilingualStringPath; import eu.europa.ted.efx.model.expressions.scalar.NumericExpression; import eu.europa.ted.efx.model.expressions.scalar.ScalarExpression; +import eu.europa.ted.efx.model.expressions.scalar.ScalarPath; import eu.europa.ted.efx.model.expressions.scalar.StringExpression; +import eu.europa.ted.efx.model.expressions.scalar.StringPath; import eu.europa.ted.efx.model.expressions.scalar.TimeExpression; import eu.europa.ted.efx.model.expressions.sequence.BooleanSequenceExpression; import eu.europa.ted.efx.model.expressions.sequence.DateSequenceExpression; @@ -56,6 +72,7 @@ import eu.europa.ted.efx.model.expressions.sequence.StringSequenceExpression; import eu.europa.ted.efx.model.expressions.sequence.TimeSequenceExpression; import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxTypeLattice; import eu.europa.ted.efx.model.types.FieldTypes; import eu.europa.ted.efx.model.variables.ParsedArguments; import eu.europa.ted.efx.model.variables.Function; @@ -310,7 +327,7 @@ public void enterSingleExpression(SingleExpressionContext ctx) { if (nodeId != null) { this.efxContext.pushNodeContext(nodeId); } else { - throw SymbolResolutionException.unknownAlias(alias.getText()); + throw SymbolResolutionException.unknownSymbol(alias.getText()); } } } @@ -715,58 +732,58 @@ private void exitConditionalDurationExpression() { @Override public void exitStringIteratorExpression(StringIteratorExpressionContext ctx) { - this.exitIteratorExpression(ctx.stringVariableDeclaration().variableName.getText(), StringExpression.class, StringSequenceExpression.class); + this.exitIteratorExpression(ctx.stringIteratorVariableDeclaration().variableName.getText(), StringExpression.class, StringSequenceExpression.class); } @Override public void exitBooleanIteratorExpression(BooleanIteratorExpressionContext ctx) { - this.exitIteratorExpression(ctx.booleanVariableDeclaration().variableName.getText(), BooleanExpression.class, BooleanSequenceExpression.class); + this.exitIteratorExpression(ctx.booleanIteratorVariableDeclaration().variableName.getText(), BooleanExpression.class, BooleanSequenceExpression.class); } @Override public void exitNumericIteratorExpression(NumericIteratorExpressionContext ctx) { - this.exitIteratorExpression(ctx.numericVariableDeclaration().variableName.getText(), NumericExpression.class, NumericSequenceExpression.class); + this.exitIteratorExpression(ctx.numericIteratorVariableDeclaration().variableName.getText(), NumericExpression.class, NumericSequenceExpression.class); } @Override public void exitDateIteratorExpression(DateIteratorExpressionContext ctx) { - this.exitIteratorExpression(ctx.dateVariableDeclaration().variableName.getText(), DateExpression.class, DateSequenceExpression.class); + this.exitIteratorExpression(ctx.dateIteratorVariableDeclaration().variableName.getText(), DateExpression.class, DateSequenceExpression.class); } @Override public void exitTimeIteratorExpression(TimeIteratorExpressionContext ctx) { - this.exitIteratorExpression(ctx.timeVariableDeclaration().variableName.getText(), TimeExpression.class, TimeSequenceExpression.class); + this.exitIteratorExpression(ctx.timeIteratorVariableDeclaration().variableName.getText(), TimeExpression.class, TimeSequenceExpression.class); } @Override public void exitDurationIteratorExpression(DurationIteratorExpressionContext ctx) { - this.exitIteratorExpression(ctx.durationVariableDeclaration().variableName.getText(), DurationExpression.class, DurationSequenceExpression.class); + this.exitIteratorExpression(ctx.durationIteratorVariableDeclaration().variableName.getText(), DurationExpression.class, DurationSequenceExpression.class); } @Override public void exitContextIteratorExpression(ContextIteratorExpressionContext ctx) { PathExpression path = this.stack.pop(PathExpression.class); - var variableType = path.getClass(); - var variableName = ctx.contextVariableDeclaration().variableName.getText(); + var variableType = path.asScalar().getClass(); + var variableName = ctx.contextIteratorVariableDeclaration().variableName.getText(); Variable variable = new Variable(variableName, this.script.composeVariableDeclaration(variableName, variableType), Expression.empty(variableType), this.script.composeVariableReference(variableName, variableType)); this.stack.declareIdentifier(variable); - this.stack.push(this.script.composeIteratorExpression(variable.declarationExpression, path)); + this.stack.push(this.script.composeIteratorExpression(variable.declarationExpression, path.asSequence())); if (ctx.fieldContext() != null) { final String contextFieldId = getFieldId(ctx.fieldContext()); this.efxContext.declareContextVariable(variable.name, new FieldContext(contextFieldId, this.symbols.getAbsolutePathOfField(contextFieldId), - this.symbols.getRelativePathOfField(contextFieldId, this.efxContext.absolutePath()))); + this.symbols.getRelativePathOfField(contextFieldId, this.efxContext.symbol()))); } else if (ctx.nodeContext() != null) { final String contextNodeId = getNodeId(ctx.nodeContext()); this.efxContext.declareContextVariable(variable.name, new NodeContext(contextNodeId, this.symbols.getAbsolutePathOfNode(contextNodeId), - this.symbols.getRelativePathOfNode(contextNodeId, this.efxContext.absolutePath()))); + this.symbols.getRelativePathOfNode(contextNodeId, this.efxContext.symbol()))); } } @@ -780,39 +797,37 @@ public void exitIteratorList(IteratorListContext ctx) { } @Override - public void exitParenthesizedStringsFromIteration(ParenthesizedStringsFromIterationContext ctx) { + public void exitParenthesizedStrings(ParenthesizedStringsContext ctx) { this.stack.push(this.script.composeParenthesizedExpression( this.stack.pop(StringSequenceExpression.class), StringSequenceExpression.class)); } @Override - public void exitParenthesizedNumbersFromIteration(ParenthesizedNumbersFromIterationContext ctx) { + public void exitParenthesizedNumbers(ParenthesizedNumbersContext ctx) { this.stack.push(this.script.composeParenthesizedExpression( this.stack.pop(NumericSequenceExpression.class), NumericSequenceExpression.class)); } @Override - public void exitParenthesizedBooleansFromIteration( - ParenthesizedBooleansFromIterationContext ctx) { + public void exitParenthesizedBooleans(ParenthesizedBooleansContext ctx) { this.stack.push(this.script.composeParenthesizedExpression( this.stack.pop(BooleanSequenceExpression.class), BooleanSequenceExpression.class)); } @Override - public void exitParenthesizedDatesFromIteration(ParenthesizedDatesFromIterationContext ctx) { + public void exitParenthesizedDates(ParenthesizedDatesContext ctx) { this.stack.push(this.script.composeParenthesizedExpression( this.stack.pop(DateSequenceExpression.class), DateSequenceExpression.class)); } @Override - public void exitParenthesizedTimesFromIteration(ParenthesizedTimesFromIterationContext ctx) { + public void exitParenthesizedTimes(ParenthesizedTimesContext ctx) { this.stack.push(this.script.composeParenthesizedExpression( this.stack.pop(TimeSequenceExpression.class), TimeSequenceExpression.class)); } @Override - public void exitParenthesizedDurationsFromIteration( - ParenthesizedDurationsFromIterationContext ctx) { + public void exitParenthesizedDurations(ParenthesizedDurationsContext ctx) { this.stack.push(this.script.composeParenthesizedExpression( this.stack.pop(DurationSequenceExpression.class), DurationSequenceExpression.class)); } @@ -942,6 +957,67 @@ public void exitDurationLiteral(DurationLiteralContext ctx) { this.stack.push(this.script.getDurationLiteralEquivalent(ctx.getText())); } + // Sequence literals for parameter values + @Override + public void exitStringSequenceLiteral(StringSequenceLiteralContext ctx) { + int count = ctx.stringLiteral().size(); + List items = new ArrayList<>(); + for (int i = 0; i < count; i++) { + items.add(0, this.stack.pop(StringExpression.class)); + } + this.stack.push(this.script.composeList(items, StringSequenceExpression.class)); + } + + @Override + public void exitNumericSequenceLiteral(NumericSequenceLiteralContext ctx) { + int count = ctx.numericLiteral().size(); + List items = new ArrayList<>(); + for (int i = 0; i < count; i++) { + items.add(0, this.stack.pop(NumericExpression.class)); + } + this.stack.push(this.script.composeList(items, NumericSequenceExpression.class)); + } + + @Override + public void exitBooleanSequenceLiteral(BooleanSequenceLiteralContext ctx) { + int count = ctx.booleanLiteral().size(); + List items = new ArrayList<>(); + for (int i = 0; i < count; i++) { + items.add(0, this.stack.pop(BooleanExpression.class)); + } + this.stack.push(this.script.composeList(items, BooleanSequenceExpression.class)); + } + + @Override + public void exitDateSequenceLiteral(DateSequenceLiteralContext ctx) { + int count = ctx.dateLiteral().size(); + List items = new ArrayList<>(); + for (int i = 0; i < count; i++) { + items.add(0, this.stack.pop(DateExpression.class)); + } + this.stack.push(this.script.composeList(items, DateSequenceExpression.class)); + } + + @Override + public void exitTimeSequenceLiteral(TimeSequenceLiteralContext ctx) { + int count = ctx.timeLiteral().size(); + List items = new ArrayList<>(); + for (int i = 0; i < count; i++) { + items.add(0, this.stack.pop(TimeExpression.class)); + } + this.stack.push(this.script.composeList(items, TimeSequenceExpression.class)); + } + + @Override + public void exitDurationSequenceLiteral(DurationSequenceLiteralContext ctx) { + int count = ctx.durationLiteral().size(); + List items = new ArrayList<>(); + for (int i = 0; i < count; i++) { + items.add(0, this.stack.pop(DurationExpression.class)); + } + this.stack.push(this.script.composeList(items, DurationSequenceExpression.class)); + } + // #endregion Literals ------------------------------------------------------ // #region References ------------------------------------------------------- @@ -949,13 +1025,13 @@ public void exitDurationLiteral(DurationLiteralContext ctx) { @Override public void exitSimpleNodeReference(SimpleNodeReferenceContext ctx) { this.stack.push( - this.symbols.getRelativePathOfNode(ctx.NodeId().getText(), this.efxContext.absolutePath())); + this.symbols.getRelativePathOfNode(ctx.NodeId().getText(), this.efxContext.symbol())); } @Override public void exitSimpleFieldReference(EfxParser.SimpleFieldReferenceContext ctx) { this.stack.push( - symbols.getRelativePathOfField(ctx.fieldId.getText(), this.efxContext.absolutePath())); + symbols.getRelativePathOfField(ctx.fieldId.getText(), this.efxContext.symbol())); } @Override @@ -993,7 +1069,7 @@ public void exitAbsoluteNodeReference(AbsoluteNodeReferenceContext ctx) { public void exitNodeReferenceWithPredicate(NodeReferenceWithPredicateContext ctx) { if (ctx.predicate() != null) { BooleanExpression predicate = this.stack.pop(BooleanExpression.class); - PathExpression nodeReference = this.stack.pop(NodePathExpression.class); + PathExpression nodeReference = this.stack.pop(PathExpression.class); this.stack.push(this.script.composeNodeReferenceWithPredicate(nodeReference, predicate)); } } @@ -1084,10 +1160,10 @@ public void exitScalarFromFieldReference(ScalarFromFieldReferenceContext ctx) { String fieldId = getFieldId(ctx.fieldReference()); if (this.symbols.isAttributeField(fieldId)) { this.stack.push(this.script.composeFieldAttributeReference( - this.symbols.getRelativePath( + this.script.contextualizePath( this.symbols.getAbsolutePathOfFieldWithoutTheAttribute(fieldId), this.efxContext.peek().absolutePath()), this.symbols.getAttributeNameFromAttributeField(fieldId), - PathExpression.fromFieldType.get(FieldTypes.fromString(this.symbols.getTypeOfField(fieldId))))); + ScalarPath.fromFieldType.get(FieldTypes.fromString(this.symbols.getTypeOfField(fieldId))))); } else { this.stack.push(this.script.composeFieldValueReference(path)); } @@ -1099,10 +1175,10 @@ public void exitSequenceFromFieldReference(SequenceFromFieldReferenceContext ctx String fieldId = getFieldId(ctx.fieldReference()); if (this.symbols.isAttributeField(fieldId)) { this.stack.push(this.script.composeFieldAttributeReference( - this.symbols.getRelativePath( + this.script.contextualizePath( this.symbols.getAbsolutePathOfFieldWithoutTheAttribute(fieldId), this.efxContext.peek().absolutePath()), this.symbols.getAttributeNameFromAttributeField(fieldId), - PathExpression.fromFieldType.get(FieldTypes.fromString(this.symbols.getTypeOfField(fieldId))))); + ScalarPath.fromFieldType.get(FieldTypes.fromString(this.symbols.getTypeOfField(fieldId))))); } else { this.stack.push(this.script.composeFieldValueReference(path)); } @@ -1111,13 +1187,13 @@ public void exitSequenceFromFieldReference(SequenceFromFieldReferenceContext ctx @Override public void exitScalarFromAttributeReference(ScalarFromAttributeReferenceContext ctx) { this.stack.push(this.script.composeFieldAttributeReference(this.stack.pop(PathExpression.class), - ctx.attributeReference().attributeName.getText(), StringPathExpression.class)); + ctx.attributeReference().attributeName.getText(), StringPath.class)); } @Override public void exitSequenceFromAttributeReference(SequenceFromAttributeReferenceContext ctx) { this.stack.push(this.script.composeFieldAttributeReference(this.stack.pop(PathExpression.class), - ctx.attributeReference().attributeName.getText(), StringPathExpression.class)); + ctx.attributeReference().attributeName.getText(), StringPath.class)); } // #endregion Value References ---------------------------------------------- @@ -1135,7 +1211,7 @@ public void exitContextFieldSpecifier(ContextFieldSpecifierContext ctx) { final String contextFieldId = getFieldId(ctx.fieldContext()); this.efxContext .push(new FieldContext(contextFieldId, this.symbols.getAbsolutePathOfField(contextFieldId), - this.symbols.getRelativePathOfField(contextFieldId, this.efxContext.absolutePath()))); + this.symbols.getRelativePathOfField(contextFieldId, this.efxContext.symbol()))); } @@ -1164,7 +1240,7 @@ public void exitContextNodeSpecifier(ContextNodeSpecifierContext ctx) { final String contextNodeId = getNodeId(ctx.node); this.efxContext .push(new NodeContext(contextNodeId, this.symbols.getAbsolutePathOfNode(contextNodeId), - this.symbols.getRelativePathOfNode(contextNodeId, this.efxContext.absolutePath()))); + this.symbols.getRelativePathOfNode(contextNodeId, this.efxContext.symbol()))); } /** @@ -1183,18 +1259,24 @@ public void exitFieldReferenceWithNodeContextOverride( @Override public void exitContextVariableSpecifier(ContextVariableSpecifierContext ctx) { - Context variableContext = this.efxContext.getContextFromVariable(ctx.variableReference().variableName.getText()); + String variableName = ctx.variableReference().variableName.getText(); + Context variableContext = this.efxContext.getContextFromVariable(variableName); + if (variableContext == null) { + throw InvalidIdentifierException.notAContextVariable(variableName); + } if (variableContext.isFieldContext()) { this.efxContext.push(new FieldContext(variableContext.symbol(), this.symbols.getAbsolutePathOfField(variableContext.symbol()), this.symbols - .getRelativePathOfField(variableContext.symbol(), this.efxContext.absolutePath()))); + .getRelativePathOfField(variableContext.symbol(), this.efxContext.symbol()))); } else if (variableContext.isNodeContext()) { this.efxContext.push(new NodeContext(variableContext.symbol(), this.symbols.getAbsolutePathOfNode(variableContext.symbol()), this.symbols - .getRelativePathOfNode(variableContext.symbol(), this.efxContext.absolutePath()))); + .getRelativePathOfNode(variableContext.symbol(), this.efxContext.symbol()))); } else { - throw new IllegalStateException("Variable context is neither a field nor a node context."); + assert false : "Context variable must be either a field or node context: " + variableName; } + // Push the context variable's path onto the stack for use by exitFieldReferenceWithVariableContextOverride + this.stack.pushIdentifierReference(variableName); } @Override @@ -1220,8 +1302,14 @@ public void exitCodelistReference(CodelistReferenceContext ctx) { } @Override - public void exitVariableReference(VariableReferenceContext ctx) { - String variableName = ctx.variableName.getText(); + public void exitScalarFromVariableReference(ScalarFromVariableReferenceContext ctx) { + String variableName = ctx.variableReference().variableName.getText(); + this.stack.pushIdentifierReference(variableName); + } + + @Override + public void exitSequenceFromVariableReference(SequenceFromVariableReferenceContext ctx) { + String variableName = ctx.variableReference().variableName.getText(); this.stack.pushIdentifierReference(variableName); } @@ -1264,7 +1352,14 @@ public void exitDurationAtSequenceIndex(DurationAtSequenceIndexContext ctx) { private void exitSequenceAtIndex( Class itemType, Class listType) { NumericExpression index = this.stack.pop(NumericExpression.class); + + var addParenthesis = this.stack.peek() instanceof PathExpression; + T list = this.stack.pop(listType); + + if (addParenthesis) { + list = this.script.composeParenthesizedExpression(list, listType); + } this.stack.push(this.script.composeIndexer(list, index, itemType)); } @@ -1334,6 +1429,30 @@ public void exitDurationFunctionInvocation(DurationFunctionInvocationContext ctx this.stack.push(this.script.composeFunctionInvocation(ctx.functionInvocation().functionName.getText(), parameters, DurationExpression.class)); } + // Map from EfxDataType to sequence expression types for function invocations + private static final Map, Class> efxDataTypeToSequenceExpressionMap = Map.ofEntries( + entry(EfxDataType.String.class, StringSequenceExpression.class), + entry(EfxDataType.Boolean.class, BooleanSequenceExpression.class), + entry(EfxDataType.Number.class, NumericSequenceExpression.class), + entry(EfxDataType.Date.class, DateSequenceExpression.class), + entry(EfxDataType.Time.class, TimeSequenceExpression.class), + entry(EfxDataType.Duration.class, DurationSequenceExpression.class)); + + @Override + public void exitSequenceFromFunctionInvocation(SequenceFromFunctionInvocationContext ctx) { + var arguments = this.stack.pop(StrictArguments.class); + var function = (Function) arguments.identifier; + var baseType = EfxTypeLattice.toPrimitive(function.dataType); + var sequenceExpressionType = efxDataTypeToSequenceExpressionMap.get(baseType); + if (sequenceExpressionType == null) { + throw ConsistencyCheckException.missingTypeMapping(function.dataType, "efxDataTypeToSequenceExpressionMap"); + } + this.stack.push(this.script.composeFunctionInvocation( + ctx.functionInvocation().functionName.getText(), + arguments.getArgumentValues(), + sequenceExpressionType)); + } + // #endregion New in EFX-2: Function invocation ----------------------------- // #region Parameter Declarations ------------------------------------------- @@ -1369,6 +1488,37 @@ public void exitDurationParameterDeclaration(DurationParameterDeclarationContext this.exitParameterDeclaration(ctx.parameterName.getText(), DurationExpression.class); } + // Sequence parameter declarations + @Override + public void exitStringSequenceParameterDeclaration(StringSequenceParameterDeclarationContext ctx) { + this.exitParameterDeclaration(ctx.parameterName.getText(), StringSequenceExpression.class); + } + + @Override + public void exitNumericSequenceParameterDeclaration(NumericSequenceParameterDeclarationContext ctx) { + this.exitParameterDeclaration(ctx.parameterName.getText(), NumericSequenceExpression.class); + } + + @Override + public void exitBooleanSequenceParameterDeclaration(BooleanSequenceParameterDeclarationContext ctx) { + this.exitParameterDeclaration(ctx.parameterName.getText(), BooleanSequenceExpression.class); + } + + @Override + public void exitDateSequenceParameterDeclaration(DateSequenceParameterDeclarationContext ctx) { + this.exitParameterDeclaration(ctx.parameterName.getText(), DateSequenceExpression.class); + } + + @Override + public void exitTimeSequenceParameterDeclaration(TimeSequenceParameterDeclarationContext ctx) { + this.exitParameterDeclaration(ctx.parameterName.getText(), TimeSequenceExpression.class); + } + + @Override + public void exitDurationSequenceParameterDeclaration(DurationSequenceParameterDeclarationContext ctx) { + this.exitParameterDeclaration(ctx.parameterName.getText(), DurationSequenceExpression.class); + } + private void exitParameterDeclaration(String parameterName, Class parameterType) { if (this.expressionArguments.isEmpty()) { throw InvalidArgumentException.missingArgument(parameterName); @@ -1409,10 +1559,40 @@ public void exitEndsWithFunction(EndsWithFunctionContext ctx) { this.stack.push(this.script.composeEndsWithCondition(text, endsWith)); } + // sequence-equal typed handlers @Override - public void exitSequenceEqualFunction(SequenceEqualFunctionContext ctx) { - final SequenceExpression two = this.stack.pop(SequenceExpression.class); - final SequenceExpression one = this.stack.pop(SequenceExpression.class); + public void exitStringSequenceEqualFunction(StringSequenceEqualFunctionContext ctx) { + exitSequenceEqualFunction(StringSequenceExpression.class); + } + + @Override + public void exitBooleanSequenceEqualFunction(BooleanSequenceEqualFunctionContext ctx) { + exitSequenceEqualFunction(BooleanSequenceExpression.class); + } + + @Override + public void exitNumericSequenceEqualFunction(NumericSequenceEqualFunctionContext ctx) { + exitSequenceEqualFunction(NumericSequenceExpression.class); + } + + @Override + public void exitDateSequenceEqualFunction(DateSequenceEqualFunctionContext ctx) { + exitSequenceEqualFunction(DateSequenceExpression.class); + } + + @Override + public void exitTimeSequenceEqualFunction(TimeSequenceEqualFunctionContext ctx) { + exitSequenceEqualFunction(TimeSequenceExpression.class); + } + + @Override + public void exitDurationSequenceEqualFunction(DurationSequenceEqualFunctionContext ctx) { + exitSequenceEqualFunction(DurationSequenceExpression.class); + } + + private void exitSequenceEqualFunction(Class sequenceType) { + final T two = this.stack.pop(sequenceType); + final T one = this.stack.pop(sequenceType); this.stack.push(this.script.composeSequenceEqualFunction(one, two)); } @@ -1421,9 +1601,33 @@ public void exitSequenceEqualFunction(SequenceEqualFunctionContext ctx) { // #region Numeric functions ------------------------------------------------ @Override - public void exitCountFunction(CountFunctionContext ctx) { - final SequenceExpression expression = this.stack.pop(SequenceExpression.class); - this.stack.push(this.script.composeCountOperation(expression)); + public void exitCountStringsFunction(CountStringsFunctionContext ctx) { + this.stack.push(this.script.composeCountOperation(this.stack.pop(StringSequenceExpression.class))); + } + + @Override + public void exitCountBooleansFunction(CountBooleansFunctionContext ctx) { + this.stack.push(this.script.composeCountOperation(this.stack.pop(BooleanSequenceExpression.class))); + } + + @Override + public void exitCountNumbersFunction(CountNumbersFunctionContext ctx) { + this.stack.push(this.script.composeCountOperation(this.stack.pop(NumericSequenceExpression.class))); + } + + @Override + public void exitCountDatesFunction(CountDatesFunctionContext ctx) { + this.stack.push(this.script.composeCountOperation(this.stack.pop(DateSequenceExpression.class))); + } + + @Override + public void exitCountTimesFunction(CountTimesFunctionContext ctx) { + this.stack.push(this.script.composeCountOperation(this.stack.pop(TimeSequenceExpression.class))); + } + + @Override + public void exitCountDurationsFunction(CountDurationsFunctionContext ctx) { + this.stack.push(this.script.composeCountOperation(this.stack.pop(DurationSequenceExpression.class))); } @Override @@ -1506,12 +1710,12 @@ public void exitStringJoinFunction(StringJoinFunctionContext ctx) { @Override public void exitPreferredLanguageFunction(PreferredLanguageFunctionContext ctx) { - this.stack.push(this.script.getPreferredLanguage(this.stack.pop(MultilingualStringPathExpression.class))); + this.stack.push(this.script.getPreferredLanguage(this.stack.pop(MultilingualStringPath.class))); } @Override public void exitPreferredLanguageTextFunction(PreferredLanguageTextFunctionContext ctx) { - this.stack.push(this.script.getTextInPreferredLanguage(this.stack.pop(MultilingualStringPathExpression.class))); + this.stack.push(this.script.getTextInPreferredLanguage(this.stack.pop(MultilingualStringPath.class))); } @Override @@ -1579,120 +1783,148 @@ public void exitYearMonthDurationFromStringFunction( // #region Sequence Functions ----------------------------------------------- + // distinct-values typed handlers @Override - public void exitDistinctValuesFunction(DistinctValuesFunctionContext ctx) { - var sequence = this.stack.peek(); - assert sequence instanceof TypedExpression : "Expected a TypedExpression at the top of the stack."; - final Class sequenceType = ((TypedExpression)sequence).getDataType(); - if (EfxDataType.String.class.isAssignableFrom(sequenceType)) { - this.exitDistinctValuesFunction(StringSequenceExpression.class); - } else if (EfxDataType.Number.class.isAssignableFrom(sequenceType)) { - this.exitDistinctValuesFunction(NumericSequenceExpression.class); - } else if (EfxDataType.Boolean.class.isAssignableFrom(sequenceType)) { - this.exitDistinctValuesFunction(BooleanSequenceExpression.class); - } else if (EfxDataType.Date.class.isAssignableFrom(sequenceType)) { - this.exitDistinctValuesFunction(DateSequenceExpression.class); - } else if (EfxDataType.Time.class.isAssignableFrom(sequenceType)) { - this.exitDistinctValuesFunction(TimeSequenceExpression.class); - } else if (EfxDataType.Duration.class.isAssignableFrom(sequenceType)) { - this.exitDistinctValuesFunction(DurationSequenceExpression.class); - } else { - throw InvalidArgumentException.unsupportedSequenceType(sequenceType.getSimpleName(), - EfxLexer.VOCABULARY.getLiteralName(EfxLexer.DistinctValuesFunction)); - } + public void exitStringDistinctValuesFunction(StringDistinctValuesFunctionContext ctx) { + exitDistinctValuesFunction(StringSequenceExpression.class); + } + + @Override + public void exitBooleanDistinctValuesFunction(BooleanDistinctValuesFunctionContext ctx) { + exitDistinctValuesFunction(BooleanSequenceExpression.class); + } + + @Override + public void exitNumericDistinctValuesFunction(NumericDistinctValuesFunctionContext ctx) { + exitDistinctValuesFunction(NumericSequenceExpression.class); + } + + @Override + public void exitDateDistinctValuesFunction(DateDistinctValuesFunctionContext ctx) { + exitDistinctValuesFunction(DateSequenceExpression.class); + } + + @Override + public void exitTimeDistinctValuesFunction(TimeDistinctValuesFunctionContext ctx) { + exitDistinctValuesFunction(TimeSequenceExpression.class); } - private void exitDistinctValuesFunction( - Class listType) { + @Override + public void exitDurationDistinctValuesFunction(DurationDistinctValuesFunctionContext ctx) { + exitDistinctValuesFunction(DurationSequenceExpression.class); + } + + private void exitDistinctValuesFunction(Class listType) { final T list = this.stack.pop(listType); this.stack.push(this.script.composeDistinctValuesFunction(list, listType)); } + // union typed handlers @Override - public void exitUnionFunction(UnionFunctionContext ctx) { - var sequence = this.stack.peek(); - assert sequence instanceof TypedExpression : "Expected a TypedExpression at the top of the stack."; - final Class sequenceType = ((TypedExpression)sequence).getDataType(); - if (EfxDataType.String.class.isAssignableFrom(sequenceType)) { - this.exitUnionFunction(StringSequenceExpression.class); - } else if (EfxDataType.Number.class.isAssignableFrom(sequenceType)) { - this.exitUnionFunction(NumericSequenceExpression.class); - } else if (EfxDataType.Boolean.class.isAssignableFrom(sequenceType)) { - this.exitUnionFunction(BooleanSequenceExpression.class); - } else if (EfxDataType.Date.class.isAssignableFrom(sequenceType)) { - this.exitUnionFunction(DateSequenceExpression.class); - } else if (EfxDataType.Time.class.isAssignableFrom(sequenceType)) { - this.exitUnionFunction(TimeSequenceExpression.class); - } else if (EfxDataType.Duration.class.isAssignableFrom(sequenceType)) { - this.exitUnionFunction(DurationSequenceExpression.class); - } else { - throw InvalidArgumentException.unsupportedSequenceType(sequenceType.getSimpleName(), - EfxLexer.VOCABULARY.getLiteralName(EfxLexer.UnionFunction)); - } + public void exitStringUnionFunction(StringUnionFunctionContext ctx) { + exitUnionFunction(StringSequenceExpression.class); + } + + @Override + public void exitBooleanUnionFunction(BooleanUnionFunctionContext ctx) { + exitUnionFunction(BooleanSequenceExpression.class); + } + + @Override + public void exitNumericUnionFunction(NumericUnionFunctionContext ctx) { + exitUnionFunction(NumericSequenceExpression.class); + } + + @Override + public void exitDateUnionFunction(DateUnionFunctionContext ctx) { + exitUnionFunction(DateSequenceExpression.class); + } + + @Override + public void exitTimeUnionFunction(TimeUnionFunctionContext ctx) { + exitUnionFunction(TimeSequenceExpression.class); + } + + @Override + public void exitDurationUnionFunction(DurationUnionFunctionContext ctx) { + exitUnionFunction(DurationSequenceExpression.class); } - private void exitUnionFunction( - Class listType) { + private void exitUnionFunction(Class listType) { final T two = this.stack.pop(listType); final T one = this.stack.pop(listType); this.stack.push(this.script.composeUnionFunction(one, two, listType)); } + // intersect typed handlers @Override - public void exitIntersectFunction(IntersectFunctionContext ctx) { - var sequence = this.stack.peek(); - assert sequence instanceof TypedExpression : "Expected a TypedExpression at the top of the stack."; - final Class sequenceType = ((TypedExpression)sequence).getDataType(); - if (EfxDataType.String.class.isAssignableFrom(sequenceType)) { - this.exitIntersectFunction(StringSequenceExpression.class); - } else if (EfxDataType.Number.class.isAssignableFrom(sequenceType)) { - this.exitIntersectFunction(NumericSequenceExpression.class); - } else if (EfxDataType.Boolean.class.isAssignableFrom(sequenceType)) { - this.exitIntersectFunction(BooleanSequenceExpression.class); - } else if (EfxDataType.Date.class.isAssignableFrom(sequenceType)) { - this.exitIntersectFunction(DateSequenceExpression.class); - } else if (EfxDataType.Time.class.isAssignableFrom(sequenceType)) { - this.exitIntersectFunction(TimeSequenceExpression.class); - } else if (EfxDataType.Duration.class.isAssignableFrom(sequenceType)) { - this.exitIntersectFunction(DurationSequenceExpression.class); - } else { - throw InvalidArgumentException.unsupportedSequenceType(sequenceType.getSimpleName(), - EfxLexer.VOCABULARY.getLiteralName(EfxLexer.IntersectFunction)); - } + public void exitStringIntersectFunction(StringIntersectFunctionContext ctx) { + exitIntersectFunction(StringSequenceExpression.class); + } + + @Override + public void exitBooleanIntersectFunction(BooleanIntersectFunctionContext ctx) { + exitIntersectFunction(BooleanSequenceExpression.class); + } + + @Override + public void exitNumericIntersectFunction(NumericIntersectFunctionContext ctx) { + exitIntersectFunction(NumericSequenceExpression.class); + } + + @Override + public void exitDateIntersectFunction(DateIntersectFunctionContext ctx) { + exitIntersectFunction(DateSequenceExpression.class); } - private void exitIntersectFunction( - Class listType) { + @Override + public void exitTimeIntersectFunction(TimeIntersectFunctionContext ctx) { + exitIntersectFunction(TimeSequenceExpression.class); + } + + @Override + public void exitDurationIntersectFunction(DurationIntersectFunctionContext ctx) { + exitIntersectFunction(DurationSequenceExpression.class); + } + + private void exitIntersectFunction(Class listType) { final T two = this.stack.pop(listType); final T one = this.stack.pop(listType); this.stack.push(this.script.composeIntersectFunction(one, two, listType)); } + // except typed handlers @Override - public void exitExceptFunction(ExceptFunctionContext ctx) { - var sequence = this.stack.peek(); - assert sequence instanceof TypedExpression : "Expected a TypedExpression at the top of the stack."; - final Class sequenceType = ((TypedExpression)sequence).getDataType(); - if (EfxDataType.String.class.isAssignableFrom(sequenceType)) { - this.exitExceptFunction(StringSequenceExpression.class); - } else if (EfxDataType.Number.class.isAssignableFrom(sequenceType)) { - this.exitExceptFunction(NumericSequenceExpression.class); - } else if (EfxDataType.Boolean.class.isAssignableFrom(sequenceType)) { - this.exitExceptFunction(BooleanSequenceExpression.class); - } else if (EfxDataType.Date.class.isAssignableFrom(sequenceType)) { - this.exitExceptFunction(DateSequenceExpression.class); - } else if (EfxDataType.Time.class.isAssignableFrom(sequenceType)) { - this.exitExceptFunction(TimeSequenceExpression.class); - } else if (EfxDataType.Duration.class.isAssignableFrom(sequenceType)) { - this.exitExceptFunction(DurationSequenceExpression.class); - } else { - throw InvalidArgumentException.unsupportedSequenceType(sequenceType.getSimpleName(), - EfxLexer.VOCABULARY.getLiteralName(EfxLexer.ExceptFunction)); - } + public void exitStringExceptFunction(StringExceptFunctionContext ctx) { + exitExceptFunction(StringSequenceExpression.class); } - private void exitExceptFunction( - Class listType) { + @Override + public void exitBooleanExceptFunction(BooleanExceptFunctionContext ctx) { + exitExceptFunction(BooleanSequenceExpression.class); + } + + @Override + public void exitNumericExceptFunction(NumericExceptFunctionContext ctx) { + exitExceptFunction(NumericSequenceExpression.class); + } + + @Override + public void exitDateExceptFunction(DateExceptFunctionContext ctx) { + exitExceptFunction(DateSequenceExpression.class); + } + + @Override + public void exitTimeExceptFunction(TimeExceptFunctionContext ctx) { + exitExceptFunction(TimeSequenceExpression.class); + } + + @Override + public void exitDurationExceptFunction(DurationExceptFunctionContext ctx) { + exitExceptFunction(DurationSequenceExpression.class); + } + + private void exitExceptFunction(Class listType) { final T two = this.stack.pop(listType); final T one = this.stack.pop(listType); this.stack.push(this.script.composeExceptFunction(one, two, listType)); @@ -1784,6 +2016,7 @@ class ExpressionPreprocessor extends EfxBaseListener { final EfxParser parser; final TokenStreamRewriter rewriter; final CallStack stack = new CallStack(); + final ContextStack efxContext; ExpressionPreprocessor(String expression) { this(CharStreams.fromString(expression)); @@ -1794,6 +2027,7 @@ class ExpressionPreprocessor extends EfxBaseListener { this.symbols = EfxExpressionTranslatorV2.this.symbols; this.errorListener = EfxExpressionTranslatorV2.this.errorListener; + this.efxContext = new ContextStack(this.symbols); this.lexer = new EfxLexer(charStream); this.tokens = new CommonTokenStream(lexer); @@ -1815,19 +2049,212 @@ String processExpression() { return this.rewriter.getText(); } + // #region Context tracking ----------------------------------------------- + @Override - public void exitScalarFromFieldReference(ScalarFromFieldReferenceContext ctx) { - if (!hasParentContextOfType(ctx, LateBoundScalarContext.class)) { - return; + public void enterSingleExpression(SingleExpressionContext ctx) { + final TerminalNode fieldContext = ctx.FieldId(); + if (fieldContext != null) { + this.efxContext.pushFieldContext(fieldContext.getText()); + } else { + final TerminalNode nodeContext = ctx.NodeId(); + if (nodeContext != null) { + this.efxContext.pushNodeContext(nodeContext.getText()); + } else { + final TerminalNode alias = ctx.Identifier(); + if (alias != null) { + String fieldId = this.symbols.getFieldIdFromAlias(alias.getText()); + if (fieldId != null) { + this.efxContext.pushFieldContext(fieldId); + } else { + String nodeId = this.symbols.getNodeIdFromAlias(alias.getText()); + if (nodeId != null) { + this.efxContext.pushNodeContext(nodeId); + } else { + throw SymbolResolutionException.unknownSymbol(alias.getText()); + } + } + } + } + } + } + + @Override + public void exitSingleExpression(SingleExpressionContext ctx) { + this.efxContext.pop(); + } + + @Override + public void enterPredicate(EfxParser.PredicateContext ctx) { + var parent = ctx.getParent(); + if (parent instanceof NodeReferenceWithPredicateContext) { + final String nodeId = getNodeId((NodeReferenceWithPredicateContext) parent); + this.efxContext.pushNodeContext(nodeId); + } else if (parent instanceof FieldReferenceWithPredicateContext) { + final String fieldId = getFieldId((FieldReferenceWithPredicateContext) parent); + this.efxContext.pushFieldContext(fieldId); + } else { + throw new ParseCancellationException("Unexpected parent context for predicate: " + parent.getClass().getSimpleName()); + } + } + + @Override + public void exitPredicate(EfxParser.PredicateContext ctx) { + this.efxContext.pop(); + } + + @Override + public void enterAbsoluteFieldReference(AbsoluteFieldReferenceContext ctx) { + if (ctx.Slash() != null) { + this.efxContext.push(null); + } + } + + @Override + public void exitAbsoluteFieldReference(EfxParser.AbsoluteFieldReferenceContext ctx) { + if (ctx.Slash() != null) { + this.efxContext.pop(); + } + } + + @Override + public void enterAbsoluteNodeReference(EfxParser.AbsoluteNodeReferenceContext ctx) { + if (ctx.Slash() != null) { + this.efxContext.push(null); + } + } + + @Override + public void exitAbsoluteNodeReference(AbsoluteNodeReferenceContext ctx) { + if (ctx.Slash() != null) { + this.efxContext.pop(); + } + } + + @Override + public void enterFieldReferenceInOtherNotice(FieldReferenceInOtherNoticeContext ctx) { + if (ctx.noticeReference() != null) { + this.efxContext.push(null); + } + } + + @Override + public void exitFieldReferenceInOtherNotice(EfxParser.FieldReferenceInOtherNoticeContext ctx) { + if (ctx.noticeReference() != null) { + this.efxContext.pop(); + } + } + + @Override + public void exitContextFieldSpecifier(ContextFieldSpecifierContext ctx) { + final String contextFieldId = getFieldId(ctx.fieldContext()); + this.efxContext + .push(new FieldContext(contextFieldId, this.symbols.getAbsolutePathOfField(contextFieldId), + this.symbols.getRelativePathOfField(contextFieldId, this.efxContext.symbol()))); + } + + @Override + public void exitFieldReferenceWithFieldContextOverride( + FieldReferenceWithFieldContextOverrideContext ctx) { + if (ctx.contextFieldSpecifier() != null) { + this.efxContext.pop(); } + } + @Override + public void exitContextNodeSpecifier(ContextNodeSpecifierContext ctx) { + final String contextNodeId = getNodeId(ctx.node); + this.efxContext + .push(new NodeContext(contextNodeId, this.symbols.getAbsolutePathOfNode(contextNodeId), + this.symbols.getRelativePathOfNode(contextNodeId, this.efxContext.symbol()))); + } + + @Override + public void exitFieldReferenceWithNodeContextOverride( + FieldReferenceWithNodeContextOverrideContext ctx) { + if (ctx.contextNodeSpecifier() != null) { + this.efxContext.pop(); + } + } + + @Override + public void exitContextVariableSpecifier(ContextVariableSpecifierContext ctx) { + String variableName = ctx.variableReference().variableName.getText(); + Context variableContext = this.efxContext.getContextFromVariable(variableName); + if (variableContext == null) { + throw InvalidIdentifierException.notAContextVariable(variableName); + } + if (variableContext.isFieldContext()) { + this.efxContext.push(new FieldContext(variableContext.symbol(), + this.symbols.getAbsolutePathOfField(variableContext.symbol()), this.symbols + .getRelativePathOfField(variableContext.symbol(), this.efxContext.symbol()))); + } else if (variableContext.isNodeContext()) { + this.efxContext.push(new NodeContext(variableContext.symbol(), + this.symbols.getAbsolutePathOfNode(variableContext.symbol()), this.symbols + .getRelativePathOfNode(variableContext.symbol(), this.efxContext.symbol()))); + } else { + assert false : "Context variable must be either a field or node context: " + variableName; + } + } + + @Override + public void exitFieldReferenceWithVariableContextOverride( + FieldReferenceWithVariableContextOverrideContext ctx) { + if (ctx.contextVariableSpecifier() != null) { + this.efxContext.pop(); + } + } + + // #endregion Context tracking -------------------------------------------- + + @Override + public void exitScalarFromFieldReference(ScalarFromFieldReferenceContext ctx) { String fieldId = getFieldId(ctx.fieldReference()); String fieldType = eFormsToEfxTypeMap.get(this.symbols.getTypeOfField(fieldId)); + // Skip repeatability check if context IS this field (e.g., inside a predicate on this field). + // In that case, we're referencing the current element being iterated, not the whole sequence. + if (this.efxContext.isEmpty() || !fieldId.equals(this.efxContext.symbol())) { + String contextNodeId = getContextNodeId(); + if (this.symbols.isFieldRepeatableFromContext(fieldId, contextNodeId)) { + // B2 Solution 3: If we're in a top-level expression context where sequences + // are valid (lateBoundScalar → lateBoundExpression → expression), auto-insert + // sequence cast instead of throwing. + if (isTopLevelLateBoundExpression(ctx)) { + this.rewriter.insertBefore(ctx.getStart(), "(" + fieldType + "*)"); + return; + } + throw TypeMismatchException.fieldMayRepeat(fieldId, this.efxContext.symbol()); + } + } + + if (!hasParentContextOfType(ctx, LateBoundScalarContext.class)) { + return; + } + // Insert the type cast this.rewriter.insertBefore(ctx.getStart(), "(" + fieldType + ")"); } + /** + * Gets the node ID of the current context for use with isFieldRepeatableFromContext. + * If the context is a NodeContext, returns the node ID directly. + * If the context is a FieldContext, returns the parent node of that field. + * If the context is empty/null, returns null (root context). + */ + private String getContextNodeId() { + if (this.efxContext.isEmpty() || this.efxContext.peek() == null) { + return null; + } + Context context = this.efxContext.peek(); + if (context.isNodeContext()) { + return context.symbol(); + } else { + // FieldContext - get the parent node of the field + return this.symbols.getParentNodeOfField(context.symbol()); + } + } + @Override public void exitScalarFromAttributeReference(ScalarFromAttributeReferenceContext ctx) { if (!hasParentContextOfType(ctx, LateBoundScalarContext.class)) { @@ -1845,7 +2272,7 @@ public void exitScalarFromFunctionInvocation(ScalarFromFunctionInvocationContext } String functionName = ctx.functionInvocation().functionName.getText(); - String functionType = javaToEfxTypeMap.get(this.stack.getTypeOfIdentifier(functionName)); + String functionType = javaToEfxTypeMap.get(EfxTypeLattice.toPrimitive(this.stack.getTypeOfIdentifier(functionName))); if (functionType != null) { // Insert the type cast @@ -1864,8 +2291,8 @@ public void exitSequenceFromFieldReference(SequenceFromFieldReferenceContext ctx String fieldType = eFormsToEfxTypeMap.get(this.symbols.getTypeOfField(fieldId)); if (fieldType != null) { - // Insert the type cast - this.rewriter.insertBefore(ctx.getStart(), "(" + fieldType + ")"); + // Insert the sequence type cast (type*) + this.rewriter.insertBefore(ctx.getStart(), "(" + fieldType + "*)"); } } @@ -1875,37 +2302,82 @@ public void exitSequenceFromAttributeReference(SequenceFromAttributeReferenceCon return; } - // Insert the type cast - this.rewriter.insertBefore(ctx.getStart(), "(" + textTypeName + ")"); + // Insert the sequence type cast (text*) + this.rewriter.insertBefore(ctx.getStart(), "(" + textTypeName + "*)"); } @Override public void exitSequenceFromFunctionInvocation(SequenceFromFunctionInvocationContext ctx) { - if (!hasParentContextOfType(ctx, LateBoundScalarContext.class)) { + if (!hasParentContextOfType(ctx, LateBoundSequenceContext.class)) { return; } String functionName = ctx.functionInvocation().functionName.getText(); - String functionType = javaToEfxTypeMap.get(this.stack.getTypeOfIdentifier(functionName)); + String functionType = javaToEfxTypeMap.get(EfxTypeLattice.toPrimitive(this.stack.getTypeOfIdentifier(functionName))); if (functionType != null) { - // Insert the type cast - this.rewriter.insertBefore(ctx.functionInvocation().FunctionPrefix().getSymbol(), "(" + functionType + ")"); + // Insert the sequence type cast (type*) + this.rewriter.insertBefore(ctx.functionInvocation().FunctionPrefix().getSymbol(), "(" + functionType + "*)"); } } @Override - public void exitVariableReference(VariableReferenceContext ctx) { + public void exitScalarFromVariableReference(ScalarFromVariableReferenceContext ctx) { + String variableName = ctx.variableReference().variableName.getText(); + Context variableContext = this.efxContext.getContextFromVariable(variableName); + + // Guard: Node context variables cannot be used as values + if (variableContext != null && variableContext.isNodeContext()) { + throw TypeMismatchException.nodesHaveNoValue(variableName, variableContext.symbol()); + } + + // Guard: Field context variables must not be repeatable in scalar context + if (variableContext != null && variableContext.isFieldContext()) { + String fieldId = variableContext.symbol(); + if (this.symbols.isFieldRepeatableFromContext(fieldId, getContextNodeId())) { + throw TypeMismatchException.fieldMayRepeat(fieldId, this.efxContext.symbol()); + } + } + + // Guard: Skip type cast insertion if not in late-bound context (explicit cast already present) if (!hasParentContextOfType(ctx, LateBoundScalarContext.class)) { return; } - String variableName = ctx.variableName.getText(); - String variableType = javaToEfxTypeMap.get(this.stack.getTypeOfIdentifier(variableName)); + // Determine type for cast: field type for context variables, variable type for regular variables + String typeCast = (variableContext != null) + ? eFormsToEfxTypeMap.get(this.symbols.getTypeOfField(variableContext.symbol())) + : javaToEfxTypeMap.get(EfxTypeLattice.toPrimitive(this.stack.getTypeOfIdentifier(variableName))); - if (variableType != null) { - // Insert the type cast - this.rewriter.insertBefore(ctx.VariablePrefix().getSymbol(), "(" + variableType + ")"); + if (typeCast != null) { + this.rewriter.insertBefore(ctx.variableReference().VariablePrefix().getSymbol(), "(" + typeCast + ")"); + } + } + + @Override + public void exitSequenceFromVariableReference(SequenceFromVariableReferenceContext ctx) { + String variableName = ctx.variableReference().variableName.getText(); + Context variableContext = this.efxContext.getContextFromVariable(variableName); + + // Guard: Node context variables cannot be used as values + if (variableContext != null && variableContext.isNodeContext()) { + throw TypeMismatchException.nodesHaveNoValue(variableName, variableContext.symbol()); + } + + // No repeatability check needed - sequences can have multiple values + + // Guard: Skip type cast insertion if not in late-bound context (explicit cast already present) + if (!hasParentContextOfType(ctx, LateBoundSequenceContext.class)) { + return; + } + + // Determine type for cast: field type for context variables, variable type for regular variables + String typeCast = (variableContext != null) + ? eFormsToEfxTypeMap.get(this.symbols.getTypeOfField(variableContext.symbol())) + : javaToEfxTypeMap.get(EfxTypeLattice.toPrimitive(this.stack.getTypeOfIdentifier(variableName))); + + if (typeCast != null) { + this.rewriter.insertBefore(ctx.variableReference().VariablePrefix().getSymbol(), "(" + typeCast + "*)"); } } @@ -1916,7 +2388,7 @@ public void exitDictionaryLookup(DictionaryLookupContext ctx) { } String dictionaryName = ctx.dictionaryName.getText(); - String dictionaryType = javaToEfxTypeMap.get(this.stack.getTypeOfIdentifier(dictionaryName)); + String dictionaryType = javaToEfxTypeMap.get(EfxTypeLattice.toPrimitive(this.stack.getTypeOfIdentifier(dictionaryName))); if (dictionaryType != null) { // Insert the type cast @@ -1935,48 +2407,86 @@ boolean hasParentContextOfType(ParserRuleContext ctx, Class expressionType) { + var expression = this.stack.pop(expressionType); + var variable = new Variable(variableName, + this.script.composeVariableDeclaration(variableName, expression.getClass()), + expression, + this.script.composeVariableReference(variableName, expression.getClass())); + this.stack.push(variable); + } + + @Override + public void exitStringSequenceVariableInitializer( + EfxParser.StringSequenceVariableInitializerContext ctx) { + this.exitSequenceVariableInitializer(ctx.variableName.getText(), StringSequenceExpression.class); + } + + @Override + public void exitBooleanSequenceVariableInitializer( + EfxParser.BooleanSequenceVariableInitializerContext ctx) { + this.exitSequenceVariableInitializer(ctx.variableName.getText(), BooleanSequenceExpression.class); + } + + @Override + public void exitNumericSequenceVariableInitializer( + EfxParser.NumericSequenceVariableInitializerContext ctx) { + this.exitSequenceVariableInitializer(ctx.variableName.getText(), NumericSequenceExpression.class); + } + + @Override + public void exitDateSequenceVariableInitializer( + EfxParser.DateSequenceVariableInitializerContext ctx) { + this.exitSequenceVariableInitializer(ctx.variableName.getText(), DateSequenceExpression.class); + } + + @Override + public void exitTimeSequenceVariableInitializer( + EfxParser.TimeSequenceVariableInitializerContext ctx) { + this.exitSequenceVariableInitializer(ctx.variableName.getText(), TimeSequenceExpression.class); + } + + @Override + public void exitDurationSequenceVariableInitializer( + EfxParser.DurationSequenceVariableInitializerContext ctx) { + this.exitSequenceVariableInitializer(ctx.variableName.getText(), DurationSequenceExpression.class); + } + // #endregion Variable Initializers // #region Schema-level Variables @@ -389,9 +447,7 @@ public void exitContextDeclaration(ContextDeclarationContext ctx) { } private void exitRootContextDeclaration() { - PathExpression contextPath = new NodePathExpression("/*"); - String symbol = "ND-Root"; - this.exitNodeContextDeclaration(symbol, contextPath, null); + this.exitNodeContextDeclaration(this.symbols.getRootNodeId(), this.symbols.getRootPath(), null); } private void exitFieldContextDeclaration(String fieldId, PathExpression contextPath, Variable contextVariable) { @@ -421,11 +477,11 @@ private Variable getContextVariable(ContextVariableInitializerContext ctx, } final String variableName = ctx.variableName.getText(); - final Class variableType = contextPath.getClass(); + final Class variableType = contextPath.asScalar().getClass(); return new Variable(variableName, this.script.composeVariableDeclaration(variableName, variableType), - this.symbols.getRelativePath(contextPath, contextPath), + this.script.contextualizePath(contextPath, contextPath), this.script.composeVariableReference(variableName, variableType)); } @@ -669,22 +725,71 @@ public void exitValidationStage(ValidationStageContext ctx) { @Override public void exitContextDeclaration(ContextDeclarationContext ctx) { + // Handle context shortcuts (., .., /) - these don't have context variable initializers + String shortcut = ctx.shortcut != null ? ctx.shortcut.getText() : "none"; + switch (shortcut) { + case "/": + this.efxContext.push(new NodeContext(this.symbols.getRootNodeId(), this.symbols.getRootPath())); + return; + case ".": + case "..": + // Same/parent context not supported in rules, but still need to push something + return; + default: + break; + } + + // Handle field or node context without variable + if (ctx.fieldContext() != null) { + final String fieldId = getFieldId(ctx.fieldContext()); + this.efxContext.push(new FieldContext(fieldId, this.symbols.getAbsolutePathOfField(fieldId), + this.symbols.getRelativePathOfField(fieldId, this.efxContext.symbol()))); + return; + } + if (ctx.nodeContext() != null) { + final String nodeId = getNodeId(ctx.nodeContext()); + this.efxContext.push(new NodeContext(nodeId, this.symbols.getAbsolutePathOfNode(nodeId), + this.symbols.getRelativePathOfNode(nodeId, this.efxContext.symbol()))); + return; + } + + // Handle context variable initializer final var initializer = ctx.contextVariableInitializer(); if (initializer == null) { - return; // No context variable initializer, nothing to do during pre-processing. + return; } + final String variableName = initializer.variableName.getText(); if (initializer.fieldContext() != null) { final String fieldId = getFieldId(initializer.fieldContext()); var fieldType = FieldTypes.fromString(this.symbols.getTypeOfField(fieldId)); this.stack.declareIdentifier( - new Variable(initializer.variableName.getText(), PathExpression.instantiate("", fieldType), - PathExpression.instantiate("", fieldType), PathExpression.instantiate("", fieldType))); + new Variable(variableName, ScalarPath.empty(fieldType), + ScalarPath.empty(fieldType), ScalarPath.empty(fieldType))); + var context = new FieldContext(fieldId, this.symbols.getAbsolutePathOfField(fieldId), + this.symbols.getRelativePathOfField(fieldId, this.efxContext.symbol())); + this.efxContext.push(context); + this.efxContext.declareContextVariable(variableName, context); } if (initializer.nodeContext() != null) { - this.stack.declareIdentifier(new Variable(initializer.variableName.getText(), NodePathExpression.empty(), - NodePathExpression.empty(), NodePathExpression.empty())); + final String nodeId = getNodeId(initializer.nodeContext()); + this.stack.declareIdentifier(new Variable(variableName, NodePath.empty(), + NodePath.empty(), NodePath.empty())); + var context = new NodeContext(nodeId, this.symbols.getAbsolutePathOfNode(nodeId), + this.symbols.getRelativePathOfNode(nodeId, this.efxContext.symbol())); + this.efxContext.push(context); + this.efxContext.declareContextVariable(variableName, context); } - return; + } + + @Override + public void enterRuleSet(RuleSetContext ctx) { + this.stack.pushStackFrame(); + } + + @Override + public void exitRuleSet(RuleSetContext ctx) { + this.stack.popStackFrame(); + this.efxContext.pop(); } /** @@ -693,10 +798,8 @@ public void exitContextDeclaration(ContextDeclarationContext ctx) { @Override public void exitVariableInitializer( EfxParser.VariableInitializerContext ctx) { - if (!this.stack.empty()) { Variable variable = this.stack.pop(Variable.class); this.stack.declareIdentifier(variable); - } } // #endregion Rules-specific variable declarations @@ -743,6 +846,44 @@ public void exitDurationVariableInitializer(EfxParser.DurationVariableInitialize DurationExpression.empty(), DurationExpression.empty())); } + // Sequence variable initializers for type tracking + + @Override + public void exitStringSequenceVariableInitializer(EfxParser.StringSequenceVariableInitializerContext ctx) { + this.stack.push(new Variable(ctx.variableName.getText(), + new StringSequenceExpression(""), new StringSequenceExpression(""))); + } + + @Override + public void exitBooleanSequenceVariableInitializer(EfxParser.BooleanSequenceVariableInitializerContext ctx) { + this.stack.push(new Variable(ctx.variableName.getText(), + new BooleanSequenceExpression(""), new BooleanSequenceExpression(""))); + } + + @Override + public void exitNumericSequenceVariableInitializer(EfxParser.NumericSequenceVariableInitializerContext ctx) { + this.stack.push(new Variable(ctx.variableName.getText(), + new NumericSequenceExpression(""), new NumericSequenceExpression(""))); + } + + @Override + public void exitDateSequenceVariableInitializer(EfxParser.DateSequenceVariableInitializerContext ctx) { + this.stack.push(new Variable(ctx.variableName.getText(), + new DateSequenceExpression(""), new DateSequenceExpression(""))); + } + + @Override + public void exitTimeSequenceVariableInitializer(EfxParser.TimeSequenceVariableInitializerContext ctx) { + this.stack.push(new Variable(ctx.variableName.getText(), + new TimeSequenceExpression(""), new TimeSequenceExpression(""))); + } + + @Override + public void exitDurationSequenceVariableInitializer(EfxParser.DurationSequenceVariableInitializerContext ctx) { + this.stack.push(new Variable(ctx.variableName.getText(), + new DurationSequenceExpression(""), new DurationSequenceExpression(""))); + } + // #endregion Variable initializers for type tracking } diff --git a/src/main/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2.java b/src/main/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2.java index e6b2850f..6080bb4e 100644 --- a/src/main/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2.java +++ b/src/main/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2.java @@ -1,3 +1,16 @@ +/* + * 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.efx.sdk2; import java.io.IOException; @@ -40,18 +53,23 @@ import eu.europa.ted.efx.model.Context.FieldContext; import eu.europa.ted.efx.model.Context.NodeContext; import eu.europa.ted.efx.model.expressions.Expression; +import eu.europa.ted.efx.model.expressions.PathExpression; import eu.europa.ted.efx.model.expressions.TypedExpression; -import eu.europa.ted.efx.model.expressions.path.NodePathExpression; -import eu.europa.ted.efx.model.expressions.path.PathExpression; -import eu.europa.ted.efx.model.expressions.path.StringPathExpression; import eu.europa.ted.efx.model.expressions.scalar.BooleanExpression; import eu.europa.ted.efx.model.expressions.scalar.DateExpression; import eu.europa.ted.efx.model.expressions.scalar.DurationExpression; +import eu.europa.ted.efx.model.expressions.scalar.NodePath; import eu.europa.ted.efx.model.expressions.scalar.NumericExpression; import eu.europa.ted.efx.model.expressions.scalar.ScalarExpression; +import eu.europa.ted.efx.model.expressions.scalar.ScalarPath; import eu.europa.ted.efx.model.expressions.scalar.StringExpression; +import eu.europa.ted.efx.model.expressions.scalar.StringPath; import eu.europa.ted.efx.model.expressions.scalar.TimeExpression; +import eu.europa.ted.efx.model.expressions.sequence.BooleanSequenceExpression; import eu.europa.ted.efx.model.expressions.sequence.DateSequenceExpression; +import eu.europa.ted.efx.model.expressions.sequence.DurationSequenceExpression; +import eu.europa.ted.efx.model.expressions.sequence.NumericSequenceExpression; +import eu.europa.ted.efx.model.expressions.sequence.SequenceExpression; import eu.europa.ted.efx.model.expressions.sequence.StringSequenceExpression; import eu.europa.ted.efx.model.expressions.sequence.TimeSequenceExpression; import eu.europa.ted.efx.model.templates.Conditional; @@ -76,6 +94,9 @@ import eu.europa.ted.efx.sdk2.EfxParser.AssetTypeContext; import eu.europa.ted.efx.sdk2.EfxParser.BooleanFunctionDeclarationContext; import eu.europa.ted.efx.sdk2.EfxParser.BooleanParameterDeclarationContext; +import eu.europa.ted.efx.sdk2.EfxParser.BooleanSequenceParameterDeclarationContext; +import eu.europa.ted.efx.sdk2.EfxParser.BooleanSequenceFunctionDeclarationContext; +import eu.europa.ted.efx.sdk2.EfxParser.BooleanSequenceVariableInitializerContext; import eu.europa.ted.efx.sdk2.EfxParser.BooleanVariableInitializerContext; import eu.europa.ted.efx.sdk2.EfxParser.ChooseTemplateContext; import eu.europa.ted.efx.sdk2.EfxParser.ComputedLabelReferenceContext; @@ -84,12 +105,18 @@ import eu.europa.ted.efx.sdk2.EfxParser.ContextVariableInitializerContext; import eu.europa.ted.efx.sdk2.EfxParser.DateFunctionDeclarationContext; import eu.europa.ted.efx.sdk2.EfxParser.DateParameterDeclarationContext; +import eu.europa.ted.efx.sdk2.EfxParser.DateSequenceParameterDeclarationContext; +import eu.europa.ted.efx.sdk2.EfxParser.DateSequenceFunctionDeclarationContext; +import eu.europa.ted.efx.sdk2.EfxParser.DateSequenceVariableInitializerContext; import eu.europa.ted.efx.sdk2.EfxParser.DateVariableInitializerContext; import eu.europa.ted.efx.sdk2.EfxParser.DictionaryDeclarationContext; import eu.europa.ted.efx.sdk2.EfxParser.DictionaryIndexClauseContext; import eu.europa.ted.efx.sdk2.EfxParser.DictionaryKeyClauseContext; import eu.europa.ted.efx.sdk2.EfxParser.DurationFunctionDeclarationContext; import eu.europa.ted.efx.sdk2.EfxParser.DurationParameterDeclarationContext; +import eu.europa.ted.efx.sdk2.EfxParser.DurationSequenceParameterDeclarationContext; +import eu.europa.ted.efx.sdk2.EfxParser.DurationSequenceFunctionDeclarationContext; +import eu.europa.ted.efx.sdk2.EfxParser.DurationSequenceVariableInitializerContext; import eu.europa.ted.efx.sdk2.EfxParser.DurationVariableInitializerContext; import eu.europa.ted.efx.sdk2.EfxParser.ExpressionTemplateContext; import eu.europa.ted.efx.sdk2.EfxParser.VariableDeclarationContext; @@ -106,6 +133,9 @@ import eu.europa.ted.efx.sdk2.EfxParser.NavigationSectionContext; import eu.europa.ted.efx.sdk2.EfxParser.NumericFunctionDeclarationContext; import eu.europa.ted.efx.sdk2.EfxParser.NumericParameterDeclarationContext; +import eu.europa.ted.efx.sdk2.EfxParser.NumericSequenceParameterDeclarationContext; +import eu.europa.ted.efx.sdk2.EfxParser.NumericSequenceFunctionDeclarationContext; +import eu.europa.ted.efx.sdk2.EfxParser.NumericSequenceVariableInitializerContext; import eu.europa.ted.efx.sdk2.EfxParser.NumericVariableInitializerContext; import eu.europa.ted.efx.sdk2.EfxParser.SecondaryTemplateContext; import eu.europa.ted.efx.sdk2.EfxParser.ShorthandBtLabelReferenceContext; @@ -118,6 +148,9 @@ import eu.europa.ted.efx.sdk2.EfxParser.StandardLabelReferenceContext; import eu.europa.ted.efx.sdk2.EfxParser.StringFunctionDeclarationContext; import eu.europa.ted.efx.sdk2.EfxParser.StringParameterDeclarationContext; +import eu.europa.ted.efx.sdk2.EfxParser.StringSequenceParameterDeclarationContext; +import eu.europa.ted.efx.sdk2.EfxParser.StringSequenceFunctionDeclarationContext; +import eu.europa.ted.efx.sdk2.EfxParser.StringSequenceVariableInitializerContext; import eu.europa.ted.efx.sdk2.EfxParser.StringVariableInitializerContext; import eu.europa.ted.efx.sdk2.EfxParser.SummarySectionContext; import eu.europa.ted.efx.sdk2.EfxParser.TemplateDefinitionContext; @@ -128,6 +161,9 @@ import eu.europa.ted.efx.sdk2.EfxParser.TextTemplateContext; import eu.europa.ted.efx.sdk2.EfxParser.TimeFunctionDeclarationContext; import eu.europa.ted.efx.sdk2.EfxParser.TimeParameterDeclarationContext; +import eu.europa.ted.efx.sdk2.EfxParser.TimeSequenceParameterDeclarationContext; +import eu.europa.ted.efx.sdk2.EfxParser.TimeSequenceFunctionDeclarationContext; +import eu.europa.ted.efx.sdk2.EfxParser.TimeSequenceVariableInitializerContext; import eu.europa.ted.efx.sdk2.EfxParser.TimeVariableInitializerContext; import eu.europa.ted.efx.sdk2.EfxParser.WhenDisplayTemplateContext; import eu.europa.ted.efx.sdk2.EfxParser.WhenInvokeTemplateContext; @@ -335,7 +371,7 @@ public void enterStringFunctionDeclaration(StringFunctionDeclarationContext ctx) @Override public void exitStringFunctionDeclaration(StringFunctionDeclarationContext ctx) { - this.exitFunctionDeclaration(ctx.functionName.getText(), EfxDataType.String.class); + this.exitFunctionDeclaration(ctx.functionName.getText()); } @Override @@ -345,7 +381,7 @@ public void enterBooleanFunctionDeclaration(BooleanFunctionDeclarationContext ct @Override public void exitBooleanFunctionDeclaration(BooleanFunctionDeclarationContext ctx) { - this.exitFunctionDeclaration(ctx.functionName.getText(), EfxDataType.Boolean.class); + this.exitFunctionDeclaration(ctx.functionName.getText()); } @Override @@ -355,7 +391,7 @@ public void enterNumericFunctionDeclaration(NumericFunctionDeclarationContext ct @Override public void exitNumericFunctionDeclaration(NumericFunctionDeclarationContext ctx) { - this.exitFunctionDeclaration(ctx.functionName.getText(), EfxDataType.Number.class); + this.exitFunctionDeclaration(ctx.functionName.getText()); } @Override @@ -365,7 +401,7 @@ public void enterDateFunctionDeclaration(DateFunctionDeclarationContext ctx) { @Override public void exitDateFunctionDeclaration(DateFunctionDeclarationContext ctx) { - this.exitFunctionDeclaration(ctx.functionName.getText(), EfxDataType.Date.class); + this.exitFunctionDeclaration(ctx.functionName.getText()); } @Override @@ -375,7 +411,7 @@ public void enterTimeFunctionDeclaration(TimeFunctionDeclarationContext ctx) { @Override public void exitTimeFunctionDeclaration(TimeFunctionDeclarationContext ctx) { - this.exitFunctionDeclaration(ctx.functionName.getText(), EfxDataType.Time.class); + this.exitFunctionDeclaration(ctx.functionName.getText()); } @Override @@ -385,14 +421,76 @@ public void enterDurationFunctionDeclaration(DurationFunctionDeclarationContext @Override public void exitDurationFunctionDeclaration(DurationFunctionDeclarationContext ctx) { - this.exitFunctionDeclaration(ctx.functionName.getText(), EfxDataType.Duration.class); + this.exitFunctionDeclaration(ctx.functionName.getText()); + } + + // Sequence function declarations + + @Override + public void enterStringSequenceFunctionDeclaration(StringSequenceFunctionDeclarationContext ctx) { + this.stack.push(new ParsedParameters()); + } + + @Override + public void exitStringSequenceFunctionDeclaration(StringSequenceFunctionDeclarationContext ctx) { + this.exitFunctionDeclaration(ctx.functionName.getText()); + } + + @Override + public void enterNumericSequenceFunctionDeclaration(NumericSequenceFunctionDeclarationContext ctx) { + this.stack.push(new ParsedParameters()); + } + + @Override + public void exitNumericSequenceFunctionDeclaration(NumericSequenceFunctionDeclarationContext ctx) { + this.exitFunctionDeclaration(ctx.functionName.getText()); + } + + @Override + public void enterBooleanSequenceFunctionDeclaration(BooleanSequenceFunctionDeclarationContext ctx) { + this.stack.push(new ParsedParameters()); } - private void exitFunctionDeclaration(String functionName, Class returnType) { + @Override + public void exitBooleanSequenceFunctionDeclaration(BooleanSequenceFunctionDeclarationContext ctx) { + this.exitFunctionDeclaration(ctx.functionName.getText()); + } + + @Override + public void enterDateSequenceFunctionDeclaration(DateSequenceFunctionDeclarationContext ctx) { + this.stack.push(new ParsedParameters()); + } + + @Override + public void exitDateSequenceFunctionDeclaration(DateSequenceFunctionDeclarationContext ctx) { + this.exitFunctionDeclaration(ctx.functionName.getText()); + } + + @Override + public void enterTimeSequenceFunctionDeclaration(TimeSequenceFunctionDeclarationContext ctx) { + this.stack.push(new ParsedParameters()); + } + + @Override + public void exitTimeSequenceFunctionDeclaration(TimeSequenceFunctionDeclarationContext ctx) { + this.exitFunctionDeclaration(ctx.functionName.getText()); + } + + @Override + public void enterDurationSequenceFunctionDeclaration(DurationSequenceFunctionDeclarationContext ctx) { + this.stack.push(new ParsedParameters()); + } + + @Override + public void exitDurationSequenceFunctionDeclaration(DurationSequenceFunctionDeclarationContext ctx) { + this.exitFunctionDeclaration(ctx.functionName.getText()); + } + + private void exitFunctionDeclaration(String functionName) { var expression = this.stack.pop(TypedExpression.class); var parameters = this.stack.pop(ParsedParameters.class); - this.stack.declareFunction(new Function(functionName, returnType, parameters, expression)); + this.stack.declareFunction(new Function(functionName, parameters, expression)); } @Override @@ -430,10 +528,10 @@ public void exitTemplateFile(TemplateFileContext ctx) { for (Identifier identifier : this.stack.getGlobals()) { if (identifier instanceof Variable) { Variable variable = (Variable) identifier; - globals.add(this.markup.renderVariableDeclaration(variable.dataType, variable.name, variable.initializationExpression)); + globals.add(this.markup.renderVariableDeclaration(variable.name, variable.initializationExpression)); } else if (identifier instanceof Function) { Function function = (Function) identifier; - globals.add(this.markup.renderFunctionDeclaration(function.dataType, function.name, function.parameters.toMap(), function.expression)); + globals.add(this.markup.renderFunctionDeclaration(function.name, function.parameters.toMap(), function.expression)); } else if (identifier instanceof Dictionary) { Dictionary dictionary = (Dictionary) identifier; globals.add(this.markup.renderDictionaryDeclaration(dictionary.name, dictionary.pathExpression, dictionary.keyExpression)); @@ -643,11 +741,11 @@ private void shorthandIndirectLabelReference(final String fieldId, final Numeric final String fieldType = this.symbols.getTypeOfField(fieldId); final PathExpression valueReference = this.symbols.isAttributeField(fieldId) ? this.script.composeFieldAttributeReference( - this.symbols.getRelativePath(this.symbols.getAbsolutePathOfFieldWithoutTheAttribute(fieldId), + this.script.contextualizePath(this.symbols.getAbsolutePathOfFieldWithoutTheAttribute(fieldId), currentContext.absolutePath()), - this.symbols.getAttributeNameFromAttributeField(fieldId), StringPathExpression.class) + this.symbols.getAttributeNameFromAttributeField(fieldId), StringPath.class) : this.script.composeFieldValueReference( - this.symbols.getRelativePathOfField(fieldId, currentContext.absolutePath())); + this.symbols.getRelativePathOfField(fieldId, currentContext.symbol())); Variable loopVariable = new Variable("item", this.script.composeVariableDeclaration("item", StringExpression.class), StringExpression.empty(), this.script.composeVariableReference("item", StringExpression.class)); @@ -658,7 +756,7 @@ private void shorthandIndirectLabelReference(final String fieldId, final Numeric this.script.composeForExpression( this.script.composeIteratorList( List.of( - this.script.composeIteratorExpression(loopVariable.declarationExpression, valueReference))), + this.script.composeIteratorExpression(loopVariable.declarationExpression, valueReference.asSequence()))), this.script.composeStringConcatenation( List.of(this.script.getStringLiteralFromUnquotedString(ASSET_TYPE_INDICATOR), this.script.getStringLiteralFromUnquotedString("|"), @@ -677,7 +775,7 @@ private void shorthandIndirectLabelReference(final String fieldId, final Numeric this.script.composeForExpression( this.script.composeIteratorList( List.of( - this.script.composeIteratorExpression(loopVariable.declarationExpression, valueReference))), + this.script.composeIteratorExpression(loopVariable.declarationExpression, valueReference.asSequence()))), this.script.composeStringConcatenation(List.of( this.script.getStringLiteralFromUnquotedString(ASSET_TYPE_CODE), this.script.getStringLiteralFromUnquotedString("|"), @@ -859,7 +957,7 @@ public void exitShorthandFieldValueReferenceFromContextField( throw InvalidUsageException.shorthandRequiresFieldContext("$value"); } this.stack.push(this.script.composeFieldValueReference( - this.symbols.getRelativePathOfField(this.efxContext.symbol(), this.efxContext.absolutePath()))); + this.symbols.getRelativePathOfField(this.efxContext.symbol(), this.efxContext.symbol()))); } // #endregion Expression Blocks ${...} -------------------------------------- @@ -910,6 +1008,12 @@ public void exitContextDeclaration(ContextDeclarationContext ctx) { private void exitSameContextDeclaration() { Context currentContext = this.blockStack.currentContext(); + + // Verify that efxContext.peek() gives the same result as blockStack.currentContext() + assert this.efxContext.isEmpty() || currentContext.symbol().equals(this.efxContext.peek().symbol()) + : "Context mismatch: blockStack.currentContext()=" + currentContext.symbol() + + " but efxContext.peek()=" + this.efxContext.peek().symbol(); + PathExpression contextPath = currentContext.absolutePath(); String symbol = currentContext.symbol(); if (currentContext.isFieldContext()) { @@ -921,6 +1025,13 @@ private void exitSameContextDeclaration() { private void exitParentContextDeclaration() { Context parentContext = this.blockStack.parentContext(); + + // Verify that efxContext.peekParent() gives the same result as blockStack.parentContext() + Context efxParentContext = this.efxContext.peekParentContext(); + assert efxParentContext == null || parentContext.symbol().equals(efxParentContext.symbol()) + : "Context mismatch: blockStack.parentContext()=" + parentContext.symbol() + + " but efxContext.peekParent()=" + efxParentContext.symbol(); + PathExpression contextPath = parentContext.absolutePath(); String symbol = parentContext.symbol(); if (parentContext.isFieldContext()) { @@ -931,9 +1042,7 @@ private void exitParentContextDeclaration() { } private void exitRootContextDeclaration() { - PathExpression contextPath = new NodePathExpression("/*"); - String symbol = "ND-Root"; - this.exitNodeContextDeclaration(symbol, contextPath, null); + this.exitNodeContextDeclaration(this.symbols.getRootNodeId(), this.symbols.getRootPath(), null); } private void exitFieldContextDeclaration(String fieldId, PathExpression contextPath, Variable contextVariable) { @@ -961,11 +1070,11 @@ private Variable getContextVariable(ContextVariableInitializerContext ctx, } final String variableName = ctx.variableName.getText(); - final Class variableType = contextPath.getClass(); + final Class variableType = contextPath.asScalar().getClass(); return new Variable(variableName, this.script.composeVariableDeclaration(variableName, variableType), - this.symbols.getRelativePath(contextPath, contextPath), + this.script.contextualizePath(contextPath, contextPath), this.script.composeVariableReference(variableName, variableType)); } @@ -1059,6 +1168,48 @@ public void exitVariableInitializer(VariableInitializerContext arg0) { this.stack.peek(Variables.class).add(variable); } + // Sequence variable initializers + + @Override + public void exitStringSequenceVariableInitializer(StringSequenceVariableInitializerContext ctx) { + this.exitSequenceVariableInitializer(ctx.variableName.getText(), StringSequenceExpression.class); + } + + @Override + public void exitNumericSequenceVariableInitializer(NumericSequenceVariableInitializerContext ctx) { + this.exitSequenceVariableInitializer(ctx.variableName.getText(), NumericSequenceExpression.class); + } + + @Override + public void exitBooleanSequenceVariableInitializer(BooleanSequenceVariableInitializerContext ctx) { + this.exitSequenceVariableInitializer(ctx.variableName.getText(), BooleanSequenceExpression.class); + } + + @Override + public void exitDateSequenceVariableInitializer(DateSequenceVariableInitializerContext ctx) { + this.exitSequenceVariableInitializer(ctx.variableName.getText(), DateSequenceExpression.class); + } + + @Override + public void exitTimeSequenceVariableInitializer(TimeSequenceVariableInitializerContext ctx) { + this.exitSequenceVariableInitializer(ctx.variableName.getText(), TimeSequenceExpression.class); + } + + @Override + public void exitDurationSequenceVariableInitializer(DurationSequenceVariableInitializerContext ctx) { + this.exitSequenceVariableInitializer(ctx.variableName.getText(), DurationSequenceExpression.class); + } + + private void exitSequenceVariableInitializer( + String variableName, Class expressionType) { + var expression = this.stack.pop(expressionType); + var variable = new Variable(variableName, + this.script.composeVariableDeclaration(variableName, expression.getClass()), + expression, + this.script.composeVariableReference(variableName, expression.getClass())); + this.stack.push(variable); + } + // #endregion Variable Initializers ----------------------------------------- // #region Hyperlinks ------------------------------------------------------- @@ -1130,6 +1281,38 @@ public void exitDurationParameterDeclaration(DurationParameterDeclarationContext this.exitParameterDeclaration(ctx.parameterName.getText(), DurationExpression.class); } + // Sequence parameter declarations + + @Override + public void exitStringSequenceParameterDeclaration(StringSequenceParameterDeclarationContext ctx) { + this.exitParameterDeclaration(ctx.parameterName.getText(), StringSequenceExpression.class); + } + + @Override + public void exitNumericSequenceParameterDeclaration(NumericSequenceParameterDeclarationContext ctx) { + this.exitParameterDeclaration(ctx.parameterName.getText(), NumericSequenceExpression.class); + } + + @Override + public void exitBooleanSequenceParameterDeclaration(BooleanSequenceParameterDeclarationContext ctx) { + this.exitParameterDeclaration(ctx.parameterName.getText(), BooleanSequenceExpression.class); + } + + @Override + public void exitDateSequenceParameterDeclaration(DateSequenceParameterDeclarationContext ctx) { + this.exitParameterDeclaration(ctx.parameterName.getText(), DateSequenceExpression.class); + } + + @Override + public void exitTimeSequenceParameterDeclaration(TimeSequenceParameterDeclarationContext ctx) { + this.exitParameterDeclaration(ctx.parameterName.getText(), TimeSequenceExpression.class); + } + + @Override + public void exitDurationSequenceParameterDeclaration(DurationSequenceParameterDeclarationContext ctx) { + this.exitParameterDeclaration(ctx.parameterName.getText(), DurationSequenceExpression.class); + } + private void exitParameterDeclaration(String parameterName, Class parameterType) { ParsedParameter parameter = new ParsedParameter(parameterName, this.script.composeParameterReference(parameterName, parameterType)); @@ -1275,13 +1458,13 @@ private Context relativizeContext(Context childContext, Context parentContext) { if (childContext.isFieldContext()) { return new FieldContext(childContext.symbol(), childContext.absolutePath(), - this.symbols.getRelativePath(childContext.absolutePath(), parentContextAbsolutePath), childContext.variable()); + this.script.contextualizePath(childContext.absolutePath(), parentContextAbsolutePath), childContext.variable()); } assert childContext.isNodeContext() : "Child context should be either a FieldContext or a NodeContext."; return new NodeContext(childContext.symbol(), childContext.absolutePath(), - this.symbols.getRelativePath(childContext.absolutePath(), parentContextAbsolutePath)); + this.script.contextualizePath(childContext.absolutePath(), parentContextAbsolutePath)); } // #endregion Template lines ----------------------------------------------- @@ -1359,22 +1542,110 @@ public void exitVariableInitializer(VariableInitializerContext arg0) { @Override public void exitContextDeclaration(ContextDeclarationContext ctx) { + // Handle context shortcuts (., .., /) + String shortcut = ctx.shortcut != null ? ctx.shortcut.getText() : "none"; + switch (shortcut) { + case ".": + this.exitSameContextDeclaration(); + return; + case "..": + this.exitParentContextDeclaration(); + return; + case "/": + this.exitRootContextDeclaration(); + return; + default: + break; + } + + // Handle field or node context without variable + if (ctx.fieldContext() != null) { + final String fieldId = getFieldId(ctx.fieldContext()); + this.efxContext.push(new FieldContext(fieldId, this.symbols.getAbsolutePathOfField(fieldId), + this.symbols.getRelativePathOfField(fieldId, this.efxContext.symbol()))); + return; + } + if (ctx.nodeContext() != null) { + final String nodeId = getNodeId(ctx.nodeContext()); + this.efxContext.push(new NodeContext(nodeId, this.symbols.getAbsolutePathOfNode(nodeId), + this.symbols.getRelativePathOfNode(nodeId, this.efxContext.symbol()))); + return; + } + + // Handle context variable initializer final var initializer = ctx.contextVariableInitializer(); if (initializer == null) { - return; // No context variable initializer, nothing to do during pre-processing. + return; } + final String variableName = initializer.variableName.getText(); if (initializer.fieldContext() != null) { final String fieldId = getFieldId(initializer.fieldContext()); var fieldType = FieldTypes.fromString(this.symbols.getTypeOfField(fieldId)); this.stack.declareIdentifier( - new Variable(initializer.variableName.getText(), PathExpression.instantiate("", fieldType), - PathExpression.instantiate("", fieldType), PathExpression.instantiate("", fieldType))); + new Variable(variableName, ScalarPath.empty(fieldType), + ScalarPath.empty(fieldType), ScalarPath.empty(fieldType))); + var context = new FieldContext(fieldId, this.symbols.getAbsolutePathOfField(fieldId), + this.symbols.getRelativePathOfField(fieldId, this.efxContext.symbol())); + this.efxContext.push(context); + this.efxContext.declareContextVariable(variableName, context); } if (initializer.nodeContext() != null) { - this.stack.declareIdentifier(new Variable(initializer.variableName.getText(), NodePathExpression.empty(), - NodePathExpression.empty(), NodePathExpression.empty())); + final String nodeId = getNodeId(initializer.nodeContext()); + this.stack.declareIdentifier(new Variable(variableName, NodePath.empty(), + NodePath.empty(), NodePath.empty())); + var context = new NodeContext(nodeId, this.symbols.getAbsolutePathOfNode(nodeId), + this.symbols.getRelativePathOfNode(nodeId, this.efxContext.symbol())); + this.efxContext.push(context); + this.efxContext.declareContextVariable(variableName, context); + } + } + + private void exitSameContextDeclaration() { + // "." means same context as parent - reuse the current top of efxContext + // This mirrors the main translator's blockStack.currentContext() behavior + if (this.efxContext.isEmpty()) { + this.exitRootContextDeclaration(); + return; + } + Context currentContext = this.efxContext.peek(); + PathExpression contextPath = currentContext.absolutePath(); + String symbol = currentContext.symbol(); + if (currentContext.isFieldContext()) { + this.efxContext.push(new FieldContext(symbol, contextPath)); + } else { + Variable variable = currentContext.variable(); + if (variable != null) { + this.efxContext.push(new NodeContext(symbol, contextPath, variable)); + } else { + this.efxContext.push(new NodeContext(symbol, contextPath)); + } + } + } + + private void exitParentContextDeclaration() { + // ".." means parent context - go one level up from current context + // This mirrors the main translator's blockStack.parentContext() behavior + Context parentContext = this.efxContext.peekParentContext(); + if (parentContext == null) { + this.exitRootContextDeclaration(); + return; + } + PathExpression contextPath = parentContext.absolutePath(); + String symbol = parentContext.symbol(); + if (parentContext.isFieldContext()) { + this.efxContext.push(new FieldContext(symbol, contextPath)); + } else { + Variable variable = parentContext.variable(); + if (variable != null) { + this.efxContext.push(new NodeContext(symbol, contextPath, variable)); + } else { + this.efxContext.push(new NodeContext(symbol, contextPath)); + } } - return; + } + + private void exitRootContextDeclaration() { + this.efxContext.push(new NodeContext(this.symbols.getRootNodeId(), this.symbols.getRootPath())); } @Override @@ -1406,7 +1677,39 @@ public void exitTimeVariableInitializer(TimeVariableInitializerContext ctx) { public void exitDurationVariableInitializer(DurationVariableInitializerContext ctx) { this.stack.push(new Variable(ctx.variableName.getText(), DurationExpression.empty(), DurationExpression.empty())); } - + + // Sequence variable initializers + + @Override + public void exitStringSequenceVariableInitializer(StringSequenceVariableInitializerContext ctx) { + this.stack.push(new Variable(ctx.variableName.getText(), new StringSequenceExpression(""), new StringSequenceExpression(""))); + } + + @Override + public void exitNumericSequenceVariableInitializer(NumericSequenceVariableInitializerContext ctx) { + this.stack.push(new Variable(ctx.variableName.getText(), new NumericSequenceExpression(""), new NumericSequenceExpression(""))); + } + + @Override + public void exitBooleanSequenceVariableInitializer(BooleanSequenceVariableInitializerContext ctx) { + this.stack.push(new Variable(ctx.variableName.getText(), new BooleanSequenceExpression(""), new BooleanSequenceExpression(""))); + } + + @Override + public void exitDateSequenceVariableInitializer(DateSequenceVariableInitializerContext ctx) { + this.stack.push(new Variable(ctx.variableName.getText(), new DateSequenceExpression(""), new DateSequenceExpression(""))); + } + + @Override + public void exitTimeSequenceVariableInitializer(TimeSequenceVariableInitializerContext ctx) { + this.stack.push(new Variable(ctx.variableName.getText(), new TimeSequenceExpression(""), new TimeSequenceExpression(""))); + } + + @Override + public void exitDurationSequenceVariableInitializer(DurationSequenceVariableInitializerContext ctx) { + this.stack.push(new Variable(ctx.variableName.getText(), new DurationSequenceExpression(""), new DurationSequenceExpression(""))); + } + // #endregion Template Variables ------------------------------------------ // #region Scope management -------------------------------------------- @@ -1417,6 +1720,10 @@ public void exitDurationVariableInitializer(DurationVariableInitializerContext c public void enterTemplateLine(TemplateLineContext ctx) { final int indentLevel = EfxTemplateTranslatorV2.this.getIndentLevel(ctx.indentation()); this.enterTemplateLine(indentLevel); + // Push root context if no context declaration block (same as main translator) + if (ctx.contextDeclarationBlock() == null) { + this.exitRootContextDeclaration(); + } } @Override @@ -1449,17 +1756,23 @@ private void enterTemplateLine(final int indentLevel) { } } - @Override - public void exitTemplateDeclaration(TemplateDeclarationContext ctx) { - this.exitTemplateLine(0); - } - @Override public void exitTemplateLine(TemplateLineContext ctx) { + // Pop context for this line (same as main translator) + if (!this.efxContext.isEmpty()) { + this.efxContext.pop(); + } final int indentLevel = EfxTemplateTranslatorV2.this.getIndentLevel(ctx.indentation()); this.exitTemplateLine(indentLevel); } + @Override + public void exitTemplateDeclaration(TemplateDeclarationContext ctx) { + // Note: Template declarations don't push context (they have parameters instead of context block) + // So we don't pop context here, unlike regular template lines + this.exitTemplateLine(0); + } + private void exitTemplateLine(final int indentLevel) { final int indentChange = indentLevel - (this.levels.isEmpty() ? 0 : this.levels.peek()); assert this.stack.empty() : "Stack should be empty at this point."; @@ -1479,6 +1792,22 @@ private void exitTemplateLine(final int indentLevel) { // #endregion Scope management -------------------------------------------- + // #region Dictionary context handling ------------------------------------ + + @Override + public void exitDictionaryIndexClause(DictionaryIndexClauseContext ctx) { + // Push field context for dictionary index - same as main translator + this.efxContext.pushFieldContext(getFieldId(ctx.fieldContext())); + } + + @Override + public void exitDictionaryKeyClause(DictionaryKeyClauseContext ctx) { + // Pop the dictionary index context - same as main translator + this.efxContext.pop(); + } + + // #endregion Dictionary context handling --------------------------------- + @Override public void exitDictionaryDeclaration(DictionaryDeclarationContext ctx) { var dictionaryName = ctx.dictionaryName.getText(); diff --git a/src/main/java/eu/europa/ted/efx/sdk2/TypeCheckerV2.java b/src/main/java/eu/europa/ted/efx/sdk2/TypeCheckerV2.java new file mode 100644 index 00000000..9d2a8c60 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/sdk2/TypeCheckerV2.java @@ -0,0 +1,72 @@ +/* + * Copyright 2026 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.efx.sdk2; + +import eu.europa.ted.efx.interfaces.TypeChecker; +import eu.europa.ted.efx.model.expressions.TypedExpression; +import eu.europa.ted.efx.model.expressions.scalar.ScalarExpression; +import eu.europa.ted.efx.model.expressions.sequence.SequenceExpression; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; +import eu.europa.ted.efx.model.types.EfxTypeLattice; + +/** + * V2 strict type checker that requires concrete types. + * Rejects abstract Sequence.class and Scalar.class targets. + */ +public class TypeCheckerV2 implements TypeChecker { + + public static final TypeCheckerV2 INSTANCE = new TypeCheckerV2(); + + private TypeCheckerV2() {} + + @Override + public boolean canConvert(Class from, + Class to) { + // Direct class compatibility + if (to.isAssignableFrom(from)) { + return true; + } + + var fromAnnotation = from.getAnnotation(EfxDataTypeAssociation.class); + var toAnnotation = to.getAnnotation(EfxDataTypeAssociation.class); + assert fromAnnotation != null : "Missing @EfxDataTypeAssociation on " + from.getName(); + assert toAnnotation != null : "Missing @EfxDataTypeAssociation on " + to.getName(); + + var fromType = fromAnnotation.dataType(); + var toType = toAnnotation.dataType(); + + // V2 requires concrete types - reject abstract Sequence/Scalar + assert to != SequenceExpression.class : "Cannot convert to untyped Sequence - use a typed sequence"; + assert to != ScalarExpression.class : "Cannot convert to untyped Scalar - use a typed scalar"; + + // Direct type compatibility + if (toType.isAssignableFrom(fromType)) { + return true; + } + + // Primitive types must be compatible (from can be a subtype of to) + var fromPrimitive = EfxTypeLattice.toPrimitive(fromType); + var toPrimitive = EfxTypeLattice.toPrimitive(toType); + if (!toPrimitive.isAssignableFrom(fromPrimitive)) { + return false; + } + + // Scalar can promote to Sequence + if (EfxTypeLattice.isScalar(fromType) && EfxTypeLattice.isSequence(toType)) { + return true; + } + + return false; + } +} diff --git a/src/main/java/eu/europa/ted/efx/sdk2/entity/SdkCodelistV2.java b/src/main/java/eu/europa/ted/efx/sdk2/entity/SdkCodelistV2.java deleted file mode 100644 index 1794738d..00000000 --- a/src/main/java/eu/europa/ted/efx/sdk2/entity/SdkCodelistV2.java +++ /dev/null @@ -1,21 +0,0 @@ -package eu.europa.ted.efx.sdk2.entity; - -import java.util.List; -import java.util.Optional; -import eu.europa.ted.eforms.sdk.component.SdkComponent; -import eu.europa.ted.eforms.sdk.component.SdkComponentType; -import eu.europa.ted.efx.sdk1.entity.SdkCodelistV1; - -/** - * Representation of an SdkCodelist for usage in the symbols map. - * - * @author rouschr - */ -@SdkComponent(versions = {"2"}, componentType = SdkComponentType.CODELIST) -public class SdkCodelistV2 extends SdkCodelistV1 { - - public SdkCodelistV2(final String codelistId, final String codelistVersion, - final List codes, final Optional parentId) { - super(codelistId, codelistVersion, codes, parentId); - } -} diff --git a/src/main/java/eu/europa/ted/efx/sdk2/entity/SdkFieldV2.java b/src/main/java/eu/europa/ted/efx/sdk2/entity/SdkFieldV2.java deleted file mode 100644 index 92e92174..00000000 --- a/src/main/java/eu/europa/ted/efx/sdk2/entity/SdkFieldV2.java +++ /dev/null @@ -1,41 +0,0 @@ -package eu.europa.ted.efx.sdk2.entity; - -import java.util.Map; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.JsonNode; -import eu.europa.ted.eforms.sdk.component.SdkComponent; -import eu.europa.ted.eforms.sdk.component.SdkComponentType; -import eu.europa.ted.efx.sdk1.entity.SdkFieldV1; - -@SdkComponent(versions = {"2"}, componentType = SdkComponentType.FIELD) -public class SdkFieldV2 extends SdkFieldV1 { - private final String alias; - - public SdkFieldV2(String id, String type, String parentNodeId, String xpathAbsolute, - String xpathRelative, String rootCodelistId, String alias) { - super(id, type, parentNodeId, xpathAbsolute, xpathRelative, rootCodelistId); - this.alias = alias; - } - - public SdkFieldV2(JsonNode fieldNode) { - super(fieldNode); - this.alias = fieldNode.has("alias") ? fieldNode.get("alias").asText(null) : null; - } - - @JsonCreator - public SdkFieldV2( - @JsonProperty("id") final String id, - @JsonProperty("type") final String type, - @JsonProperty("parentNodeId") final String parentNodeId, - @JsonProperty("xpathAbsolute") final String xpathAbsolute, - @JsonProperty("xpathRelative") final String xpathRelative, - @JsonProperty("codeList") final Map> codelist, - @JsonProperty("alias") final String alias) { - this(id, type, parentNodeId, xpathAbsolute, xpathRelative, getCodelistId(codelist), alias); - } - - public String getAlias() { - return alias; - } -} diff --git a/src/main/java/eu/europa/ted/efx/sdk2/entity/SdkNodeV2.java b/src/main/java/eu/europa/ted/efx/sdk2/entity/SdkNodeV2.java deleted file mode 100644 index 9865a639..00000000 --- a/src/main/java/eu/europa/ted/efx/sdk2/entity/SdkNodeV2.java +++ /dev/null @@ -1,29 +0,0 @@ -package eu.europa.ted.efx.sdk2.entity; - -import com.fasterxml.jackson.databind.JsonNode; -import eu.europa.ted.eforms.sdk.component.SdkComponent; -import eu.europa.ted.eforms.sdk.component.SdkComponentType; -import eu.europa.ted.efx.sdk1.entity.SdkNodeV1; - -/** - * A node is something like a section. Nodes can be parents of other nodes or parents of fields. - */ -@SdkComponent(versions = {"2"}, componentType = SdkComponentType.NODE) -public class SdkNodeV2 extends SdkNodeV1 { - private final String alias; - - public SdkNodeV2(String id, String parentId, String xpathAbsolute, String xpathRelative, - boolean repeatable, String alias) { - super(id, parentId, xpathAbsolute, xpathRelative, repeatable); - this.alias = alias; - } - - public SdkNodeV2(JsonNode node) { - super(node); - this.alias = node.has("alias") ? node.get("alias").asText(null) : null; - } - - public String getAlias() { - return alias; - } -} diff --git a/src/main/java/eu/europa/ted/efx/sdk2/entity/SdkNoticeSubtypeV2.java b/src/main/java/eu/europa/ted/efx/sdk2/entity/SdkNoticeSubtypeV2.java deleted file mode 100644 index 337d543d..00000000 --- a/src/main/java/eu/europa/ted/efx/sdk2/entity/SdkNoticeSubtypeV2.java +++ /dev/null @@ -1,21 +0,0 @@ -package eu.europa.ted.efx.sdk2.entity; - -import com.fasterxml.jackson.databind.JsonNode; -import eu.europa.ted.eforms.sdk.component.SdkComponent; -import eu.europa.ted.eforms.sdk.component.SdkComponentType; -import eu.europa.ted.efx.sdk1.entity.SdkNoticeSubtypeV1; - -/** - * Represents a notice subtype from the SDK's notice-types.json file. - */ -@SdkComponent(versions = {"2"}, componentType = SdkComponentType.NOTICE_TYPE) -public class SdkNoticeSubtypeV2 extends SdkNoticeSubtypeV1 { - - public SdkNoticeSubtypeV2(String subTypeId, String documentType, String type) { - super(subTypeId, documentType, type); - } - - public SdkNoticeSubtypeV2(JsonNode json) { - super(json); - } -} diff --git a/src/main/java/eu/europa/ted/efx/xpath/XPathContextualizer.java b/src/main/java/eu/europa/ted/efx/xpath/XPathContextualizer.java index 9f95c1c8..cfd0eb5e 100644 --- a/src/main/java/eu/europa/ted/efx/xpath/XPathContextualizer.java +++ b/src/main/java/eu/europa/ted/efx/xpath/XPathContextualizer.java @@ -1,13 +1,27 @@ +/* + * 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.efx.xpath; import eu.europa.ted.eforms.xpath.XPathProcessor; -import eu.europa.ted.efx.model.expressions.path.PathExpression; +import eu.europa.ted.efx.model.expressions.Expression; +import eu.europa.ted.efx.model.expressions.PathExpression; public class XPathContextualizer { /** * Makes the given xpath relative to the given context xpath. - * + * * @param contextXpath the context xpath * @param xpath the xpath to contextualize * @return the contextualized xpath @@ -22,13 +36,13 @@ public static PathExpression contextualize(final PathExpression contextXpath, String result = XPathProcessor.contextualize(contextXpath.getScript(), xpath.getScript()); - return PathExpression.instantiate(result, xpath.getDataType()); + return Expression.instantiate(result, xpath.getClass()); } public static PathExpression join(final PathExpression first, final PathExpression second) { String joinedXPath = XPathProcessor.join(first.getScript(), second.getScript()); - return PathExpression.instantiate(joinedXPath, second.getDataType()); + return Expression.instantiate(joinedXPath, second.getClass()); } } diff --git a/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java b/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java index 8cb20d7c..3bd4a92f 100644 --- a/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java +++ b/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java @@ -1,3 +1,16 @@ +/* + * 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.efx.xpath; import static java.util.Map.entry; @@ -18,18 +31,25 @@ import eu.europa.ted.efx.interfaces.ScriptGenerator; import eu.europa.ted.efx.interfaces.TranslatorOptions; import eu.europa.ted.efx.model.expressions.Expression; +import eu.europa.ted.efx.model.expressions.PathExpression; import eu.europa.ted.efx.model.expressions.TypedExpression; import eu.europa.ted.efx.model.expressions.iteration.IteratorExpression; import eu.europa.ted.efx.model.expressions.iteration.IteratorListExpression; -import eu.europa.ted.efx.model.expressions.path.NodePathExpression; -import eu.europa.ted.efx.model.expressions.path.PathExpression; +import eu.europa.ted.efx.model.expressions.LiteralExpression; import eu.europa.ted.efx.model.expressions.scalar.BooleanExpression; +import eu.europa.ted.efx.model.expressions.scalar.BooleanLiteral; import eu.europa.ted.efx.model.expressions.scalar.DateExpression; +import eu.europa.ted.efx.model.expressions.scalar.DateLiteral; import eu.europa.ted.efx.model.expressions.scalar.DurationExpression; +import eu.europa.ted.efx.model.expressions.scalar.DurationLiteral; +import eu.europa.ted.efx.model.expressions.scalar.NodePath; import eu.europa.ted.efx.model.expressions.scalar.NumericExpression; +import eu.europa.ted.efx.model.expressions.scalar.NumericLiteral; import eu.europa.ted.efx.model.expressions.scalar.ScalarExpression; import eu.europa.ted.efx.model.expressions.scalar.StringExpression; +import eu.europa.ted.efx.model.expressions.scalar.StringLiteral; import eu.europa.ted.efx.model.expressions.scalar.TimeExpression; +import eu.europa.ted.efx.model.expressions.scalar.TimeLiteral; import eu.europa.ted.efx.model.expressions.sequence.NumericSequenceExpression; import eu.europa.ted.efx.model.expressions.sequence.SequenceExpression; import eu.europa.ted.efx.model.expressions.sequence.StringSequenceExpression; @@ -63,38 +83,38 @@ public XPathScriptGenerator(TranslatorOptions translatorOptions) { @Override public PathExpression composeNodeReferenceWithPredicate(PathExpression nodeReference, BooleanExpression predicate) { - return PathExpression.instantiate(nodeReference.getScript() + '[' + predicate.getScript() + ']', EfxDataType.Node.class); + return Expression.instantiate(nodeReference.getScript() + '[' + predicate.getScript() + ']', nodeReference.getClass()); } @Override public PathExpression composeFieldReferenceWithPredicate(PathExpression fieldReference, BooleanExpression predicate) { - return PathExpression.instantiate(fieldReference.getScript() + '[' + predicate.getScript() + ']', fieldReference.getDataType()); + return Expression.instantiate(fieldReference.getScript() + '[' + predicate.getScript() + ']', fieldReference.getClass()); } @Override public PathExpression composeFieldReferenceWithAxis(final PathExpression fieldReference, final String axis) { String resultXPath = XPathProcessor.addAxis(axis, fieldReference.getScript()); - return PathExpression.instantiate(resultXPath, fieldReference.getDataType()); + return Expression.instantiate(resultXPath, fieldReference.getClass()); } @Override public PathExpression composeFieldValueReference(PathExpression fieldReference) { if (fieldReference.is(EfxDataType.String.class)) { - return PathExpression.instantiate(fieldReference.getScript() + "/normalize-space(text())", fieldReference.getDataType()); + return Expression.instantiate(fieldReference.getScript() + "/normalize-space(text())", fieldReference.getClass()); } if (fieldReference.is(EfxDataType.Number.class)) { - return PathExpression.instantiate(fieldReference.getScript() + "/number()", fieldReference.getDataType()); + return Expression.instantiate(fieldReference.getScript() + "/number()", fieldReference.getClass()); } if (fieldReference.is(EfxDataType.Date.class)) { - return PathExpression.instantiate(fieldReference.getScript() + "/xs:date(text())", fieldReference.getDataType()); + return Expression.instantiate(fieldReference.getScript() + "/xs:date(text())", fieldReference.getClass()); } if (fieldReference.is(EfxDataType.Time.class)) { - return PathExpression.instantiate(fieldReference.getScript() + "/xs:time(text())", fieldReference.getDataType()); + return Expression.instantiate(fieldReference.getScript() + "/xs:time(text())", fieldReference.getClass()); } if (fieldReference.is(EfxDataType.Duration.class)) { - return PathExpression.instantiate("(for $F in " + fieldReference.getScript() + " return (if ($F/@unitCode='WEEK')" + // + return Expression.instantiate("(for $F in " + fieldReference.getScript() + " return (if ($F/@unitCode='WEEK')" + // " then xs:dayTimeDuration(concat('P', $F/number() * 7, 'D'))" + // " else if ($F/@unitCode='DAY')" + // " then xs:dayTimeDuration(concat('P', $F/number(), 'D'))" + // @@ -104,10 +124,10 @@ public PathExpression composeFieldValueReference(PathExpression fieldReference) " then xs:yearMonthDuration(concat('P', $F/number(), 'M'))" + // // " else if (" + fieldReference.script + ")" + // // " then fn:error('Invalid @unitCode')" + // - " else ()))", fieldReference.getDataType()); + " else ()))", fieldReference.getClass()); } - return PathExpression.instantiate(fieldReference.getScript(), fieldReference.getDataType()); + return Expression.instantiate(fieldReference.getScript(), fieldReference.getClass()); } @Override @@ -161,41 +181,41 @@ public T composeList(List iterators) { return new IteratorListExpression( @@ -269,7 +283,7 @@ public T composeParenthesizedExpression(T expression, Cla @Override public PathExpression composeExternalReference(StringExpression externalReference) { - return new NodePathExpression( + return new NodePath( "fn:doc(concat($urlPrefix, " + externalReference.getScript() + "))"); } @@ -277,7 +291,7 @@ public PathExpression composeExternalReference(StringExpression externalReferenc @Override public PathExpression composeFieldInExternalReference(PathExpression externalReference, PathExpression fieldReference) { - return PathExpression.instantiate(externalReference.getScript() + fieldReference.getScript(), fieldReference.getDataType()); + return Expression.instantiate(externalReference.getScript() + fieldReference.getScript(), fieldReference.getClass()); } @@ -286,6 +300,12 @@ public PathExpression joinPaths(final PathExpression first, final PathExpression return XPathContextualizer.join(first, second); } + @Override + public PathExpression contextualizePath(final PathExpression absolutePath, + final PathExpression contextPath) { + return XPathContextualizer.contextualize(contextPath, absolutePath); + } + //#region Indexers ---------------------------------------------------------- @Override @@ -451,13 +471,13 @@ public StringExpression composeStringJoin(StringSequenceExpression list, StringE @Override public StringExpression composeNumberFormatting(NumericExpression number, StringExpression format) { - String formatString = format.isLiteral() ? this.translatorOptions.getDecimalFormat().adaptFormatString(format.getScript()) : format.getScript(); + String formatString = format instanceof LiteralExpression ? this.translatorOptions.getDecimalFormat().adaptFormatString(format.getScript()) : format.getScript(); return new StringExpression("format-number(" + number.getScript() + ", " + formatString + ")"); } @Override - public StringExpression getStringLiteralFromUnquotedString(String value) { - return new StringExpression("'" + value + "'", true); + public StringLiteral getStringLiteralFromUnquotedString(String value) { + return new StringLiteral("'" + value + "'"); } @Override diff --git a/src/test/java/eu/europa/ted/efx/mock/AbstractSymbolResolverMock.java b/src/test/java/eu/europa/ted/efx/mock/AbstractSymbolResolverMock.java deleted file mode 100644 index 4799ba45..00000000 --- a/src/test/java/eu/europa/ted/efx/mock/AbstractSymbolResolverMock.java +++ /dev/null @@ -1,174 +0,0 @@ -package eu.europa.ted.efx.mock; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import com.fasterxml.jackson.databind.ObjectMapper; -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.efx.exceptions.SymbolResolutionException; -import eu.europa.ted.efx.interfaces.SymbolResolver; -import eu.europa.ted.efx.model.expressions.path.NodePathExpression; -import eu.europa.ted.efx.model.expressions.path.PathExpression; -import eu.europa.ted.efx.model.types.FieldTypes; -import eu.europa.ted.efx.xpath.XPathContextualizer; - -public abstract class AbstractSymbolResolverMock - implements SymbolResolver { - protected final Logger logger = LoggerFactory.getLogger(getClass()); - - private static final Path TEST_JSON_DIR = Path.of("src", "test", "resources", "json"); - - private static final ObjectMapper mapper = new ObjectMapper(); - - protected Map fieldById; - protected Map nodeById; - protected Map codelistById; - - public AbstractSymbolResolverMock() throws IOException { - this.loadMapData(); - } - - protected abstract Map createNodeById(); - - protected abstract Map createCodelistById(); - - protected abstract Class getSdkFieldClass(); - - protected abstract String getFieldsJsonFilename(); - - protected void loadMapData() throws IOException { - this.fieldById = createFieldById(); - this.nodeById = createNodeById(); - this.codelistById = createCodelistById(); - } - - protected Map createFieldById() throws IOException { - Path fieldsJsonPath = Path.of(TEST_JSON_DIR.toString(), getFieldsJsonFilename()); - - if (!Files.isRegularFile(fieldsJsonPath)) { - throw new FileNotFoundException(fieldsJsonPath.toString()); - } - - List fields = mapper - .readerForListOf(getSdkFieldClass()) - .readValue(fieldsJsonPath.toFile()); - - return fields.stream().collect(Collectors.toMap(SdkField::getId, Function.identity())); - } - - protected F getFieldById(String fieldId) { - return this.fieldById.get(fieldId); - } - - protected N getNodeById(String nodeId) { - return this.nodeById.get(nodeId); - } - - protected C getCodelistById(String codelistId) { - return this.codelistById.get(codelistId); - } - - @Override - public PathExpression getRelativePathOfField(String fieldId, PathExpression contextPath) { - return XPathContextualizer.contextualize(contextPath, getAbsolutePathOfField(fieldId)); - } - - @Override - public PathExpression getRelativePathOfNode(String nodeId, PathExpression contextPath) { - return XPathContextualizer.contextualize(contextPath, getAbsolutePathOfNode(nodeId)); - } - - @Override - public PathExpression getRelativePath(PathExpression absolutePath, PathExpression contextPath) { - return XPathContextualizer.contextualize(contextPath, absolutePath); - } - - @Override - public String getTypeOfField(String fieldId) { - final SdkField sdkField = getFieldById(fieldId); - if (sdkField == null) { - throw SymbolResolutionException.unknownField(fieldId); - } - return sdkField.getType(); - } - - @Override - public String getRootCodelistOfField(final String fieldId) { - final SdkField sdkField = getFieldById(fieldId); - if (sdkField == null) { - throw SymbolResolutionException.unknownField(fieldId); - } - final String codelistId = sdkField.getCodelistId(); - if (codelistId == null) { - throw SymbolResolutionException.noCodelistForField(fieldId); - } - - final SdkCodelist sdkCodelist = getCodelistById(codelistId); - if (sdkCodelist == null) { - throw SymbolResolutionException.unknownCodelist(codelistId); - } - - return sdkCodelist.getRootCodelistId(); - } - - @Override - public List expandCodelist(String codelistId) { - SdkCodelist codelist = getCodelistById(codelistId); - if (codelist == null) { - throw SymbolResolutionException.unknownCodelist(codelistId); - } - return codelist.getCodes(); - } - - /** - * Gets the id of the parent node of a given field. - * - * @param fieldId The id of the field who's parent node we are looking for. - * @return The id of the parent node of the given field. - */ - @Override - public String getParentNodeOfField(final String fieldId) { - final SdkField sdkField = getFieldById(fieldId); - if (sdkField != null) { - return sdkField.getParentNodeId(); - } - throw SymbolResolutionException.unknownField(fieldId); - } - - /** - * @param fieldId The id of a field. - * @return The xPath of the given field. - */ - @Override - public PathExpression getAbsolutePathOfField(final String fieldId) { - final SdkField sdkField = getFieldById(fieldId); - if (sdkField == null) { - throw SymbolResolutionException.unknownField(fieldId); - } - return PathExpression.instantiate(sdkField.getXpathAbsolute(), FieldTypes.fromString(sdkField.getType())); - } - - /** - * @param nodeId The id of a node or a field. - * @return The xPath of the given node or field. - */ - @Override - public PathExpression getAbsolutePathOfNode(final String nodeId) { - final SdkNode sdkNode = getNodeById(nodeId); - - if (sdkNode == null) { - throw SymbolResolutionException.unknownNode(nodeId); - } - - return new NodePathExpression(sdkNode.getXpathAbsolute()); - } -} diff --git a/src/test/java/eu/europa/ted/efx/mock/MarkupGeneratorMock.java b/src/test/java/eu/europa/ted/efx/mock/MarkupGeneratorMock.java index be995298..8db27881 100644 --- a/src/test/java/eu/europa/ted/efx/mock/MarkupGeneratorMock.java +++ b/src/test/java/eu/europa/ted/efx/mock/MarkupGeneratorMock.java @@ -1,3 +1,16 @@ +/* + * 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.efx.mock; import java.util.List; @@ -11,16 +24,18 @@ import eu.europa.ted.efx.interfaces.Parameter; import eu.europa.ted.efx.interfaces.TranslatorContext; import eu.europa.ted.efx.model.expressions.Expression; -import eu.europa.ted.efx.model.expressions.path.PathExpression; +import eu.europa.ted.efx.model.expressions.PathExpression; +import eu.europa.ted.efx.model.expressions.TypedExpression; import eu.europa.ted.efx.model.expressions.scalar.NumericExpression; import eu.europa.ted.efx.model.expressions.scalar.StringExpression; import eu.europa.ted.efx.model.templates.Conditional; import eu.europa.ted.efx.model.templates.Markup; import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxTypeLattice; public class MarkupGeneratorMock implements MarkupGenerator { - static final Map, Markup> typeFromEfxDataType = Map + static final Map, Markup> typeFromEfxDataType = Map .ofEntries( Map.entry(EfxDataType.String.class, new Markup("string")), // Map.entry(EfxDataType.MultilingualString.class, new Markup("string")), // @@ -34,19 +49,33 @@ public class MarkupGeneratorMock implements MarkupGenerator { @Override - public Markup renderVariableDeclaration(Class dataType, String variableName, - Expression initialiser) { - return new Markup(String.format("%s:%s=%s", this.getEfxDataTypeEquivalent(dataType).script, variableName, initialiser.getScript())); + public Markup renderVariableDeclaration(String variableName, TypedExpression initialiser) { + Class dataType = initialiser.getDataType(); + String typeName = this.getEfxDataTypeEquivalent(EfxTypeLattice.toPrimitive(dataType)).script; + if (EfxTypeLattice.isSequence(dataType)) { + typeName += "*"; + } + return new Markup(String.format("%s:%s=%s", typeName, variableName, initialiser.getScript())); } @Override - public Markup renderFunctionDeclaration(Class type, String name, Map> parameters, - Expression expression) { + public Markup renderFunctionDeclaration(String name, Map> parameters, TypedExpression expression) { + Class type = expression.getDataType(); + String typeName = this.getEfxDataTypeEquivalent(EfxTypeLattice.toPrimitive(type)).script; + if (EfxTypeLattice.isSequence(type)) { + typeName += "*"; + } return new Markup( - String.format("%s:%s(%s) -> { %s }", this.getEfxDataTypeEquivalent(type).script, name, + String.format("%s:%s(%s) -> { %s }", typeName, name, parameters.entrySet().stream() - .map(entry -> this.getEfxDataTypeEquivalent(entry.getValue()).script + ":" + entry.getKey()) - .collect(Collectors.joining(", ")), + .map(entry -> { + String paramType = this.getEfxDataTypeEquivalent(EfxTypeLattice.toPrimitive(entry.getValue())).script; + if (EfxTypeLattice.isSequence(entry.getValue())) { + paramType += "*"; + } + return paramType + ":" + entry.getKey(); + }) + .collect(Collectors.joining(", ")), expression.getScript())); } @@ -182,7 +211,8 @@ private String renderSection(String heading, List markupList) { @Override public Markup getEfxDataTypeEquivalent(Class type) { - return typeFromEfxDataType.getOrDefault(type, Markup.empty()); + // Use toPrimitive() to handle both scalar and sequence types + return typeFromEfxDataType.getOrDefault(EfxTypeLattice.toPrimitive(type), Markup.empty()); } @Override diff --git a/src/test/java/eu/europa/ted/efx/mock/sdk1/SymbolResolverMockV1.java b/src/test/java/eu/europa/ted/efx/mock/sdk1/SymbolResolverMockV1.java index c0408de9..b985ee14 100644 --- a/src/test/java/eu/europa/ted/efx/mock/sdk1/SymbolResolverMockV1.java +++ b/src/test/java/eu/europa/ted/efx/mock/sdk1/SymbolResolverMockV1.java @@ -1,102 +1,87 @@ +/* + * Copyright 2023 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.efx.mock.sdk1; import static java.util.Map.entry; -import java.io.IOException; +import java.nio.file.Path; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; -import eu.europa.ted.eforms.xpath.XPathInfo; -import eu.europa.ted.eforms.xpath.XPathProcessor; -import eu.europa.ted.efx.mock.AbstractSymbolResolverMock; -import eu.europa.ted.efx.model.expressions.Expression; -import eu.europa.ted.efx.model.expressions.path.NodePathExpression; -import eu.europa.ted.efx.model.expressions.path.PathExpression; -import eu.europa.ted.efx.sdk1.entity.SdkCodelistV1; -import eu.europa.ted.efx.sdk1.entity.SdkFieldV1; -import eu.europa.ted.efx.sdk1.entity.SdkNodeV1; +import eu.europa.ted.eforms.sdk.SdkSymbolResolver; +import eu.europa.ted.eforms.sdk.component.SdkComponent; +import eu.europa.ted.eforms.sdk.component.SdkComponentType; +import eu.europa.ted.eforms.sdk.entity.SdkCodelist; +import eu.europa.ted.eforms.sdk.entity.v1.SdkCodelistV1; +import eu.europa.ted.eforms.sdk.repository.SdkFieldRepository; +import eu.europa.ted.eforms.sdk.repository.SdkNodeRepository; -public class SymbolResolverMockV1 - extends AbstractSymbolResolverMock { +@SdkComponent(versions = {"1"}, componentType = SdkComponentType.SYMBOL_RESOLVER, qualifier = "mock") +public class SymbolResolverMockV1 extends SdkSymbolResolver { - public SymbolResolverMockV1() throws IOException { + private static final String SDK_VERSION = "eforms-sdk-1.0"; + private static final Path JSON_PATH = Path.of("src", "test", "resources", "json", "sdk1-fields.json"); + + public SymbolResolverMockV1() throws InstantiationException { super(); + loadTestData(); } - private static Entry buildCodelistMock(final String codelistId, + private void loadTestData() throws InstantiationException { + // Load nodes and fields using SDK repositories + this.nodeById = new SdkNodeRepository(SDK_VERSION, JSON_PATH); + this.nodeByAlias = new HashMap<>(); // SDK1 doesn't support aliases + + this.fieldById = new SdkFieldRepository(SDK_VERSION, JSON_PATH, this.nodeById); + this.fieldByAlias = new HashMap<>(); // SDK1 doesn't support aliases + + // Mock codelists + this.codelistById = createMockCodelists(); + + // Mock notice types - not needed, we override getAllNoticeSubtypeIds() + this.noticeTypesById = new HashMap<>(); + } + + private static Entry buildCodelistMock(final String codelistId, final Optional parentId) { return entry(codelistId, new SdkCodelistV1(codelistId, "0.0.1", Arrays.asList("code1", "code2", "code3"), parentId)); } - @Override - protected Map createNodeById() { - return Map.ofEntries( - entry("ND-Root", new SdkNodeV1("ND-Root", null, "/*", "/*", false)), - entry("ND-SubNode", - new SdkNodeV1("ND-SubNode", "ND-Root", "/*/SubNode", "SubNode", false))); - } - - @Override - protected Map createCodelistById() { + private Map createMockCodelists() { return new HashMap<>(Map.ofEntries( buildCodelistMock("accessibility", Optional.empty()), buildCodelistMock("authority-activity", Optional.of("main-activity")), buildCodelistMock("main-activity", Optional.empty()))); } - @Override - protected Class getSdkFieldClass() { - return SdkFieldV1.class; - } - - @Override - protected String getFieldsJsonFilename() { - return "fields-sdk1.json"; - } - - @Override - public boolean isAttributeField(final String fieldId) { - XPathInfo xpathInfo = XPathProcessor.parse(this.getAbsolutePathOfField(fieldId).getScript()); - return xpathInfo.isAttribute(); - } - - @Override - public String getAttributeNameFromAttributeField(String fieldId) { - XPathInfo xpathInfo = XPathProcessor.parse(this.getAbsolutePathOfField(fieldId).getScript()); - return xpathInfo.getAttributeName(); - } - - @Override - public PathExpression getAbsolutePathOfFieldWithoutTheAttribute(String fieldId) { - XPathInfo xpathInfo = XPathProcessor.parse(this.getAbsolutePathOfField(fieldId).getScript()); - return Expression.instantiate(xpathInfo.getPathToLastElement(), NodePathExpression.class); - } - @Override public String getFieldIdFromAlias(String alias) { - throw new UnsupportedOperationException( - "Alias resolution is not supported in SDK-1."); + throw new UnsupportedOperationException("Alias resolution is not supported in SDK-1."); } @Override public String getNodeIdFromAlias(String alias) { - throw new UnsupportedOperationException( - "Alias resolution is not supported in SDK-1."); + throw new UnsupportedOperationException("Alias resolution is not supported in SDK-1."); } - /** - * Returns a list of all valid notice type IDs for testing. - * Uses a small set that covers all testing scenarios: - * - Numeric types (1-5) for basic testing and ranges - * - Alphanumeric types (E1, E2, X01) for non-numeric ID testing - */ @Override - public java.util.List getAllNoticeSubtypeIds() { - return java.util.Arrays.asList("1", "2", "3", "4", "5", "E1", "E2", "X01"); + public List getAllNoticeSubtypeIds() { + return Arrays.asList("1", "2", "3", "4", "5", "E1", "E2", "X01"); } - } diff --git a/src/test/java/eu/europa/ted/efx/mock/sdk2/SymbolResolverMockV2.java b/src/test/java/eu/europa/ted/efx/mock/sdk2/SymbolResolverMockV2.java index 3bf2860a..73a24d0d 100644 --- a/src/test/java/eu/europa/ted/efx/mock/sdk2/SymbolResolverMockV2.java +++ b/src/test/java/eu/europa/ted/efx/mock/sdk2/SymbolResolverMockV2.java @@ -1,73 +1,69 @@ +/* + * 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.efx.mock.sdk2; import static java.util.Map.entry; -import java.io.IOException; +import java.nio.file.Path; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; -import java.util.stream.Collectors; -import eu.europa.ted.eforms.xpath.XPathInfo; -import eu.europa.ted.eforms.xpath.XPathProcessor; -import eu.europa.ted.efx.mock.AbstractSymbolResolverMock; -import eu.europa.ted.efx.model.expressions.Expression; -import eu.europa.ted.efx.model.expressions.path.NodePathExpression; -import eu.europa.ted.efx.model.expressions.path.PathExpression; -import eu.europa.ted.efx.sdk2.entity.SdkCodelistV2; -import eu.europa.ted.efx.sdk2.entity.SdkFieldV2; -import eu.europa.ted.efx.sdk2.entity.SdkNodeV2; +import eu.europa.ted.eforms.sdk.SdkSymbolResolver; +import eu.europa.ted.eforms.sdk.component.SdkComponent; +import eu.europa.ted.eforms.sdk.component.SdkComponentType; +import eu.europa.ted.eforms.sdk.entity.SdkCodelist; +import eu.europa.ted.eforms.sdk.entity.v2.SdkCodelistV2; +import eu.europa.ted.eforms.sdk.repository.SdkFieldRepository; +import eu.europa.ted.eforms.sdk.repository.SdkNodeRepository; -public class SymbolResolverMockV2 - extends AbstractSymbolResolverMock { - protected Map fieldByAlias; - protected Map nodeByAlias; +@SdkComponent(versions = {"2"}, componentType = SdkComponentType.SYMBOL_RESOLVER, qualifier = "mock") +public class SymbolResolverMockV2 extends SdkSymbolResolver { - public SymbolResolverMockV2() throws IOException { - super(); - } + private static final String SDK_VERSION = "eforms-sdk-2.0"; + private static final Path JSON_PATH = Path.of("src", "test", "resources", "json", "sdk2-fields.json"); - private static Entry buildCodelistMock(final String codelistId, - final Optional parentId) { - return entry(codelistId, new SdkCodelistV2(codelistId, "0.0.1", - Arrays.asList("code1", "code2", "code3"), parentId)); + public SymbolResolverMockV2() throws InstantiationException { + super(); + loadTestData(); } - @Override - public void loadMapData() throws IOException { - super.loadMapData(); + private void loadTestData() throws InstantiationException { + // Load nodes and fields using SDK repositories + this.nodeById = new SdkNodeRepository(SDK_VERSION, JSON_PATH); + this.nodeByAlias = indexNodesByAlias(); - this.fieldByAlias = this.fieldById.entrySet().stream() - .collect(Collectors.toMap(e -> e.getValue().getAlias(), e -> e.getValue())); + this.fieldById = new SdkFieldRepository(SDK_VERSION, JSON_PATH, this.nodeById); + this.fieldByAlias = indexFieldsByAlias(); - this.nodeByAlias = this.nodeById.entrySet().stream() - .collect(Collectors.toMap(e -> e.getValue().getAlias(), e -> e.getValue())); - } - - @Override - public SdkFieldV2 getFieldById(final String fieldId) { - return this.fieldById.containsKey(fieldId) ? this.fieldById.get(fieldId) - : this.fieldByAlias.get(fieldId); - } + // Mock codelists + this.codelistById = createMockCodelists(); - @Override - public SdkNodeV2 getNodeById(final String nodeId) { - return this.nodeById.containsKey(nodeId) ? this.nodeById.get(nodeId) - : this.nodeByAlias.get(nodeId); + // Mock notice types - not needed, we override getAllNoticeSubtypeIds() + this.noticeTypesById = new HashMap<>(); } - @Override - protected Map createNodeById() { - return Map.ofEntries( - entry("ND-Root", new SdkNodeV2("ND-Root", null, "/*", "/*", false, "Root")), - entry("ND-SubNode", new SdkNodeV2("ND-SubNode", "ND-Root", "/*/SubNode", "SubNode", false, "SubNode")), - entry("ND-SubSubNode", new SdkNodeV2("ND-SubSubNode", "ND-SubNode", "/*/SubNode/SubSubNode", "SubSubNode", false, "SubSubNode"))); + private static Entry buildCodelistMock(final String codelistId, + final Optional parentId) { + return entry(codelistId, new SdkCodelistV2(codelistId, "0.0.1", + Arrays.asList("code1", "code2", "code3"), parentId)); } - @Override - protected Map createCodelistById() { + private Map createMockCodelists() { return new HashMap<>(Map.ofEntries( buildCodelistMock("accessibility", Optional.empty()), buildCodelistMock("authority-activity", Optional.of("main-activity")), @@ -75,58 +71,7 @@ protected Map createCodelistById() { } @Override - protected Class getSdkFieldClass() { - return SdkFieldV2.class; - } - - @Override - protected String getFieldsJsonFilename() { - return "fields-sdk2.json"; - } - - @Override - public boolean isAttributeField(final String fieldId) { - XPathInfo xpathInfo = XPathProcessor.parse(this.getAbsolutePathOfField(fieldId).getScript()); - return xpathInfo.isAttribute(); - } - - @Override - public String getAttributeNameFromAttributeField(String fieldId) { - XPathInfo xpathInfo = XPathProcessor.parse(this.getAbsolutePathOfField(fieldId).getScript()); - return xpathInfo.getAttributeName(); - } - - @Override - public PathExpression getAbsolutePathOfFieldWithoutTheAttribute(String fieldId) { - XPathInfo xpathInfo = XPathProcessor.parse(this.getAbsolutePathOfField(fieldId).getScript()); - return Expression.instantiate(xpathInfo.getPathToLastElement(), NodePathExpression.class); - } - - @Override - public String getFieldIdFromAlias(String alias) { - if (this.fieldByAlias.containsKey(alias)) { - return this.fieldByAlias.get(alias).getId(); - } - return null; - - } - - @Override - public String getNodeIdFromAlias(String alias) { - if (this.nodeByAlias.containsKey(alias)) { - return this.nodeByAlias.get(alias).getId(); - } - return null; - } - - /** - * Returns a list of all valid notice type IDs for testing. - * Uses a small set that covers all testing scenarios: - * - Numeric types (1-5) for basic testing and ranges - * - Alphanumeric types (E1, E2, X01) for non-numeric ID testing - */ - @Override - public java.util.List getAllNoticeSubtypeIds() { - return java.util.Arrays.asList("1", "2", "3", "4", "5", "E1", "E2", "X01"); + public List getAllNoticeSubtypeIds() { + return Arrays.asList("1", "2", "3", "4", "5", "E1", "E2", "X01"); } } diff --git a/src/test/java/eu/europa/ted/efx/model/types/EfxTypeLatticeTest.java b/src/test/java/eu/europa/ted/efx/model/types/EfxTypeLatticeTest.java new file mode 100644 index 00000000..9a92bf41 --- /dev/null +++ b/src/test/java/eu/europa/ted/efx/model/types/EfxTypeLatticeTest.java @@ -0,0 +1,101 @@ +/* + * Copyright 2026 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.efx.model.types; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.lang.reflect.Field; +import java.util.List; + +import org.junit.jupiter.api.Test; + +class EfxTypeLatticeTest { + + /** + * Verifies that all concrete EfxDataType types (those implementing Cardinality.Scalar or Cardinality.Sequence) + * are registered in EfxTypeLattice.TYPE_VARIANTS. + * + * This test catches "forgot to register" errors at build time when adding new types. + */ + @Test + void allConcreteTypesShouldBeRegistered() { + for (Class nested : EfxDataType.class.getDeclaredClasses()) { + // Skip marker interfaces (Cardinality, Primitive, ConcreteScalar, ConcreteSequence) + if (nested == EfxDataType.Cardinality.class + || nested == EfxDataType.Primitive.class + || nested == EfxDataType.ConcreteScalar.class + || nested == EfxDataType.ConcreteSequence.class) { + continue; + } + + boolean isScalar = EfxDataType.Cardinality.Scalar.class.isAssignableFrom(nested); + boolean isSequence = EfxDataType.Cardinality.Sequence.class.isAssignableFrom(nested); + + if (isScalar || isSequence) { + assertTrue( + EfxTypeLattice.isRegistered(nested), + "Concrete type " + nested.getSimpleName() + " not registered in TYPE_VARIANTS" + ); + } + } + } + + /** + * Verifies that TYPE_VARIANTS is ordered correctly: subtypes must come before their supertypes. + * This is critical because the lookup methods use isAssignableFrom() which would match the wrong type + * if supertypes came first. + * + * For example, MultilingualString extends String, so MultilingualString must appear before String + * in TYPE_VARIANTS, otherwise a MultilingualString type would incorrectly match String. + */ + @Test + @SuppressWarnings("unchecked") + void typeVariantsShouldBeOrderedWithSubtypesBeforeSupertypes() throws Exception { + // Use reflection to access the private TYPE_VARIANTS field + Field typeVariantsField = EfxTypeLattice.class.getDeclaredField("TYPE_VARIANTS"); + typeVariantsField.setAccessible(true); + List typeVariants = (List) typeVariantsField.get(null); + + // Get the TypeVariants inner class + Class typeVariantsClass = Class.forName("eu.europa.ted.efx.model.types.EfxTypeLattice$TypeVariants"); + Field primitiveField = typeVariantsClass.getDeclaredField("primitive"); + primitiveField.setAccessible(true); + + // Check each pair: earlier types should not be supertypes of later types + for (int i = 0; i < typeVariants.size(); i++) { + Object currentVariant = typeVariants.get(i); + Class currentPrimitive = (Class) primitiveField.get(currentVariant); + + for (int j = i + 1; j < typeVariants.size(); j++) { + Object laterVariant = typeVariants.get(j); + Class laterPrimitive = (Class) primitiveField.get(laterVariant); + + // Check if the current type is a supertype of the later type + // This would be a violation: supertypes must come AFTER subtypes + if (currentPrimitive.isAssignableFrom(laterPrimitive) + && !currentPrimitive.equals(laterPrimitive)) { + fail(String.format( + "TYPE_VARIANTS ordering violation: %s (at index %d) is a supertype of %s (at index %d). " + + "Supertypes must come after their subtypes to ensure correct matching with isAssignableFrom(). " + + "Please move %s after %s in the TYPE_VARIANTS list.", + currentPrimitive.getSimpleName(), i, + laterPrimitive.getSimpleName(), j, + laterPrimitive.getSimpleName(), + currentPrimitive.getSimpleName())); + } + } + } + } +} diff --git a/src/test/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2Test.java b/src/test/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2Test.java index c0ce95a3..3f1f83ba 100644 --- a/src/test/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2Test.java +++ b/src/test/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2Test.java @@ -1,3 +1,16 @@ +/* + * Copyright 2023 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.efx.sdk2; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -5,6 +18,8 @@ import org.antlr.v4.runtime.misc.ParseCancellationException; import org.junit.jupiter.api.Test; import eu.europa.ted.efx.EfxTestsBase; +import eu.europa.ted.efx.exceptions.InvalidIdentifierException; +import eu.europa.ted.efx.exceptions.TypeMismatchException; class EfxExpressionTranslatorV2Test extends EfxTestsBase { @Override @@ -543,6 +558,15 @@ void testStringsFromStringIteration_UsingFieldReference() { "'a' in (for text:$x in BT-00-Text return concat($x, 'text'))"); } + @Test + void testStringsFromStringIteration_UsingMultilingualFieldReference() { + // This test verifies that iterating over a multilingual text field with a text iterator works correctly + // The iterator variable should inherit the actual type (multilingual string) from the sequence + testExpressionTranslationWithContext( + "'a' = (for $x in PathNode/TextMultilingualField/normalize-space(text()) return concat($x, 'text'))", "ND-Root", + "'a' in (for text:$x in BT-00-Text-Multilingual return concat($x, 'text'))"); + } + @Test void testStringsFromBooleanIteration_UsingLiterals() { @@ -1679,6 +1703,42 @@ void testParameterizedExpression_WithDurationParameter() { "{ND-Root, measure:$p1, measure:$p2} ${$p1 == $p2}", "P1Y", "P2Y"); } + @Test + void testParameterizedExpression_WithTextSequenceParameter() { + testExpressionTranslation("count(('a','b','c'))", + "{ND-Root, text*:$items} ${count($items)}", "('a', 'b', 'c')"); + } + + @Test + void testParameterizedExpression_WithNumericSequenceParameter() { + testExpressionTranslation("count((1,2,3))", + "{ND-Root, number*:$items} ${count($items)}", "(1, 2, 3)"); + } + + @Test + void testParameterizedExpression_WithBooleanSequenceParameter() { + testExpressionTranslation("count((true(),false(),true()))", + "{ND-Root, indicator*:$items} ${count($items)}", "(TRUE, FALSE, ALWAYS)"); + } + + @Test + void testParameterizedExpression_WithDateSequenceParameter() { + testExpressionTranslation("count((xs:date('2024-01-01Z'),xs:date('2024-12-31Z')))", + "{ND-Root, date*:$items} ${count($items)}", "(2024-01-01Z, 2024-12-31Z)"); + } + + @Test + void testParameterizedExpression_WithTimeSequenceParameter() { + testExpressionTranslation("count((xs:time('10:00:00Z'),xs:time('18:00:00Z')))", + "{ND-Root, time*:$items} ${count($items)}", "(10:00:00Z, 18:00:00Z)"); + } + + @Test + void testParameterizedExpression_WithDurationSequenceParameter() { + testExpressionTranslation("count((xs:yearMonthDuration('P1Y'),xs:yearMonthDuration('P2M')))", + "{ND-Root, measure*:$items} ${count($items)}", "(P1Y, P2M)"); + } + // #endregion: Compare sequences // #endregion Sequence Functions @@ -1686,15 +1746,35 @@ void testParameterizedExpression_WithDurationParameter() { // #region: Indexers -------------------------------------------------------- @Test - void testIndexer_WithFieldReference() { - testExpressionTranslationWithContext("PathNode/TextField/normalize-space(text())[1]", "ND-Root", "(text)BT-00-Text[1]"); + void testIndexer_WithNonRepeatableField() { + // Indexing a non-repeatable field - currently allowed (semantically odd but syntactically valid) + testExpressionTranslationWithContext("(PathNode/TextField/normalize-space(text()))[1]", "ND-Root", + "BT-00-Text[1]"); + } + + @Test + void testIndexer_WithNonRepeatableFieldAndPredicate() { + // Indexing a non-repeatable field with predicate + testExpressionTranslationWithContext( + "(PathNode/TextField[./normalize-space(text()) = 'hello']/normalize-space(text()))[1]", "ND-Root", + "BT-00-Text[BT-00-Text == 'hello'][1]"); } @Test - void testIndexer_WithFieldReferenceAndPredicate() { + void testIndexer_WithRepeatableField() { + // Indexing a repeatable field should work - BT-00-Repeatable-Text is repeatable + // No explicit cast - preprocessor inserts (text*) making it stringSequence[indexer] + testExpressionTranslationWithContext("(PathNode/RepeatableTextField/normalize-space(text()))[1]", "ND-Root", + "BT-00-Repeatable-Text[1]"); + } + + @Test + void testIndexer_WithRepeatableFieldAndPredicate() { + // Indexing a repeatable field with predicate should work + // No explicit cast - preprocessor handles typing testExpressionTranslationWithContext( - "PathNode/TextField[./normalize-space(text()) = 'hello']/normalize-space(text())[1]", "ND-Root", - "(text)BT-00-Text[BT-00-Text == 'hello'][1]"); + "(PathNode/RepeatableTextField[(./normalize-space(text()))[1] = 'hello']/normalize-space(text()))[1]", "ND-Root", + "BT-00-Repeatable-Text[BT-00-Repeatable-Text[1] == 'hello'][1]"); } @Test @@ -1703,4 +1783,292 @@ void testIndexer_WithTextSequence() { } // #endregion: Indexers + + // #region: Scalar/Sequence Validation -------------------------------------- + + @Test + void testScalarFromRepeatableField_ThrowsError() { + // A repeatable field used as scalar should throw TypeMismatchException.fieldMayRepeat() + TypeMismatchException ex = assertThrows(TypeMismatchException.class, + () -> translateExpressionWithContext("ND-Root", "BT-00-Repeatable-Text == 'test'")); + assertEquals(TypeMismatchException.ErrorCode.EXPECTED_SEQUENCE, ex.getErrorCode()); + } + + @Test + void testScalarFromFieldInRepeatableNode_ThrowsErrorFromRootContext() { + // Field in ND-RepeatableNode (repeatable) used as scalar from ND-Root should throw + TypeMismatchException ex = assertThrows(TypeMismatchException.class, + () -> translateExpressionWithContext("ND-Root", "BT-00-Text-In-Repeatable-Node == 'test'")); + assertEquals(TypeMismatchException.ErrorCode.EXPECTED_SEQUENCE, ex.getErrorCode()); + } + + @Test + void testScalarFromFieldInRepeatableNode_OkFromSameContext() { + // Field in ND-RepeatableNode used as scalar from ND-RepeatableNode context should NOT throw + testExpressionTranslationWithContext("TextField/normalize-space(text()) = 'test'", + "ND-RepeatableNode", "BT-00-Text-In-Repeatable-Node == 'test'"); + } + + @Test + void testScalarFromFieldInNestedRepeatableNode_ThrowsErrorFromRootContext() { + // Field in ND-RepeatableSubSubNode (inside ND-NonRepeatableSubNode inside ND-RepeatableNode) used from root should throw + TypeMismatchException ex = assertThrows(TypeMismatchException.class, + () -> translateExpressionWithContext("ND-Root", "BT-00-Text-In-RepeatableSubSubNode == 'test'")); + assertEquals(TypeMismatchException.ErrorCode.EXPECTED_SEQUENCE, ex.getErrorCode()); + } + + @Test + void testScalarFromFieldInNestedRepeatableNode_ThrowsErrorFromRepeatableNodeContext() { + // Field in ND-RepeatableSubSubNode used from ND-RepeatableNode should still throw (ND-RepeatableSubSubNode is also repeatable) + TypeMismatchException ex = assertThrows(TypeMismatchException.class, + () -> translateExpressionWithContext("ND-RepeatableNode", "BT-00-Text-In-RepeatableSubSubNode == 'test'")); + assertEquals(TypeMismatchException.ErrorCode.EXPECTED_SEQUENCE, ex.getErrorCode()); + } + + @Test + void testScalarFromFieldInNestedRepeatableNode_OkFromRepeatableSubSubNodeContext() { + // Field in ND-RepeatableSubSubNode used from ND-RepeatableSubSubNode context should NOT throw + testExpressionTranslationWithContext("TextField/normalize-space(text()) = 'test'", + "ND-RepeatableSubSubNode", "BT-00-Text-In-RepeatableSubSubNode == 'test'"); + } + + @Test + void testScalarFromFieldInNonRepeatableNestedInRepeatable_ThrowsErrorFromRootContext() { + // Field in ND-NonRepeatableSubNode (non-repeatable) inside ND-RepeatableNode (repeatable) used from root should throw + TypeMismatchException ex = assertThrows(TypeMismatchException.class, + () -> translateExpressionWithContext("ND-Root", "BT-00-Text-In-NonRepeatableSubNode == 'test'")); + assertEquals(TypeMismatchException.ErrorCode.EXPECTED_SEQUENCE, ex.getErrorCode()); + } + + @Test + void testScalarFromFieldInNonRepeatableNestedInRepeatable_OkFromRepeatableNodeContext() { + // Field in ND-NonRepeatableSubNode used from ND-RepeatableNode context should NOT throw + testExpressionTranslationWithContext("NonRepeatableSubNode/TextField/normalize-space(text()) = 'test'", + "ND-RepeatableNode", "BT-00-Text-In-NonRepeatableSubNode == 'test'"); + } + + // #endregion: Scalar/Sequence Validation + + // #region: InvalidIdentifierException Tests -------------------------------- + + @Test + void testContextSpecifier_WithRegularVariable_ThrowsNotAContextVariable() { + // A regular variable (not declared with context:) used as context specifier should throw + InvalidIdentifierException ex = assertThrows(InvalidIdentifierException.class, + () -> translateExpressionWithContext("ND-Root", "for text:$x in BT-00-Text return $x::BT-00-Number")); + assertEquals(InvalidIdentifierException.ErrorCode.NOT_A_CONTEXT_VARIABLE, ex.getErrorCode()); + } + + @Test + void testContextSpecifier_UndeclaredVariable_ThrowsNotAContextVariable() { + // An undeclared variable used as context specifier should throw (NOT_A_CONTEXT_VARIABLE + // because the lookup for context variables fails before checking if the variable exists) + InvalidIdentifierException ex = assertThrows(InvalidIdentifierException.class, + () -> translateExpressionWithContext("ND-Root", "$undefined::BT-00-Text")); + assertEquals(InvalidIdentifierException.ErrorCode.NOT_A_CONTEXT_VARIABLE, ex.getErrorCode()); + } + + // #endregion: InvalidIdentifierException Tests + + // #region: TypeMismatchException - nodeCannotBeValue ------------------------- + + @Test + void testScalarFromNodeContextVariable_ThrowsNodeCannotBeValue() { + // A node context variable used as scalar value should throw + TypeMismatchException ex = assertThrows(TypeMismatchException.class, + () -> translateExpressionWithContext("ND-Root", "for context:$n in ND-SubNode return $n == 'test'")); + assertEquals(TypeMismatchException.ErrorCode.EXPECTED_FIELD_CONTEXT, ex.getErrorCode()); + } + + @Test + void testSequenceFromNodeContextVariable_ThrowsNodeCannotBeValue() { + // A node context variable used in count() should throw + TypeMismatchException ex = assertThrows(TypeMismatchException.class, + () -> translateExpressionWithContext("ND-Root", "for context:$n in ND-SubNode return count($n)")); + assertEquals(TypeMismatchException.ErrorCode.EXPECTED_FIELD_CONTEXT, ex.getErrorCode()); + } + + // #endregion: TypeMismatchException - nodeCannotBeValue + + // #region: TypeMismatchException - fieldMayRepeat (Context Variables) -------- + + @Test + void testScalarFromFieldContextVariable_NonRepeatable_Works() { + // A non-repeatable field context variable used as scalar should work + testExpressionTranslationWithContext( + "for $f in PathNode/TextField return $f = 'test'", + "ND-Root", + "for context:$f in BT-00-Text return $f == 'test'"); + } + + @Test + void testScalarFromFieldContextVariable_Repeatable_ThrowsFieldMayRepeat() { + // A repeatable field context variable used as scalar should throw + TypeMismatchException ex = assertThrows(TypeMismatchException.class, + () -> translateExpressionWithContext("ND-Root", "for context:$f in BT-00-Repeatable-Text return $f == 'test'")); + assertEquals(TypeMismatchException.ErrorCode.EXPECTED_SEQUENCE, ex.getErrorCode()); + } + + // #endregion: TypeMismatchException - fieldMayRepeat (Context Variables) + + // #region: Context Override Syntax Tests ------------------------------------ + + @Test + void testContextOverride_NodeContextVariable_Works() { + // A node context variable used with context specifier syntax should work + testExpressionTranslationWithContext( + "for $n in SubNode return $n/SubTextField/normalize-space(text())", + "ND-Root", + "for context:$n in ND-SubNode return $n::BT-01-SubNode-Text"); + } + + // Note: Test 4.2 (regular variable as context specifier) is already covered by + // testContextSpecifier_WithRegularVariable_ThrowsNotAContextVariable above + + // #endregion: Context Override Syntax Tests + + // #region: For Loop Iterator Sequence Validation ----------------------------- + // These tests verify that repeatable fields are correctly allowed when used as + // sequences in for loop iterators, even though they would fail as scalars in return. + + @Test + void testForLoopIterator_RepeatableFieldAsSequence_Works() { + // A repeatable field used as a sequence in a for loop iterator should work. + // The return expression uses the iterator variable which is scalar. + // This is the correct way to handle repeatable fields in for loops. + testExpressionTranslationWithContext( + "for $b in PathNode/RepeatableTextField/normalize-space(text()) return $b", + "ND-Root", + "for text:$b in BT-00-Repeatable-Text return $b"); + } + + @Test + void testForLoopIterator_RepeatableFieldWithPredicate_Works() { + // A repeatable field with a predicate used as sequence in iterator should work. + // This pattern is equivalent to the problematic SDK template rewritten correctly. + testExpressionTranslationWithContext( + "for $a in PathNode/TextField/normalize-space(text()), $b in SubNode/RepeatableInSubNode/Text[./normalize-space(text()) = $a]/normalize-space(text()) return $b", + "ND-Root", + "for text:$a in BT-00-Text, text:$b in BT-13-Text[BT-13-Text == $a] return $b"); + } + + // #endregion: For Loop Iterator Sequence Validation + + // #region: Predicate Comparison with Repeatable Fields ------------------------- + // These tests verify that repeatable fields in predicate comparisons are handled correctly. + + @Test + void testPredicateComparison_RepeatableFieldAsScalar_ThrowsError() { + // A repeatable field used as scalar in a predicate comparison should throw. + // Pattern: FIELD[REPEATABLE_FIELD == $var] - the repeatable field is used as scalar + TypeMismatchException ex = assertThrows(TypeMismatchException.class, + () -> translateExpressionWithContext("ND-Root", "BT-00-Text[BT-00-Repeatable-Text == 'test']")); + assertEquals(TypeMismatchException.ErrorCode.EXPECTED_SEQUENCE, ex.getErrorCode()); + } + + @Test + void testPredicateComparison_RepeatableFieldWithSomeSatisfies_Works() { + // Using "some ... satisfies" to check if any value matches should work. + // This is the correct way to compare against a repeatable field. + // Pattern: FIELD[some text:$x in REPEATABLE_FIELD satisfies $x == $var] + testExpressionTranslationWithContext( + "PathNode/TextField[some $x in ../RepeatableTextField/normalize-space(text()) satisfies $x = 'test']/normalize-space(text())", + "ND-Root", + "BT-00-Text[some text:$x in BT-00-Repeatable-Text satisfies $x == 'test']"); + } + + // #endregion: Predicate Comparison with Repeatable Fields + + // #region: B1 Grammar Issue - Parentheses in some...in expressions -------- + // These tests verify that parentheses around field references in some...in + // expressions are correctly parsed as sequences, not as single-element lists. + // + // The issue: (FIELD) in "some text:$x in (FIELD) satisfies ..." was previously + // parsed as a scalar list instead of a parenthesized sequence reference. + // This has been fixed by the scalar/sequence grammar separation (TEDEFO-4808). + + @Test + void testSomeSatisfies_ParenthesizedFieldReference_String() { + // Parentheses around a string field reference should still be treated as a sequence. + // The parentheses are preserved in the XPath output. + testExpressionTranslationWithContext( + "some $x in (PathNode/RepeatableTextField/normalize-space(text())) satisfies $x = 'test'", + "ND-Root", + "some text:$x in (BT-00-Repeatable-Text) satisfies $x == 'test'"); + } + + @Test + void testSomeSatisfies_ParenthesizedFieldReferenceWithPredicate_String() { + // Parentheses around a field reference with predicate should work as sequence. + // Note: The predicate comparison still needs some...satisfies pattern for repeatable fields. + testExpressionTranslationWithContext( + "some $x in (PathNode/TextField[some $y in ../RepeatableTextField/normalize-space(text()) satisfies $y = 'filter']/normalize-space(text())) satisfies $x = 'test'", + "ND-Root", + "some text:$x in (BT-00-Text[some text:$y in BT-00-Repeatable-Text satisfies $y == 'filter']) satisfies $x == 'test'"); + } + + @Test + void testSomeSatisfies_ParenthesizedFieldFromRepeatableNode_String() { + // A field from a repeatable node, wrapped in parentheses, should work as sequence. + testExpressionTranslationWithContext( + "some $x in (RepeatableNode/TextField/normalize-space(text())) satisfies $x = 'test'", + "ND-Root", + "some text:$x in (BT-00-Text-In-Repeatable-Node) satisfies $x == 'test'"); + } + + @Test + void testSomeSatisfies_NestedParenthesizedFieldReference_String() { + // Double parentheses should also work. + testExpressionTranslationWithContext( + "some $x in ((PathNode/RepeatableTextField/normalize-space(text()))) satisfies $x = 'test'", + "ND-Root", + "some text:$x in ((BT-00-Repeatable-Text)) satisfies $x == 'test'"); + } + + @Test + void testForLoop_ParenthesizedFieldReference_String() { + // Parentheses around a field reference in a for loop should work as sequence. + testExpressionTranslationWithContext( + "for $x in (PathNode/RepeatableTextField/normalize-space(text())) return $x", + "ND-Root", + "for text:$x in (BT-00-Repeatable-Text) return $x"); + } + + @Test + void testEverySatisfies_ParenthesizedFieldReference_String() { + // "every" quantifier with parenthesized field reference should work as sequence. + testExpressionTranslationWithContext( + "every $x in (PathNode/RepeatableTextField/normalize-space(text())) satisfies $x != ''", + "ND-Root", + "every text:$x in (BT-00-Repeatable-Text) satisfies $x != ''"); + } + + @Test + void testCount_ParenthesizedFieldReference() { + // count() with parenthesized field reference should work as sequence. + testExpressionTranslationWithContext( + "count((PathNode/RepeatableTextField/normalize-space(text())))", + "ND-Root", + "count((BT-00-Repeatable-Text))"); + } + + @Test + void testDistinctValues_ParenthesizedFieldReference_String() { + // distinct-values() with parenthesized field reference should work as sequence. + testExpressionTranslationWithContext( + "distinct-values((PathNode/RepeatableTextField/normalize-space(text())))", + "ND-Root", + "distinct-values((BT-00-Repeatable-Text))"); + } + + @Test + void testStringJoin_ParenthesizedFieldReference() { + // string-join() with parenthesized field reference should work as sequence. + testExpressionTranslationWithContext( + "string-join((PathNode/RepeatableTextField/normalize-space(text())), ', ')", + "ND-Root", + "string-join((BT-00-Repeatable-Text), ', ')"); + } + + // #endregion: B1 Grammar Issue } diff --git a/src/test/java/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test.java b/src/test/java/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test.java index cce1dc6d..e758677e 100644 --- a/src/test/java/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test.java +++ b/src/test/java/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test.java @@ -153,6 +153,15 @@ void testWithClause_VariablePositioning() throws IOException { assertAllOutputs(testName, outputFiles); } + @Test + void testWithClause_ContextVariableOverride() throws IOException { + String testName = "testWithClause_ContextVariableOverride"; + Map outputFiles = translator.translateRules(readInput(testName)); + + assertEquals(7, outputFiles.size(), "Should generate exactly 7 files"); + assertAllOutputs(testName, outputFiles); + } + //#endregion WITH clause tests //#region ASSERT and REPORT tests @@ -273,6 +282,64 @@ void testVariable_StageLevel() throws IOException { //#endregion Variable tests (output verification) + //#region Sequence variable tests + + @Test + void testVariable_TextSequence_GlobalLevel() throws IOException { + String testName = "testVariable_TextSequence_GlobalLevel"; + Map outputFiles = translator.translateRules(readInput(testName)); + + assertEquals(7, outputFiles.size(), "Should generate exactly 7 files"); + assertAllOutputs(testName, outputFiles); + } + + @Test + void testVariable_NumericSequence_GlobalLevel() throws IOException { + String testName = "testVariable_NumericSequence_GlobalLevel"; + Map outputFiles = translator.translateRules(readInput(testName)); + + assertEquals(7, outputFiles.size(), "Should generate exactly 7 files"); + assertAllOutputs(testName, outputFiles); + } + + @Test + void testVariable_BooleanSequence_GlobalLevel() throws IOException { + String testName = "testVariable_BooleanSequence_GlobalLevel"; + Map outputFiles = translator.translateRules(readInput(testName)); + + assertEquals(7, outputFiles.size(), "Should generate exactly 7 files"); + assertAllOutputs(testName, outputFiles); + } + + @Test + void testVariable_DateSequence_GlobalLevel() throws IOException { + String testName = "testVariable_DateSequence_GlobalLevel"; + Map outputFiles = translator.translateRules(readInput(testName)); + + assertEquals(7, outputFiles.size(), "Should generate exactly 7 files"); + assertAllOutputs(testName, outputFiles); + } + + @Test + void testVariable_TimeSequence_GlobalLevel() throws IOException { + String testName = "testVariable_TimeSequence_GlobalLevel"; + Map outputFiles = translator.translateRules(readInput(testName)); + + assertEquals(7, outputFiles.size(), "Should generate exactly 7 files"); + assertAllOutputs(testName, outputFiles); + } + + @Test + void testVariable_DurationSequence_GlobalLevel() throws IOException { + String testName = "testVariable_DurationSequence_GlobalLevel"; + Map outputFiles = translator.translateRules(readInput(testName)); + + assertEquals(7, outputFiles.size(), "Should generate exactly 7 files"); + assertAllOutputs(testName, outputFiles); + } + + //#endregion Sequence variable tests + //#region Variable tests (error verification) @Test @@ -439,4 +506,22 @@ void testOutput_ComprehensiveMixedRules() throws IOException { } //#endregion Comprehensive/Integration tests + + //#region Context variable type tests + + /** + * Context variables should always be scalar, even when pointing to a repeatable field. + * This is because the context iterates, so the variable holds the current iteration value. + * Using $ctx in scalar arithmetic ($ctx + 1) should work. + */ + @Test + void testContextVariable_RepeatableField_UsedAsScalar() throws IOException { + String testName = "testContextVariable_RepeatableField_UsedAsScalar"; + Map outputFiles = translator.translateRules(readInput(testName)); + + assertEquals(7, outputFiles.size(), "Should generate exactly 7 files"); + assertAllOutputs(testName, outputFiles); + } + + //#endregion Context variable type tests } diff --git a/src/test/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2Test.java b/src/test/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2Test.java index 6e2d4ace..fa775c03 100644 --- a/src/test/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2Test.java +++ b/src/test/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2Test.java @@ -1,7 +1,22 @@ +/* + * 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.efx.sdk2; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.antlr.v4.runtime.misc.ParseCancellationException; import org.junit.jupiter.api.Test; import eu.europa.ted.efx.EfxTestsBase; @@ -14,9 +29,9 @@ protected String getSdkVersion() { return "eforms-sdk-2.0"; } - // #region Core Template Structure ------------------------------------------ + // #region Core Template Structure ------------------------------------------- - // #region templateDefinition ----------------------------------------------- + // #region templateDefinition ------------------------------------------------ @Test void testTemplateDefinition_InvokeTemplate() { @@ -79,9 +94,9 @@ void testTemplateDefinition_ParameterValidation() { "invoke param-validation-template('test', 42);"))); } - // #endregion templateDefinition -------------------------------------------- + // #endregion templateDefinition --------------------------------------------- - // #region templateDeclaration ---------------------------------------------- + // #region templateDeclaration ----------------------------------------------- @Test void testTemplateDeclaration_NoParameters() { @@ -153,9 +168,9 @@ void testTemplateDeclaration_SpecialCharactersInName() { "invoke template-with-dashes('test');"))); } - // #endregion templateDeclaration ------------------------------------------- + // #endregion templateDeclaration -------------------------------------------- - // #region templateFragment ------------------------------------------------- + // #region templateFragment -------------------------------------------------- @Test void testTemplateFragment_TextAndExpression() { @@ -223,7 +238,7 @@ void testTemplateFragment_MultipleExpressions() { translateTemplate("{BT-00-Number} ${BT-00-Number} + ${BT-00-Number} = ${BT-00-Number + BT-00-Number}")); } - // #region textBlock ------------------------------------------------------- + // #region textBlock --------------------------------------------------------- @Test void testTextBlock_SimpleText() { @@ -291,9 +306,9 @@ void testTextBlock_WithEscapedCharacters() { translateTemplate("display Text with quotes: \"' and backslash: \\\\;")); } - // #endregion textBlock ----------------------------------------------------- + // #endregion textBlock ------------------------------------------------------ - // #region linkedTextBlock -------------------------------------------------- + // #region linkedTextBlock --------------------------------------------------- @Test void testLinkedTextBlock() { @@ -328,9 +343,9 @@ void testLinkedExpressionBlock() { translateTemplate("DISPLAY here is a ${'multi word link'}@{'http://example.com'}. How about it?;")); } - // #endregion linkedTextBlock ----------------------------------------------- + // #endregion linkedTextBlock ------------------------------------------------ - // #endregion templateFragment ---------------------------------------------- + // #endregion templateFragment ----------------------------------------------- @Test void testTemplate_ComplexNesting() { @@ -346,9 +361,9 @@ void testTemplate_ComplexNesting() { "invoke complex-template('BT-00-Text');"))); } - // #endregion Core Template Structure --------------------------------------- + // #endregion Core Template Structure ---------------------------------------- - // #region Globals ---------------------------------------------------------- + // #region Globals ----------------------------------------------------------- @Test void testGlobals_VariableDeclaration() { @@ -470,7 +485,7 @@ void testGlobals_DictionaryDeclaration() { "display ${$dic['key']};"))); } - // #endregion Globals ------------------------------------------------------- + // #endregion Globals -------------------------------------------------------- @Test void testDisplayTemplate() { @@ -487,7 +502,289 @@ void testDisplayTemplate() { "display this is a ${$t};"))); } - // #region templateLine ----------------------------------------------------- + // #region Sequence variable declarations ------------------------------------ + + @Test + void testDisplayTemplate_WithTextSequenceVariable() { + assertEquals( + lines( + "GLOBALS:", + "string*:items=('a','b','c')", + "TEMPLATES:", + "let body01() -> { text('count: ')eval(count($items)) }", + "MAIN:", + "for-each(/*).call(body01())"), + translateTemplate(lines( + "let text*:$items = ('a', 'b', 'c');", + "display count: ${count($items)};"))); + } + + @Test + void testDisplayTemplate_WithNumericSequenceVariable() { + assertEquals( + lines( + "GLOBALS:", + "decimal*:nums=(1,2,3)", + "TEMPLATES:", + "let body01() -> { text('count: ')eval(count($nums)) }", + "MAIN:", + "for-each(/*).call(body01())"), + translateTemplate(lines( + "let number*:$nums = (1, 2, 3);", + "display count: ${count($nums)};"))); + } + + @Test + void testDisplayTemplate_WithBooleanSequenceVariable() { + assertEquals( + lines( + "GLOBALS:", + "boolean*:flags=(true(),false())", + "TEMPLATES:", + "let body01() -> { text('count: ')eval(count($flags)) }", + "MAIN:", + "for-each(/*).call(body01())"), + translateTemplate(lines( + "let indicator*:$flags = (TRUE, FALSE);", + "display count: ${count($flags)};"))); + } + + @Test + void testDisplayTemplate_WithDateSequenceVariable() { + assertEquals( + lines( + "GLOBALS:", + "date*:dates=(xs:date('2024-01-01Z'),xs:date('2024-12-31Z'))", + "TEMPLATES:", + "let body01() -> { text('count: ')eval(count($dates)) }", + "MAIN:", + "for-each(/*).call(body01())"), + translateTemplate(lines( + "let date*:$dates = (2024-01-01Z, 2024-12-31Z);", + "display count: ${count($dates)};"))); + } + + @Test + void testDisplayTemplate_WithTimeSequenceVariable() { + assertEquals( + lines( + "GLOBALS:", + "time*:times=(xs:time('10:00:00Z'),xs:time('18:00:00Z'))", + "TEMPLATES:", + "let body01() -> { text('count: ')eval(count($times)) }", + "MAIN:", + "for-each(/*).call(body01())"), + translateTemplate(lines( + "let time*:$times = (10:00:00Z, 18:00:00Z);", + "display count: ${count($times)};"))); + } + + @Test + void testDisplayTemplate_WithDurationSequenceVariable() { + assertEquals( + lines( + "GLOBALS:", + "duration*:durs=(xs:yearMonthDuration('P1Y'),xs:yearMonthDuration('P2M'))", + "TEMPLATES:", + "let body01() -> { text('count: ')eval(count($durs)) }", + "MAIN:", + "for-each(/*).call(body01())"), + translateTemplate(lines( + "let measure*:$durs = (P1Y, P2M);", + "display count: ${count($durs)};"))); + } + + // #endregion Sequence variable declarations --------------------------------- + + // #region Sequence function declarations ------------------------------------ + + @Test + void testDisplayTemplate_WithTextSequenceFunction() { + assertEquals( + lines( + "GLOBALS:", + "string*:getItems() -> { ('a','b','c') }", + "TEMPLATES:", + "let body01() -> { text('count: ')eval(count(udf:getItems())) }", + "MAIN:", + "for-each(/*).call(body01())"), + translateTemplate(lines( + "let text*:?getItems() = ('a', 'b', 'c');", + "display count: ${count(?getItems())};"))); + } + + @Test + void testDisplayTemplate_WithNumericSequenceFunction() { + assertEquals( + lines( + "GLOBALS:", + "decimal*:getNumbers() -> { (1,2,3) }", + "TEMPLATES:", + "let body01() -> { text('count: ')eval(count(udf:getNumbers())) }", + "MAIN:", + "for-each(/*).call(body01())"), + translateTemplate(lines( + "let number*:?getNumbers() = (1, 2, 3);", + "display count: ${count(?getNumbers())};"))); + } + + @Test + void testDisplayTemplate_WithBooleanSequenceFunction() { + assertEquals( + lines( + "GLOBALS:", + "boolean*:getFlags() -> { (true(),false()) }", + "TEMPLATES:", + "let body01() -> { text('count: ')eval(count(udf:getFlags())) }", + "MAIN:", + "for-each(/*).call(body01())"), + translateTemplate(lines( + "let indicator*:?getFlags() = (TRUE, FALSE);", + "display count: ${count(?getFlags())};"))); + } + + @Test + void testDisplayTemplate_WithDateSequenceFunction() { + assertEquals( + lines( + "GLOBALS:", + "date*:getDates() -> { (xs:date('2024-01-01Z'),xs:date('2024-12-31Z')) }", + "TEMPLATES:", + "let body01() -> { text('count: ')eval(count(udf:getDates())) }", + "MAIN:", + "for-each(/*).call(body01())"), + translateTemplate(lines( + "let date*:?getDates() = (2024-01-01Z, 2024-12-31Z);", + "display count: ${count(?getDates())};"))); + } + + @Test + void testDisplayTemplate_WithTimeSequenceFunction() { + assertEquals( + lines( + "GLOBALS:", + "time*:getTimes() -> { (xs:time('10:00:00Z'),xs:time('18:00:00Z')) }", + "TEMPLATES:", + "let body01() -> { text('count: ')eval(count(udf:getTimes())) }", + "MAIN:", + "for-each(/*).call(body01())"), + translateTemplate(lines( + "let time*:?getTimes() = (10:00:00Z, 18:00:00Z);", + "display count: ${count(?getTimes())};"))); + } + + @Test + void testDisplayTemplate_WithDurationSequenceFunction() { + assertEquals( + lines( + "GLOBALS:", + "duration*:getDurations() -> { (xs:yearMonthDuration('P1Y'),xs:yearMonthDuration('P2M')) }", + "TEMPLATES:", + "let body01() -> { text('count: ')eval(count(udf:getDurations())) }", + "MAIN:", + "for-each(/*).call(body01())"), + translateTemplate(lines( + "let measure*:?getDurations() = (P1Y, P2M);", + "display count: ${count(?getDurations())};"))); + } + + // #endregion Sequence function declarations --------------------------------- + + // #region Sequence parameter declarations ----------------------------------- + + @Test + void testDisplayTemplate_WithTextSequenceParameter() { + assertEquals( + lines( + "GLOBALS:", + "string*:processItems(string*:items) -> { $items }", + "TEMPLATES:", + "let body01() -> { text('done') }", + "MAIN:", + "for-each(/*).call(body01())"), + translateTemplate(lines( + "let text*:?processItems(text*:$items) = $items;", + "display done;"))); + } + + @Test + void testDisplayTemplate_WithNumericSequenceParameter() { + assertEquals( + lines( + "GLOBALS:", + "decimal*:processNumbers(decimal*:nums) -> { $nums }", + "TEMPLATES:", + "let body01() -> { text('done') }", + "MAIN:", + "for-each(/*).call(body01())"), + translateTemplate(lines( + "let number*:?processNumbers(number*:$nums) = $nums;", + "display done;"))); + } + + @Test + void testDisplayTemplate_WithBooleanSequenceParameter() { + assertEquals( + lines( + "GLOBALS:", + "boolean*:processFlags(boolean*:flags) -> { $flags }", + "TEMPLATES:", + "let body01() -> { text('done') }", + "MAIN:", + "for-each(/*).call(body01())"), + translateTemplate(lines( + "let indicator*:?processFlags(indicator*:$flags) = $flags;", + "display done;"))); + } + + @Test + void testDisplayTemplate_WithDateSequenceParameter() { + assertEquals( + lines( + "GLOBALS:", + "date*:processDates(date*:dates) -> { $dates }", + "TEMPLATES:", + "let body01() -> { text('done') }", + "MAIN:", + "for-each(/*).call(body01())"), + translateTemplate(lines( + "let date*:?processDates(date*:$dates) = $dates;", + "display done;"))); + } + + @Test + void testDisplayTemplate_WithTimeSequenceParameter() { + assertEquals( + lines( + "GLOBALS:", + "time*:processTimes(time*:times) -> { $times }", + "TEMPLATES:", + "let body01() -> { text('done') }", + "MAIN:", + "for-each(/*).call(body01())"), + translateTemplate(lines( + "let time*:?processTimes(time*:$times) = $times;", + "display done;"))); + } + + @Test + void testDisplayTemplate_WithDurationSequenceParameter() { + assertEquals( + lines( + "GLOBALS:", + "duration*:processDurations(duration*:durs) -> { $durs }", + "TEMPLATES:", + "let body01() -> { text('done') }", + "MAIN:", + "for-each(/*).call(body01())"), + translateTemplate(lines( + "let measure*:?processDurations(measure*:$durs) = $durs;", + "display done;"))); + } + + // #endregion Sequence parameter declarations -------------------------------- + + // #region templateLine ------------------------------------------------------ @Test void testTemplateLine_NoIndentation() { @@ -791,10 +1088,10 @@ void testTemplateLine_Indentation_DeepNesting() { " {BT-00-Text} Level 3"))); } - // #endregion templateLine ------------------------------------------------- + // #endregion templateLine --------------------------------------------------- - // #region otherSections ---------------------------------------------------- + // #region otherSections ----------------------------------------------------- @Test void testOtherSections_SummarySection() { @@ -827,9 +1124,9 @@ void testOtherSections_SummarySection() { "{BT-00-Text} Summary: ${BT-00-Text}"))); } - // #endregion otherSections ------------------------------------------------- + // #endregion otherSections -------------------------------------------------- - // #region Labels ----------------------------------------------------------- + // #region Labels ------------------------------------------------------------ @Test void testLabelBlock_StandardLabelReference() { @@ -1098,9 +1395,9 @@ void testLabelBlock_Expression_NestedExpressions() { "{/, text:$assetType='field', text:$labelType='name', text:$assetId='BT-00-Text'} #{${$assetType}|${$labelType}|${$assetId}}")); } - // #endregion Labels -------------------------------------------------------- + // #endregion Labels --------------------------------------------------------- - // #region Expression block ------------------------------------------------- + // #region Expression block -------------------------------------------------- @Test void testExpressionBlock_ShorthandFieldValueReferenceFromContextField() { @@ -1129,9 +1426,9 @@ void testExpressionBlock_ShorthandFieldValueReferenceFromContextField_WithNodeCo assertThrows(ParseCancellationException.class, () -> translateTemplate("{ND-Root} $value")); } - // #endregion Expression block ---------------------------------------------- + // #endregion Expression block ----------------------------------------------- - // #region contextDeclarationBlock ------------------------------------------ + // #region contextDeclarationBlock ------------------------------------------- @Test void testContextDeclarationBlock_ContextFieldVariable() { @@ -1183,9 +1480,31 @@ void testContextDeclarationBlock_OnlyVariables() { "{text:$var1='test', number:$var2=123, indicator:$var3=TRUE} Only vars: ${$var1}, ${$var2}, ${$var3}")); } - // #endregion contextDeclarationBlock --------------------------------------- + /** + * Context variables are always scalar even when the context field is repeatable, + * because the template iterates over values and the variable holds each iteration's value. + */ + @Test + void testContextDeclarationBlock_RepeatableFieldContextVariable_UsedAsScalar() { + String result = translateTemplate("{context:$ctx = BT-13-Number} Value plus one: ${$ctx + 1}"); + + assertTrue(result.contains("decimal:ctx"), + "Context variable should be scalar (decimal:ctx), not sequence. Actual: " + result); + assertFalse(result.contains("decimal*:ctx"), + "Context variable should NOT be typed as sequence. Actual: " + result); + + assertEquals( + lines( + "TEMPLATES:", + "let body01(decimal:ctx) -> { text('Value plus one: ')eval($ctx + 1) }", + "MAIN:", + "for-each(/*/SubNode/RepeatableInSubNode/Number).call(body01(decimal:ctx=.))"), + result); + } - // #region chooseTemplate ------------------------------------ + // #endregion contextDeclarationBlock ---------------------------------------- + + // #region chooseTemplate ---------------------------------------------------- @Test void testChooseTemplate_WhenBlock_MultipleConditions() { @@ -1296,9 +1615,9 @@ void testChooseTemplate_WhenNoOtherwiseNoContext() { "when TRUE display this is a ${$t};"))); } - // #endregion chooseTemplate --------------------------------- + // #endregion chooseTemplate ------------------------------------------------- - // #region variableList --------------------------------------------- + // #region variableList ------------------------------------------------------ @Test void testVariableList_WithAllDataTypes() { @@ -1323,9 +1642,9 @@ void testVariableList_ExpressionInitializers() { translateTemplate("{BT-00-Text, text:$computed=concat('prefix-', BT-00-Text)} Computed: ${$computed}")); } - // #endregion variableList ------------------------------------------ + // #endregion variableList --------------------------------------------------- - // #region templateLine edge cases ------------------------------------------ + // #region templateLine edge cases ------------------------------------------- @Test void testTemplateLine_OutlineNumber_Only() { @@ -1349,9 +1668,9 @@ void testTemplateLine_OutlineNumber_WithContext() { translateTemplate("1 {BT-00-Text} Value: ${BT-00-Text}")); } - // #endregion templateLine edge cases --------------------------------------- + // #endregion templateLine edge cases ---------------------------------------- - // #region InvalidIndentationException -------------------------------------- + // #region InvalidIndentationException --------------------------------------- @Test void testTemplateLine_InvalidIndentation_FirstIndentation() { @@ -1403,9 +1722,9 @@ void testTemplateDeclaration_InvalidNestedStructure() { assertThrows(InvalidIndentationException.class, () -> translateTemplate(template)); } - // #endregion InvalidIndentationException ----------------------------------- + // #endregion InvalidIndentationException ------------------------------------ - // #region TypeMismatchException -------------------------------------------- + // #region TypeMismatchException --------------------------------------------- @Test void testTemplateDefinition_InvalidParameters() { @@ -1415,9 +1734,79 @@ void testTemplateDefinition_InvalidParameters() { assertThrows(InvalidArgumentException.class, () -> translateTemplate(template)); } - // #endregion TypeMismatchException ----------------------------------------- + // #endregion TypeMismatchException ------------------------------------------ + + // #region Repeatable Fields in Expression Blocks ---------------------------- + + @Test + void testExpressionBlock_RepeatableFieldDirect() { + assertEquals( + lines( + "TEMPLATES:", + "let body01() -> { text('Value: ')eval(PathNode/RepeatableTextField/normalize-space(text())) }", + "MAIN:", + "for-each(/*).call(body01())"), + translateTemplate("{ND-Root} Value: ${BT-00-Repeatable-Text}")); + } + + @Test + void testExpressionBlock_RepeatableFieldWithForLoop() { + assertEquals( + lines( + "TEMPLATES:", + "let body01() -> { text('Value: ')eval(for $x in PathNode/RepeatableTextField/normalize-space(text()) return $x) }", + "MAIN:", + "for-each(/*).call(body01())"), + translateTemplate("{ND-Root} Value: ${for text:$x in BT-00-Repeatable-Text return $x}")); + } + + @Test + void testExpressionBlock_RepeatableFieldWithExplicitCast() { + assertEquals( + lines( + "TEMPLATES:", + "let body01() -> { text('Value: ')eval(PathNode/RepeatableTextField/normalize-space(text())) }", + "MAIN:", + "for-each(/*).call(body01())"), + translateTemplate("{ND-Root} Value: ${(text*)BT-00-Repeatable-Text}")); + } + + @Test + void testExpressionBlock_ScalarField() { + assertEquals( + lines( + "TEMPLATES:", + "let body01() -> { text('Value: ')eval(PathNode/TextField/normalize-space(text())) }", + "MAIN:", + "for-each(/*).call(body01())"), + translateTemplate("{ND-Root} Value: ${BT-00-Text}")); + } + + @Test + void testExpressionBlock_LiteralNumericSequence() { + assertEquals( + lines( + "TEMPLATES:", + "let body01() -> { text('Values: ')eval((1,2,3)) }", + "MAIN:", + "for-each(/*).call(body01())"), + translateTemplate("{ND-Root} Values: ${(1,2,3)}")); + } + + @Test + void testExpressionBlock_MixedSequenceWithField() { + assertEquals( + lines( + "TEMPLATES:", + "let body01() -> { text('Values: ')eval((1,2,PathNode/NumberField/number())) }", + "MAIN:", + "for-each(/*).call(body01())"), + translateTemplate("{ND-Root} Values: ${(1,2,BT-00-Number)}")); + } + + // #endregion Repeatable Fields in Expression Blocks ------------------------- - // #region IllegalArgumentException ----------------------------------------- + // #region IllegalArgumentException ------------------------------------------ @Test void testTemplateDefinition_InvalidTooManyParameters() { @@ -1435,7 +1824,7 @@ void testTemplateDefinition_InvalidTooFewParameters() { assertThrows(InvalidArgumentException.class, () -> translateTemplate(template)); } - // #endregion IllegalArgumentException -------------------------------------- + // #endregion IllegalArgumentException --------------------------------------- @Test void testContextualizer_WithPredicate() { diff --git a/src/test/java/eu/europa/ted/efx/sdk2/SdkSymbolResolverTest.java b/src/test/java/eu/europa/ted/efx/sdk2/SdkSymbolResolverTest.java new file mode 100644 index 00000000..70cbbb1a --- /dev/null +++ b/src/test/java/eu/europa/ted/efx/sdk2/SdkSymbolResolverTest.java @@ -0,0 +1,650 @@ +/* + * Copyright 2026 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.efx.sdk2; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import eu.europa.ted.efx.exceptions.SymbolResolutionException; +import eu.europa.ted.efx.exceptions.SymbolResolutionException.ErrorCode; +import eu.europa.ted.efx.interfaces.SymbolResolver; +import eu.europa.ted.efx.mock.sdk2.SymbolResolverMockV2; +import eu.europa.ted.efx.model.expressions.PathExpression; +import eu.europa.ted.efx.model.expressions.scalar.BooleanPath; +import eu.europa.ted.efx.model.expressions.scalar.DatePath; +import eu.europa.ted.efx.model.expressions.scalar.DurationPath; +import eu.europa.ted.efx.model.expressions.scalar.MultilingualStringPath; +import eu.europa.ted.efx.model.expressions.scalar.NodePath; +import eu.europa.ted.efx.model.expressions.scalar.NumericPath; +import eu.europa.ted.efx.model.expressions.scalar.ScalarPath; +import eu.europa.ted.efx.model.expressions.scalar.StringPath; +import eu.europa.ted.efx.model.expressions.scalar.TimePath; +import eu.europa.ted.efx.model.expressions.sequence.NodeSequencePath; +import eu.europa.ted.efx.model.expressions.sequence.SequencePath; + +/** + * Comprehensive tests for SdkSymbolResolver covering: + * 1. Exception handling for bad inputs + * 2. Repeatability logic from different contexts + * 3. PathExpression shape (ScalarPath vs SequencePath) + * 4. PathExpression data types + * 5. Context-aware relative paths + * 6. Basic lookups + * 7. Alias resolution + * 8. Root and notice type methods + */ +class SdkSymbolResolverTest { + + private static SymbolResolver resolver; + + @BeforeAll + static void setup() throws Exception { + resolver = new SymbolResolverMockV2(); + } + + // ========================================================================= + // 1. Exception Handling + // ========================================================================= + + @Nested + @DisplayName("1. Exception Handling") + class ExceptionTests { + + @Test + @DisplayName("getAbsolutePathOfField throws UNKNOWN_SYMBOL for nonexistent field") + void getAbsolutePathOfField_unknownField_throwsException() { + SymbolResolutionException ex = assertThrows( + SymbolResolutionException.class, + () -> resolver.getAbsolutePathOfField("BT-NONEXISTENT")); + + assertEquals(ErrorCode.UNKNOWN_SYMBOL, ex.getErrorCode()); + } + + @Test + @DisplayName("getAbsolutePathOfNode throws UNKNOWN_SYMBOL for nonexistent node") + void getAbsolutePathOfNode_unknownNode_throwsException() { + SymbolResolutionException ex = assertThrows( + SymbolResolutionException.class, + () -> resolver.getAbsolutePathOfNode("ND-NONEXISTENT")); + + assertEquals(ErrorCode.UNKNOWN_SYMBOL, ex.getErrorCode()); + } + + @Test + @DisplayName("expandCodelist throws UNKNOWN_CODELIST for nonexistent codelist") + void expandCodelist_unknownCodelist_throwsException() { + SymbolResolutionException ex = assertThrows( + SymbolResolutionException.class, + () -> resolver.expandCodelist("nonexistent-codelist")); + + assertEquals(ErrorCode.UNKNOWN_CODELIST, ex.getErrorCode()); + } + + @Test + @DisplayName("getRootCodelistOfField throws NO_CODELIST_FOR_FIELD for non-code field") + void getRootCodelistOfField_nonCodeField_throwsException() { + SymbolResolutionException ex = assertThrows( + SymbolResolutionException.class, + () -> resolver.getRootCodelistOfField("BT-12-Text")); + + assertEquals(ErrorCode.NO_CODELIST_FOR_FIELD, ex.getErrorCode()); + } + } + + // ========================================================================= + // 2. Repeatability Logic + // ========================================================================= + + @Nested + @DisplayName("2. Repeatability Logic") + class RepeatabilityTests { + + @Test + @DisplayName("Scalar field from root context returns false") + void isFieldRepeatableFromContext_scalarFieldFromRoot_returnsFalse() { + assertFalse(resolver.isFieldRepeatableFromContext("BT-12-Text", null), + "Field with no repeatable ancestors should return false from root"); + } + + @Test + @DisplayName("Field in self-repeatable node from root returns true") + void isFieldRepeatableFromContext_fieldInRepNode_fromRoot_returnsTrue() { + assertTrue(resolver.isFieldRepeatableFromContext("BT-13-Text", null), + "Field in repeatable node should return true from root"); + } + + @Test + @DisplayName("Field under repeatable ancestor from root returns true") + void isFieldRepeatableFromContext_fieldUnderRepAncestor_fromRoot_returnsTrue() { + assertTrue(resolver.isFieldRepeatableFromContext("BT-21-Number", null), + "Field under repeatable ancestor should return true from root"); + } + + @Test + @DisplayName("Field under repeatable ancestor from that ancestor returns false") + void isFieldRepeatableFromContext_fieldUnderRepAncestor_fromRepAncestor_returnsFalse() { + assertFalse(resolver.isFieldRepeatableFromContext("BT-21-Number", "ND-RepeatableNode"), + "Walk should stop at context node, returning false"); + } + + @Test + @DisplayName("Deeply nested field with inner repeatable node from outer context returns true") + void isFieldRepeatableFromContext_deeplyNested_fromOuterRepContext_returnsTrue() { + assertTrue(resolver.isFieldRepeatableFromContext("BT-25-Date", "ND-RepeatableNode"), + "Should still return true because inner repeatable node is crossed"); + } + + @Test + @DisplayName("Scalar field from cross-branch repeatable context stays scalar") + void isFieldRepeatableFromContext_scalarField_fromCrossBranchRepContext_returnsFalse() { + assertFalse(resolver.isFieldRepeatableFromContext("BT-12-Text", "ND-RepeatableNode"), + "Scalar field should remain scalar even from rep context on different branch"); + } + + @Test + @DisplayName("Sequence field from cross-branch non-repeatable context stays sequence") + void isFieldRepeatableFromContext_sequenceField_fromCrossBranchNonRepContext_returnsTrue() { + assertTrue(resolver.isFieldRepeatableFromContext("BT-21-Number", "ND-SubNode"), + "Sequence field should remain sequence from non-rep context on different branch"); + } + + @Test + @DisplayName("Attribute field in repeatable node from root returns true") + void isFieldRepeatableFromContext_attributeInRepNode_fromRoot_returnsTrue() { + assertTrue(resolver.isFieldRepeatableFromContext("BT-00-Attribute-In-Repeatable-Node", null), + "Attribute field in repeatable node should return true from root"); + } + + @Test + @DisplayName("Attribute field in repeatable node from that node returns false") + void isFieldRepeatableFromContext_attributeInRepNode_fromRepNode_returnsFalse() { + assertFalse(resolver.isFieldRepeatableFromContext("BT-00-Attribute-In-Repeatable-Node", "ND-RepeatableNode"), + "Attribute field should not be repeatable when context is its parent repeatable node"); + } + } + + // ========================================================================= + // 3. PathExpression Shape Tests + // ========================================================================= + + @Nested + @DisplayName("3. PathExpression Shape Tests") + class PathExpressionShapeTests { + + @Test + @DisplayName("Non-repeatable field returns ScalarPath") + void nonRepeatableField_shouldReturnScalarPath() { + assertFalse(resolver.isFieldRepeatableFromContext("BT-00-Text", null), + "Precondition: field should NOT be repeatable from root"); + + PathExpression path = resolver.getAbsolutePathOfField("BT-00-Text"); + + assertTrue(path instanceof ScalarPath, + "Non-repeatable field should return ScalarPath, got: " + path.getClass().getSimpleName()); + } + + @Test + @DisplayName("Repeatable field returns SequencePath") + void repeatableField_shouldReturnSequencePath() { + assertTrue(resolver.isFieldRepeatableFromContext("BT-00-Repeatable-Text", null), + "Precondition: field should be repeatable from root"); + + PathExpression path = resolver.getAbsolutePathOfField("BT-00-Repeatable-Text"); + + assertTrue(path instanceof SequencePath, + "Repeatable field should return SequencePath, got: " + path.getClass().getSimpleName()); + } + + @Test + @DisplayName("Field in repeatable node returns SequencePath") + void fieldInRepeatableNode_shouldReturnSequencePath() { + assertTrue(resolver.isFieldRepeatableFromContext("BT-00-Text-In-Repeatable-Node", null), + "Precondition: field in repeatable node should be repeatable from root"); + + PathExpression path = resolver.getAbsolutePathOfField("BT-00-Text-In-Repeatable-Node"); + + assertTrue(path instanceof SequencePath, + "Field in repeatable node should return SequencePath, got: " + path.getClass().getSimpleName()); + } + + @Test + @DisplayName("Field in non-repeatable subnode of repeatable node returns SequencePath") + void fieldInNonRepeatableSubNode_shouldReturnSequencePath() { + assertTrue(resolver.isFieldRepeatableFromContext("BT-00-Text-In-NonRepeatableSubNode", null), + "Precondition: field under repeatable ancestor should be repeatable from root"); + + PathExpression path = resolver.getAbsolutePathOfField("BT-00-Text-In-NonRepeatableSubNode"); + + assertTrue(path instanceof SequencePath, + "Field under repeatable ancestor should return SequencePath, got: " + path.getClass().getSimpleName()); + } + + @Test + @DisplayName("Field in double-repeatable context returns SequencePath") + void fieldInDoubleRepeatableContext_shouldReturnSequencePath() { + assertTrue(resolver.isFieldRepeatableFromContext("BT-00-Text-In-RepeatableSubSubNode", null), + "Precondition: field in double-repeatable context should be repeatable from root"); + + PathExpression path = resolver.getAbsolutePathOfField("BT-00-Text-In-RepeatableSubSubNode"); + + assertTrue(path instanceof SequencePath, + "Field in double-repeatable context should return SequencePath, got: " + path.getClass().getSimpleName()); + } + + @Test + @DisplayName("Non-repeatable root node returns NodePath (scalar)") + void nonRepeatableRootNode_shouldReturnScalarPath() { + PathExpression path = resolver.getAbsolutePathOfNode("ND-Root"); + + assertTrue(path instanceof ScalarPath, + "Non-repeatable node should return ScalarPath, got: " + path.getClass().getSimpleName()); + assertEquals(NodePath.class, path.getClass(), + "Non-repeatable node should return NodePath"); + } + + @Test + @DisplayName("Repeatable node returns SequencePath") + void repeatableNode_shouldReturnSequencePath() { + PathExpression path = resolver.getAbsolutePathOfNode("ND-RepeatableNode"); + + assertTrue(path instanceof SequencePath, + "Repeatable node should return SequencePath, got: " + path.getClass().getSimpleName()); + } + + @Test + @DisplayName("Non-repeatable subnode under repeatable parent returns SequencePath") + void nonRepeatableSubNode_underRepeatableParent_shouldReturnSequencePath() { + assertTrue(resolver.isNodeRepeatableFromContext("ND-NonRepeatableSubNode", null), + "Precondition: node under repeatable ancestor should be repeatable from root"); + + PathExpression path = resolver.getAbsolutePathOfNode("ND-NonRepeatableSubNode"); + + assertTrue(path instanceof SequencePath, + "Node under repeatable ancestor should return SequencePath, got: " + path.getClass().getSimpleName()); + } + + @Test + @DisplayName("Repeatable subsubnode returns SequencePath") + void repeatableSubSubNode_shouldReturnSequencePath() { + PathExpression path = resolver.getAbsolutePathOfNode("ND-RepeatableSubSubNode"); + + assertTrue(path instanceof SequencePath, + "Repeatable subsubnode should return SequencePath, got: " + path.getClass().getSimpleName()); + } + + @Test + @DisplayName("Non-repeatable attribute field without attribute returns NodePath") + void nonRepeatableAttributeField_withoutAttribute_shouldReturnNodePath() { + assertFalse(resolver.isFieldRepeatableFromContext("BT-00-Attribute", null), + "Precondition: attribute field should NOT be repeatable from root"); + + PathExpression path = resolver.getAbsolutePathOfFieldWithoutTheAttribute("BT-00-Attribute"); + + assertTrue(path instanceof NodePath, + "Non-repeatable attribute field without @attribute should return NodePath, got: " + path.getClass().getSimpleName()); + } + + @Test + @DisplayName("Attribute field in repeatable node without attribute returns NodeSequencePath") + void attributeInRepeatableNode_withoutAttribute_shouldReturnNodeSequencePath() { + assertTrue(resolver.isFieldRepeatableFromContext("BT-00-Attribute-In-Repeatable-Node", null), + "Precondition: attribute field in repeatable node should be repeatable from root"); + + PathExpression path = resolver.getAbsolutePathOfFieldWithoutTheAttribute("BT-00-Attribute-In-Repeatable-Node"); + + assertTrue(path instanceof NodeSequencePath, + "Attribute field in repeatable node without @attribute should return NodeSequencePath, got: " + path.getClass().getSimpleName()); + } + } + + // ========================================================================= + // 4. PathExpression Data Type Tests + // ========================================================================= + + @Nested + @DisplayName("4. PathExpression Data Type Tests") + class PathExpressionDataTypeTests { + + @Test + @DisplayName("Text field returns StringPath") + void textField_shouldReturnStringPath() { + PathExpression path = resolver.getAbsolutePathOfField("BT-00-Text"); + assertEquals(StringPath.class, path.getClass(), + "Text field should return StringPath"); + } + + @Test + @DisplayName("Indicator field returns BooleanPath") + void indicatorField_shouldReturnBooleanPath() { + PathExpression path = resolver.getAbsolutePathOfField("BT-00-Indicator"); + assertEquals(BooleanPath.class, path.getClass(), + "Indicator field should return BooleanPath"); + } + + @Test + @DisplayName("Code field returns StringPath") + void codeField_shouldReturnStringPath() { + PathExpression path = resolver.getAbsolutePathOfField("BT-00-Code"); + assertEquals(StringPath.class, path.getClass(), + "Code field should return StringPath"); + } + + @Test + @DisplayName("Multilingual field returns MultilingualStringPath") + void multilingualField_shouldReturnMultilingualStringPath() { + PathExpression path = resolver.getAbsolutePathOfField("BT-00-Text-Multilingual"); + assertEquals(MultilingualStringPath.class, path.getClass(), + "Multilingual field should return MultilingualStringPath"); + } + + @Test + @DisplayName("Date field returns DatePath") + void dateField_shouldReturnDatePath() { + PathExpression path = resolver.getAbsolutePathOfField("BT-00-StartDate"); + assertEquals(DatePath.class, path.getClass(), + "Date field should return DatePath"); + } + + @Test + @DisplayName("Time field returns TimePath") + void timeField_shouldReturnTimePath() { + PathExpression path = resolver.getAbsolutePathOfField("BT-00-StartTime"); + assertEquals(TimePath.class, path.getClass(), + "Time field should return TimePath"); + } + + @Test + @DisplayName("Measure field returns DurationPath") + void measureField_shouldReturnDurationPath() { + PathExpression path = resolver.getAbsolutePathOfField("BT-00-Measure"); + assertEquals(DurationPath.class, path.getClass(), + "Measure field should return DurationPath"); + } + + @Test + @DisplayName("Integer field returns NumericPath") + void integerField_shouldReturnNumericPath() { + PathExpression path = resolver.getAbsolutePathOfField("BT-00-Integer"); + assertEquals(NumericPath.class, path.getClass(), + "Integer field should return NumericPath"); + } + + @Test + @DisplayName("Amount field returns NumericPath") + void amountField_shouldReturnNumericPath() { + PathExpression path = resolver.getAbsolutePathOfField("BT-00-Amount"); + assertEquals(NumericPath.class, path.getClass(), + "Amount field should return NumericPath"); + } + + @Test + @DisplayName("Number field returns NumericPath") + void numberField_shouldReturnNumericPath() { + PathExpression path = resolver.getAbsolutePathOfField("BT-00-Number"); + assertEquals(NumericPath.class, path.getClass(), + "Number field should return NumericPath"); + } + } + + // ========================================================================= + // 5. Context-Aware Relative Paths + // ========================================================================= + + @Nested + @DisplayName("5. Context-Aware Relative Paths") + class RelativePathTests { + + @Test + @DisplayName("Field in repeatable node from root returns SequencePath") + void fromRoot_repeatableField_shouldReturnSequencePath() { + assertTrue(resolver.isFieldRepeatableFromContext("BT-00-Text-In-Repeatable-Node", null), + "Precondition: field should be repeatable from root"); + + PathExpression path = resolver.getRelativePathOfField("BT-00-Text-In-Repeatable-Node", "ND-Root"); + + assertTrue(path instanceof SequencePath, + "Field in repeatable node from root should return SequencePath, got: " + path.getClass().getSimpleName()); + } + + @Test + @DisplayName("Field in repeatable node from same node context returns ScalarPath") + void fromRepeatableNode_sameField_shouldReturnScalarPath() { + assertFalse(resolver.isFieldRepeatableFromContext("BT-00-Text-In-Repeatable-Node", "ND-RepeatableNode"), + "Precondition: field should NOT be repeatable from its own parent node context"); + + PathExpression path = resolver.getRelativePathOfField("BT-00-Text-In-Repeatable-Node", "ND-RepeatableNode"); + + assertTrue(path instanceof ScalarPath, + "Field from its parent repeatable node context should return ScalarPath, got: " + path.getClass().getSimpleName()); + } + + @Test + @DisplayName("Non-repeatable field from root returns ScalarPath") + void fromRoot_nonRepeatableField_shouldReturnScalarPath() { + assertFalse(resolver.isFieldRepeatableFromContext("BT-00-Text", null), + "Precondition: field should NOT be repeatable"); + + PathExpression path = resolver.getRelativePathOfField("BT-00-Text", "ND-Root"); + + assertTrue(path instanceof ScalarPath, + "Non-repeatable field should return ScalarPath, got: " + path.getClass().getSimpleName()); + } + + @Test + @DisplayName("Repeatable node from root returns SequencePath") + void fromRoot_repeatableNode_shouldReturnSequencePath() { + PathExpression path = resolver.getRelativePathOfNode("ND-RepeatableNode", "ND-Root"); + + assertTrue(path instanceof SequencePath, + "Repeatable node from root should return SequencePath, got: " + path.getClass().getSimpleName()); + } + + @Test + @DisplayName("Non-repeatable subnode from repeatable parent returns ScalarPath") + void fromRepeatableNode_nonRepeatableSubNode_shouldReturnScalarPath() { + PathExpression path = resolver.getRelativePathOfNode("ND-NonRepeatableSubNode", "ND-RepeatableNode"); + + assertTrue(path instanceof ScalarPath, + "Non-repeatable subnode from parent context should return ScalarPath, got: " + path.getClass().getSimpleName()); + } + + @Test + @DisplayName("Field relative to another field with repeatable ancestor returns SequencePath") + void fieldToField_withRepeatableAncestor_shouldReturnSequencePath() { + // BT-21-Number is in ND-NonRepeatableSubNode, whose parent ND-RepeatableNode is repeatable + // BT-00-Text is in ND-Root + // Walking up from field: ND-NonRepeatableSubNode (not rep) → ND-RepeatableNode (rep!) → sequence + PathExpression path = resolver.getRelativePathOfField("BT-21-Number", "BT-00-Text"); + + assertTrue(path instanceof SequencePath, + "Field with repeatable ancestor relative to field in root should return SequencePath, got: " + path.getClass().getSimpleName()); + } + + @Test + @DisplayName("Field relative to sibling field in same node returns ScalarPath") + void fieldToField_siblingInSameNode_shouldReturnScalarPath() { + // Both BT-21-Number and BT-21-TextMultilingual are in ND-NonRepeatableSubNode + // Context ancestry includes ND-NonRepeatableSubNode, so walk stops immediately + PathExpression path = resolver.getRelativePathOfField("BT-21-Number", "BT-21-TextMultilingual"); + + assertTrue(path instanceof ScalarPath, + "Field relative to sibling field in same node should return ScalarPath, got: " + path.getClass().getSimpleName()); + } + } + + // ========================================================================= + // 6. Basic Lookups + // ========================================================================= + + @Nested + @DisplayName("6. Basic Lookups") + class BasicLookupTests { + + @Test + @DisplayName("getTypeOfField returns correct type for text field") + void getTypeOfField_textField_returnsText() { + assertEquals("text", resolver.getTypeOfField("BT-12-Text")); + } + + @Test + @DisplayName("getTypeOfField returns correct type for number field") + void getTypeOfField_numberField_returnsNumber() { + assertEquals("number", resolver.getTypeOfField("BT-21-Number")); + } + + @Test + @DisplayName("getParentNodeOfField returns correct parent node") + void getParentNodeOfField_returnsCorrectParent() { + assertEquals("ND-SubSubNode2", resolver.getParentNodeOfField("BT-12-Text")); + } + + @Test + @DisplayName("isAttributeField returns true for attribute field") + void isAttributeField_attributeField_returnsTrue() { + assertTrue(resolver.isAttributeField("BT-00-Attribute"), + "Attribute field should return true"); + } + + @Test + @DisplayName("isAttributeField returns false for element field") + void isAttributeField_elementField_returnsFalse() { + assertFalse(resolver.isAttributeField("BT-12-Text"), + "Element field should return false"); + } + + @Test + @DisplayName("getAttributeNameFromAttributeField returns attribute name") + void getAttributeNameFromAttributeField_returnsAttributeName() { + String attrName = resolver.getAttributeNameFromAttributeField("BT-00-Attribute"); + assertNotNull(attrName, "Should return attribute name"); + assertFalse(attrName.isEmpty(), "Attribute name should not be empty"); + } + + @Test + @DisplayName("expandCodelist returns non-empty list for valid codelist") + void expandCodelist_validCodelist_returnsNonEmptyList() { + var codes = resolver.expandCodelist("accessibility"); + assertFalse(codes.isEmpty(), "Codelist should have at least one code"); + } + + @Test + @DisplayName("getRootCodelistOfField returns root codelist for code field") + void getRootCodelistOfField_codeField_returnsRootCodelist() { + String rootCodelist = resolver.getRootCodelistOfField("BT-00-Code"); + assertNotNull(rootCodelist, "Should return root codelist"); + assertFalse(rootCodelist.isEmpty(), "Root codelist should not be empty"); + } + + @Test + @DisplayName("Attribute field without attribute returns NodePath") + void getAbsolutePathOfFieldWithoutTheAttribute_returnsNodePath() { + PathExpression path = resolver.getAbsolutePathOfFieldWithoutTheAttribute("BT-00-Attribute"); + + assertEquals(NodePath.class, path.getClass(), + "Attribute field without attribute should return NodePath"); + } + + @Test + @DisplayName("Code attribute field without attribute returns NodePath") + void getAbsolutePathOfFieldWithoutTheAttribute_codeAttribute_returnsNodePath() { + PathExpression path = resolver.getAbsolutePathOfFieldWithoutTheAttribute("BT-00-CodeAttribute"); + + assertEquals(NodePath.class, path.getClass(), + "Code attribute field without attribute should return NodePath"); + } + } + + // ========================================================================= + // 7. Alias Resolution + // ========================================================================= + + @Nested + @DisplayName("7. Alias Resolution") + class AliasResolutionTests { + + @Test + @DisplayName("getFieldIdFromAlias returns field ID for valid alias") + void getFieldIdFromAlias_validAlias_returnsFieldId() { + String fieldId = resolver.getFieldIdFromAlias("textField"); + assertEquals("BT-00-Text", fieldId, "Should return field ID for alias 'textField'"); + } + + @Test + @DisplayName("getNodeIdFromAlias returns node ID for valid alias") + void getNodeIdFromAlias_validAlias_returnsNodeId() { + String nodeId = resolver.getNodeIdFromAlias("Root"); + assertEquals("ND-Root", nodeId, "Should return node ID for alias 'Root'"); + } + + @Test + @DisplayName("getNodeIdFromAlias returns null for unknown alias") + void getNodeIdFromAlias_unknownAlias_returnsNull() { + assertNull(resolver.getNodeIdFromAlias("nonexistent-alias"), + "Unknown alias should return null"); + } + + @Test + @DisplayName("getFieldIdFromAlias returns null for unknown alias") + void getFieldIdFromAlias_unknownAlias_returnsNull() { + assertNull(resolver.getFieldIdFromAlias("nonexistent-alias"), + "Unknown alias should return null to enable fallback"); + } + } + + // ========================================================================= + // 8. Root and Notice Type Methods + // ========================================================================= + + @Nested + @DisplayName("8. Root and Notice Type Methods") + class RootAndNoticeTypeTests { + + @Test + @DisplayName("getRootNodeId returns root node ID") + void getRootNodeId_returnsRootNodeId() { + String rootNodeId = resolver.getRootNodeId(); + assertEquals("ND-Root", rootNodeId, "Should return ND-Root"); + } + + @Test + @DisplayName("getRootPath returns NodePath to root") + void getRootPath_returnsNodePathToRoot() { + PathExpression rootPath = resolver.getRootPath(); + + assertNotNull(rootPath, "Should return root path"); + assertEquals(NodePath.class, rootPath.getClass(), + "Root path should be NodePath"); + } + + @Test + @DisplayName("getAllNoticeSubtypeIds returns list of notice types") + void getAllNoticeSubtypeIds_returnsNoticeTypes() { + List noticeTypes = resolver.getAllNoticeSubtypeIds(); + + assertNotNull(noticeTypes, "Should return list of notice types"); + assertFalse(noticeTypes.isEmpty(), "Notice types list should not be empty"); + } + } +} diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testContextVariable_RepeatableField_UsedAsScalar/dynamic/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testContextVariable_RepeatableField_UsedAsScalar/dynamic/complete-validation.sch new file mode 100644 index 00000000..e77e22fe --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testContextVariable_RepeatableField_UsedAsScalar/dynamic/complete-validation.sch @@ -0,0 +1,30 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testContextVariable_RepeatableField_UsedAsScalar/dynamic/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testContextVariable_RepeatableField_UsedAsScalar/dynamic/validation-stage-1a-1.sch new file mode 100644 index 00000000..bcc45f2f --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testContextVariable_RepeatableField_UsedAsScalar/dynamic/validation-stage-1a-1.sch @@ -0,0 +1,7 @@ + + + + + rule|text|R-CTX-001 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testContextVariable_RepeatableField_UsedAsScalar/dynamic/validation-stage-1a-2.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testContextVariable_RepeatableField_UsedAsScalar/dynamic/validation-stage-1a-2.sch new file mode 100644 index 00000000..0595d7d9 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testContextVariable_RepeatableField_UsedAsScalar/dynamic/validation-stage-1a-2.sch @@ -0,0 +1,7 @@ + + + + + rule|text|R-CTX-001 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testContextVariable_RepeatableField_UsedAsScalar/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testContextVariable_RepeatableField_UsedAsScalar/input.efx new file mode 100644 index 00000000..06710d15 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testContextVariable_RepeatableField_UsedAsScalar/input.efx @@ -0,0 +1,6 @@ +---- STAGE 1a ---- + +WITH context : $ctx = BT-13-Number +ASSERT $ctx + 1 > 0 +AS ERROR R-CTX-001 +FOR BT-13-Number IN 1, 2 diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testContextVariable_RepeatableField_UsedAsScalar/schematrons.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testContextVariable_RepeatableField_UsedAsScalar/schematrons.json new file mode 100644 index 00000000..2bd84096 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testContextVariable_RepeatableField_UsedAsScalar/schematrons.json @@ -0,0 +1,31 @@ +{ + "schematrons" : [ { + "name" : "complete-validation", + "type" : "dynamic", + "filename" : "dynamic/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-1a-2", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-2.sch" + }, { + "name" : "complete-validation", + "type" : "static", + "filename" : "static/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-1a-2", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-2.sch" + } ] +} diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testContextVariable_RepeatableField_UsedAsScalar/static/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testContextVariable_RepeatableField_UsedAsScalar/static/complete-validation.sch new file mode 100644 index 00000000..e77e22fe --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testContextVariable_RepeatableField_UsedAsScalar/static/complete-validation.sch @@ -0,0 +1,30 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testContextVariable_RepeatableField_UsedAsScalar/static/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testContextVariable_RepeatableField_UsedAsScalar/static/validation-stage-1a-1.sch new file mode 100644 index 00000000..bcc45f2f --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testContextVariable_RepeatableField_UsedAsScalar/static/validation-stage-1a-1.sch @@ -0,0 +1,7 @@ + + + + + rule|text|R-CTX-001 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testContextVariable_RepeatableField_UsedAsScalar/static/validation-stage-1a-2.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testContextVariable_RepeatableField_UsedAsScalar/static/validation-stage-1a-2.sch new file mode 100644 index 00000000..0595d7d9 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testContextVariable_RepeatableField_UsedAsScalar/static/validation-stage-1a-2.sch @@ -0,0 +1,7 @@ + + + + + rule|text|R-CTX-001 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_BooleanSequence_GlobalLevel/dynamic/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_BooleanSequence_GlobalLevel/dynamic/complete-validation.sch new file mode 100644 index 00000000..4be5377d --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_BooleanSequence_GlobalLevel/dynamic/complete-validation.sch @@ -0,0 +1,31 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_BooleanSequence_GlobalLevel/dynamic/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_BooleanSequence_GlobalLevel/dynamic/validation-stage-1a-1.sch new file mode 100644 index 00000000..77686d2e --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_BooleanSequence_GlobalLevel/dynamic/validation-stage-1a-1.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-SEQ-003 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_BooleanSequence_GlobalLevel/dynamic/validation-stage-1a-2.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_BooleanSequence_GlobalLevel/dynamic/validation-stage-1a-2.sch new file mode 100644 index 00000000..10b44766 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_BooleanSequence_GlobalLevel/dynamic/validation-stage-1a-2.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-SEQ-003 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_BooleanSequence_GlobalLevel/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_BooleanSequence_GlobalLevel/input.efx new file mode 100644 index 00000000..677d8d65 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_BooleanSequence_GlobalLevel/input.efx @@ -0,0 +1,8 @@ +LET indicator* : $flags = (TRUE, FALSE, TRUE); + +---- STAGE 1a ---- + +WITH BT-00-Text +ASSERT count($flags) > 0 +AS ERROR R-SEQ-003 +FOR BT-00-Text IN 1, 2 diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_BooleanSequence_GlobalLevel/schematrons.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_BooleanSequence_GlobalLevel/schematrons.json new file mode 100644 index 00000000..2bd84096 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_BooleanSequence_GlobalLevel/schematrons.json @@ -0,0 +1,31 @@ +{ + "schematrons" : [ { + "name" : "complete-validation", + "type" : "dynamic", + "filename" : "dynamic/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-1a-2", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-2.sch" + }, { + "name" : "complete-validation", + "type" : "static", + "filename" : "static/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-1a-2", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-2.sch" + } ] +} diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_BooleanSequence_GlobalLevel/static/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_BooleanSequence_GlobalLevel/static/complete-validation.sch new file mode 100644 index 00000000..4be5377d --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_BooleanSequence_GlobalLevel/static/complete-validation.sch @@ -0,0 +1,31 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_BooleanSequence_GlobalLevel/static/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_BooleanSequence_GlobalLevel/static/validation-stage-1a-1.sch new file mode 100644 index 00000000..77686d2e --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_BooleanSequence_GlobalLevel/static/validation-stage-1a-1.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-SEQ-003 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_BooleanSequence_GlobalLevel/static/validation-stage-1a-2.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_BooleanSequence_GlobalLevel/static/validation-stage-1a-2.sch new file mode 100644 index 00000000..10b44766 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_BooleanSequence_GlobalLevel/static/validation-stage-1a-2.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-SEQ-003 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DateSequence_GlobalLevel/dynamic/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DateSequence_GlobalLevel/dynamic/complete-validation.sch new file mode 100644 index 00000000..a8589a00 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DateSequence_GlobalLevel/dynamic/complete-validation.sch @@ -0,0 +1,31 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DateSequence_GlobalLevel/dynamic/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DateSequence_GlobalLevel/dynamic/validation-stage-1a-1.sch new file mode 100644 index 00000000..4b264b26 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DateSequence_GlobalLevel/dynamic/validation-stage-1a-1.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-SEQ-004 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DateSequence_GlobalLevel/dynamic/validation-stage-1a-2.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DateSequence_GlobalLevel/dynamic/validation-stage-1a-2.sch new file mode 100644 index 00000000..8a5a7306 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DateSequence_GlobalLevel/dynamic/validation-stage-1a-2.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-SEQ-004 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DateSequence_GlobalLevel/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DateSequence_GlobalLevel/input.efx new file mode 100644 index 00000000..d2382c4e --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DateSequence_GlobalLevel/input.efx @@ -0,0 +1,8 @@ +LET date* : $dates = (2024-01-01Z, 2024-06-01Z); + +---- STAGE 1a ---- + +WITH BT-00-Text +ASSERT count($dates) > 0 +AS ERROR R-SEQ-004 +FOR BT-00-Text IN 1, 2 diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DateSequence_GlobalLevel/schematrons.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DateSequence_GlobalLevel/schematrons.json new file mode 100644 index 00000000..2bd84096 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DateSequence_GlobalLevel/schematrons.json @@ -0,0 +1,31 @@ +{ + "schematrons" : [ { + "name" : "complete-validation", + "type" : "dynamic", + "filename" : "dynamic/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-1a-2", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-2.sch" + }, { + "name" : "complete-validation", + "type" : "static", + "filename" : "static/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-1a-2", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-2.sch" + } ] +} diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DateSequence_GlobalLevel/static/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DateSequence_GlobalLevel/static/complete-validation.sch new file mode 100644 index 00000000..a8589a00 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DateSequence_GlobalLevel/static/complete-validation.sch @@ -0,0 +1,31 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DateSequence_GlobalLevel/static/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DateSequence_GlobalLevel/static/validation-stage-1a-1.sch new file mode 100644 index 00000000..4b264b26 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DateSequence_GlobalLevel/static/validation-stage-1a-1.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-SEQ-004 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DateSequence_GlobalLevel/static/validation-stage-1a-2.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DateSequence_GlobalLevel/static/validation-stage-1a-2.sch new file mode 100644 index 00000000..8a5a7306 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DateSequence_GlobalLevel/static/validation-stage-1a-2.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-SEQ-004 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DurationSequence_GlobalLevel/dynamic/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DurationSequence_GlobalLevel/dynamic/complete-validation.sch new file mode 100644 index 00000000..99118255 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DurationSequence_GlobalLevel/dynamic/complete-validation.sch @@ -0,0 +1,31 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DurationSequence_GlobalLevel/dynamic/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DurationSequence_GlobalLevel/dynamic/validation-stage-1a-1.sch new file mode 100644 index 00000000..ea49c8b6 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DurationSequence_GlobalLevel/dynamic/validation-stage-1a-1.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-SEQ-006 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DurationSequence_GlobalLevel/dynamic/validation-stage-1a-2.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DurationSequence_GlobalLevel/dynamic/validation-stage-1a-2.sch new file mode 100644 index 00000000..22d604f4 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DurationSequence_GlobalLevel/dynamic/validation-stage-1a-2.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-SEQ-006 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DurationSequence_GlobalLevel/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DurationSequence_GlobalLevel/input.efx new file mode 100644 index 00000000..6d911e90 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DurationSequence_GlobalLevel/input.efx @@ -0,0 +1,8 @@ +LET measure* : $durations = (P1D, P2D); + +---- STAGE 1a ---- + +WITH BT-00-Text +ASSERT count($durations) > 0 +AS ERROR R-SEQ-006 +FOR BT-00-Text IN 1, 2 diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DurationSequence_GlobalLevel/schematrons.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DurationSequence_GlobalLevel/schematrons.json new file mode 100644 index 00000000..2bd84096 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DurationSequence_GlobalLevel/schematrons.json @@ -0,0 +1,31 @@ +{ + "schematrons" : [ { + "name" : "complete-validation", + "type" : "dynamic", + "filename" : "dynamic/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-1a-2", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-2.sch" + }, { + "name" : "complete-validation", + "type" : "static", + "filename" : "static/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-1a-2", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-2.sch" + } ] +} diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DurationSequence_GlobalLevel/static/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DurationSequence_GlobalLevel/static/complete-validation.sch new file mode 100644 index 00000000..99118255 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DurationSequence_GlobalLevel/static/complete-validation.sch @@ -0,0 +1,31 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DurationSequence_GlobalLevel/static/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DurationSequence_GlobalLevel/static/validation-stage-1a-1.sch new file mode 100644 index 00000000..ea49c8b6 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DurationSequence_GlobalLevel/static/validation-stage-1a-1.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-SEQ-006 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DurationSequence_GlobalLevel/static/validation-stage-1a-2.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DurationSequence_GlobalLevel/static/validation-stage-1a-2.sch new file mode 100644 index 00000000..22d604f4 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_DurationSequence_GlobalLevel/static/validation-stage-1a-2.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-SEQ-006 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_NumericSequence_GlobalLevel/dynamic/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_NumericSequence_GlobalLevel/dynamic/complete-validation.sch new file mode 100644 index 00000000..d1b9c782 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_NumericSequence_GlobalLevel/dynamic/complete-validation.sch @@ -0,0 +1,31 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_NumericSequence_GlobalLevel/dynamic/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_NumericSequence_GlobalLevel/dynamic/validation-stage-1a-1.sch new file mode 100644 index 00000000..d492aee4 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_NumericSequence_GlobalLevel/dynamic/validation-stage-1a-1.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-SEQ-002 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_NumericSequence_GlobalLevel/dynamic/validation-stage-1a-2.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_NumericSequence_GlobalLevel/dynamic/validation-stage-1a-2.sch new file mode 100644 index 00000000..789b4033 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_NumericSequence_GlobalLevel/dynamic/validation-stage-1a-2.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-SEQ-002 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_NumericSequence_GlobalLevel/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_NumericSequence_GlobalLevel/input.efx new file mode 100644 index 00000000..c29f5f54 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_NumericSequence_GlobalLevel/input.efx @@ -0,0 +1,8 @@ +LET number* : $numbers = (1, 2, 3); + +---- STAGE 1a ---- + +WITH BT-00-Text +ASSERT count($numbers) > 0 +AS ERROR R-SEQ-002 +FOR BT-00-Text IN 1, 2 diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_NumericSequence_GlobalLevel/schematrons.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_NumericSequence_GlobalLevel/schematrons.json new file mode 100644 index 00000000..2bd84096 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_NumericSequence_GlobalLevel/schematrons.json @@ -0,0 +1,31 @@ +{ + "schematrons" : [ { + "name" : "complete-validation", + "type" : "dynamic", + "filename" : "dynamic/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-1a-2", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-2.sch" + }, { + "name" : "complete-validation", + "type" : "static", + "filename" : "static/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-1a-2", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-2.sch" + } ] +} diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_NumericSequence_GlobalLevel/static/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_NumericSequence_GlobalLevel/static/complete-validation.sch new file mode 100644 index 00000000..d1b9c782 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_NumericSequence_GlobalLevel/static/complete-validation.sch @@ -0,0 +1,31 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_NumericSequence_GlobalLevel/static/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_NumericSequence_GlobalLevel/static/validation-stage-1a-1.sch new file mode 100644 index 00000000..d492aee4 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_NumericSequence_GlobalLevel/static/validation-stage-1a-1.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-SEQ-002 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_NumericSequence_GlobalLevel/static/validation-stage-1a-2.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_NumericSequence_GlobalLevel/static/validation-stage-1a-2.sch new file mode 100644 index 00000000..789b4033 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_NumericSequence_GlobalLevel/static/validation-stage-1a-2.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-SEQ-002 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TextSequence_GlobalLevel/dynamic/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TextSequence_GlobalLevel/dynamic/complete-validation.sch new file mode 100644 index 00000000..5763fecc --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TextSequence_GlobalLevel/dynamic/complete-validation.sch @@ -0,0 +1,31 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TextSequence_GlobalLevel/dynamic/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TextSequence_GlobalLevel/dynamic/validation-stage-1a-1.sch new file mode 100644 index 00000000..85a74e93 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TextSequence_GlobalLevel/dynamic/validation-stage-1a-1.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-SEQ-001 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TextSequence_GlobalLevel/dynamic/validation-stage-1a-2.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TextSequence_GlobalLevel/dynamic/validation-stage-1a-2.sch new file mode 100644 index 00000000..e01442ff --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TextSequence_GlobalLevel/dynamic/validation-stage-1a-2.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-SEQ-001 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TextSequence_GlobalLevel/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TextSequence_GlobalLevel/input.efx new file mode 100644 index 00000000..bea6cfac --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TextSequence_GlobalLevel/input.efx @@ -0,0 +1,8 @@ +LET text* : $languages = ('EN', 'FR', 'DE'); + +---- STAGE 1a ---- + +WITH BT-00-Text +ASSERT count($languages) > 0 +AS ERROR R-SEQ-001 +FOR BT-00-Text IN 1, 2 diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TextSequence_GlobalLevel/schematrons.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TextSequence_GlobalLevel/schematrons.json new file mode 100644 index 00000000..2bd84096 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TextSequence_GlobalLevel/schematrons.json @@ -0,0 +1,31 @@ +{ + "schematrons" : [ { + "name" : "complete-validation", + "type" : "dynamic", + "filename" : "dynamic/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-1a-2", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-2.sch" + }, { + "name" : "complete-validation", + "type" : "static", + "filename" : "static/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-1a-2", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-2.sch" + } ] +} diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TextSequence_GlobalLevel/static/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TextSequence_GlobalLevel/static/complete-validation.sch new file mode 100644 index 00000000..5763fecc --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TextSequence_GlobalLevel/static/complete-validation.sch @@ -0,0 +1,31 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TextSequence_GlobalLevel/static/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TextSequence_GlobalLevel/static/validation-stage-1a-1.sch new file mode 100644 index 00000000..85a74e93 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TextSequence_GlobalLevel/static/validation-stage-1a-1.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-SEQ-001 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TextSequence_GlobalLevel/static/validation-stage-1a-2.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TextSequence_GlobalLevel/static/validation-stage-1a-2.sch new file mode 100644 index 00000000..e01442ff --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TextSequence_GlobalLevel/static/validation-stage-1a-2.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-SEQ-001 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TimeSequence_GlobalLevel/dynamic/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TimeSequence_GlobalLevel/dynamic/complete-validation.sch new file mode 100644 index 00000000..c3aea285 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TimeSequence_GlobalLevel/dynamic/complete-validation.sch @@ -0,0 +1,31 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TimeSequence_GlobalLevel/dynamic/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TimeSequence_GlobalLevel/dynamic/validation-stage-1a-1.sch new file mode 100644 index 00000000..bf822272 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TimeSequence_GlobalLevel/dynamic/validation-stage-1a-1.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-SEQ-005 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TimeSequence_GlobalLevel/dynamic/validation-stage-1a-2.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TimeSequence_GlobalLevel/dynamic/validation-stage-1a-2.sch new file mode 100644 index 00000000..8b32b4be --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TimeSequence_GlobalLevel/dynamic/validation-stage-1a-2.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-SEQ-005 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TimeSequence_GlobalLevel/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TimeSequence_GlobalLevel/input.efx new file mode 100644 index 00000000..97a77966 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TimeSequence_GlobalLevel/input.efx @@ -0,0 +1,8 @@ +LET time* : $times = (09:00:00Z, 17:00:00Z); + +---- STAGE 1a ---- + +WITH BT-00-Text +ASSERT count($times) > 0 +AS ERROR R-SEQ-005 +FOR BT-00-Text IN 1, 2 diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TimeSequence_GlobalLevel/schematrons.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TimeSequence_GlobalLevel/schematrons.json new file mode 100644 index 00000000..2bd84096 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TimeSequence_GlobalLevel/schematrons.json @@ -0,0 +1,31 @@ +{ + "schematrons" : [ { + "name" : "complete-validation", + "type" : "dynamic", + "filename" : "dynamic/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-1a-2", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-2.sch" + }, { + "name" : "complete-validation", + "type" : "static", + "filename" : "static/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-1a-2", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-2.sch" + } ] +} diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TimeSequence_GlobalLevel/static/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TimeSequence_GlobalLevel/static/complete-validation.sch new file mode 100644 index 00000000..c3aea285 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TimeSequence_GlobalLevel/static/complete-validation.sch @@ -0,0 +1,31 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TimeSequence_GlobalLevel/static/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TimeSequence_GlobalLevel/static/validation-stage-1a-1.sch new file mode 100644 index 00000000..bf822272 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TimeSequence_GlobalLevel/static/validation-stage-1a-1.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-SEQ-005 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TimeSequence_GlobalLevel/static/validation-stage-1a-2.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TimeSequence_GlobalLevel/static/validation-stage-1a-2.sch new file mode 100644 index 00000000..8b32b4be --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testVariable_TimeSequence_GlobalLevel/static/validation-stage-1a-2.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-SEQ-005 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testWithClause_ContextVariableOverride/dynamic/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testWithClause_ContextVariableOverride/dynamic/complete-validation.sch new file mode 100644 index 00000000..ed3741d1 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testWithClause_ContextVariableOverride/dynamic/complete-validation.sch @@ -0,0 +1,34 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + PathNode/TextField + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testWithClause_ContextVariableOverride/dynamic/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testWithClause_ContextVariableOverride/dynamic/validation-stage-1a-1.sch new file mode 100644 index 00000000..945ab7d3 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testWithClause_ContextVariableOverride/dynamic/validation-stage-1a-1.sch @@ -0,0 +1,7 @@ + + + + + rule|text|R-CTX-001 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testWithClause_ContextVariableOverride/dynamic/validation-stage-1a-2.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testWithClause_ContextVariableOverride/dynamic/validation-stage-1a-2.sch new file mode 100644 index 00000000..21908528 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testWithClause_ContextVariableOverride/dynamic/validation-stage-1a-2.sch @@ -0,0 +1,7 @@ + + + + + rule|text|R-CTX-001 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testWithClause_ContextVariableOverride/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testWithClause_ContextVariableOverride/input.efx new file mode 100644 index 00000000..92f1ca00 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testWithClause_ContextVariableOverride/input.efx @@ -0,0 +1,10 @@ +// Test: Context variable override syntax ($ctx::FieldRef) +// Verifies that context variables can be used to override context when accessing fields +// This exercises the preprocessor's context variable lookup functionality + +---- STAGE 1a ---- + +WITH context : $ctx = ND-Root + ASSERT $ctx::BT-00-Text is not empty + AS ERROR R-CTX-001 + FOR BT-00-Text IN 1, 2 diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testWithClause_ContextVariableOverride/schematrons.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testWithClause_ContextVariableOverride/schematrons.json new file mode 100644 index 00000000..2bd84096 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testWithClause_ContextVariableOverride/schematrons.json @@ -0,0 +1,31 @@ +{ + "schematrons" : [ { + "name" : "complete-validation", + "type" : "dynamic", + "filename" : "dynamic/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-1a-2", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-2.sch" + }, { + "name" : "complete-validation", + "type" : "static", + "filename" : "static/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-1a-2", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-2.sch" + } ] +} diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testWithClause_ContextVariableOverride/static/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testWithClause_ContextVariableOverride/static/complete-validation.sch new file mode 100644 index 00000000..ed3741d1 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testWithClause_ContextVariableOverride/static/complete-validation.sch @@ -0,0 +1,34 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + PathNode/TextField + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testWithClause_ContextVariableOverride/static/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testWithClause_ContextVariableOverride/static/validation-stage-1a-1.sch new file mode 100644 index 00000000..945ab7d3 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testWithClause_ContextVariableOverride/static/validation-stage-1a-1.sch @@ -0,0 +1,7 @@ + + + + + rule|text|R-CTX-001 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testWithClause_ContextVariableOverride/static/validation-stage-1a-2.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testWithClause_ContextVariableOverride/static/validation-stage-1a-2.sch new file mode 100644 index 00000000..21908528 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testWithClause_ContextVariableOverride/static/validation-stage-1a-2.sch @@ -0,0 +1,7 @@ + + + + + rule|text|R-CTX-001 + + diff --git a/src/test/resources/json/README.md b/src/test/resources/json/README.md new file mode 100644 index 00000000..53727b5b --- /dev/null +++ b/src/test/resources/json/README.md @@ -0,0 +1,67 @@ +# SDK 2 Test Data + +This folder contains mock data for testing EFX SDK version 2 functionality. + +## Node Hierarchy + +``` +ND-Root (non-rep) +| ++-- ND-SubNode (non-rep) +| +-- ND-SubSubNode (non-rep) <- non-rep in non-rep +| +-- ND-SubSubNode2 (non-rep) <- non-rep in non-rep (sibling) +| +-- ND-RepeatableInSubNode (REP) <- rep in non-rep +| +-- ND-RepeatableInSubNode2 (REP) <- rep in non-rep (sibling) +| ++-- ND-RepeatableNode (REP) + +-- ND-NonRepeatableSubNode (non-rep) <- non-rep in rep + | +-- ND-RepeatableSubSubNode (REP) <- rep in non-rep in rep + +-- ND-NonRepeatableSubNode2 (non-rep) <- non-rep in rep (sibling) + +-- ND-RepeatableInRepeatableNode (REP) <- rep in rep + +-- ND-RepeatableInRepeatableNode2 (REP) <- rep in rep (sibling) +``` + +## Repeatability Coverage + +The structure covers all 4 parent/child repeatability combinations: + +| Parent Type | Child Type | Nodes | +|-------------|------------|-------| +| Non-rep | Non-rep | ND-SubSubNode, ND-SubSubNode2 | +| Non-rep | Rep | ND-RepeatableInSubNode, ND-RepeatableInSubNode2 | +| Rep | Non-rep | ND-NonRepeatableSubNode, ND-NonRepeatableSubNode2 | +| Rep | Rep | ND-RepeatableInRepeatableNode, ND-RepeatableInRepeatableNode2 | + +Sibling pairs enable cross-branch backtracking tests. + +## Field Naming Convention + +Fields use the pattern `BT-XY-Type` where: +- **X** = Branch (1 = ND-SubNode branch, 2 = ND-RepeatableNode branch) +- **Y** = Node within branch (1-4 for children of each branch, 5 for deeper nesting) +- **Type** = Data type (Text, TextMultilingual, Indicator, Number, Date, Time, Measure) + +| Prefix | Parent Node | Repeatability from Root | +|--------|-------------|------------------------| +| BT-00-* | ND-Root | scalar | +| BT-11-* | ND-SubSubNode | scalar | +| BT-12-* | ND-SubSubNode2 | scalar | +| BT-13-* | ND-RepeatableInSubNode | SEQUENCE (self-rep) | +| BT-14-* | ND-RepeatableInSubNode2 | SEQUENCE (self-rep) | +| BT-21-* | ND-NonRepeatableSubNode | SEQUENCE (ancestor-rep) | +| BT-22-* | ND-NonRepeatableSubNode2 | SEQUENCE (ancestor-rep) | +| BT-23-* | ND-RepeatableInRepeatableNode | SEQUENCE (self + ancestor) | +| BT-24-* | ND-RepeatableInRepeatableNode2 | SEQUENCE (self + ancestor) | +| BT-25-* | ND-RepeatableSubSubNode | SEQUENCE (self + ancestor) | + +## Files + +- `fields-sdk2.json` - Field definitions with XPath expressions +- `nodes-sdk2.json` - Node definitions with hierarchy and repeatability + +## Test Scenarios Enabled + +1. **Scalar vs SEQUENCE resolution** - Fields in non-rep vs rep contexts +2. **Ancestor repeatability** - Fields under repeatable ancestors +3. **Cross-branch navigation** - Paths that backtrack through parent nodes +4. **Multiple data types** - Each node has fields of 7 different types diff --git a/src/test/resources/json/fields-sdk1.json b/src/test/resources/json/fields-sdk1.json deleted file mode 100644 index 2edcc39e..00000000 --- a/src/test/resources/json/fields-sdk1.json +++ /dev/null @@ -1,184 +0,0 @@ -[ - { - "id": "BT-00-Text", - "type": "text", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/TextField", - "xpathRelative": "PathNode/TextField" - }, - { - "id": "BT-00-Attribute", - "type": "text", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/TextField/@Attribute", - "xpathRelative": "PathNode/TextField/@Attribute" - }, - { - "id": "BT-00-Indicator", - "type": "indicator", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/IndicatorField", - "xpathRelative": "PathNode/IndicatorField" - }, - { - "id": "BT-00-Code", - "type": "code", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/CodeField", - "xpathRelative": "PathNode/CodeField", - "codeList": { - "value": { - "id": "authority-activity", - "type": "flat", - "parentId": "main-activity" - } - } - }, - { - "id": "BT-00-Internal-Code", - "type": "internal-code", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/InternalCodeField", - "xpathRelative": "PathNode/CodeField", - "codeList": { - "value": { - "id": "authority-activity", - "type": "flat", - "parentId": "main-activity" - } - } - }, - { - "id": "BT-00-CodeAttribute", - "type": "code", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/CodeField/@attribute", - "xpathRelative": "PathNode/CodeField/@attribute", - "codeList": { - "value": { - "id": "authority-activity", - "type": "flat", - "parentId": "main-activity" - } - } - }, - { - "id": "BT-00-Text-Multilingual", - "type": "text-multilingual", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/TextMultilingualField", - "xpathRelative": "PathNode/TextMultilingualField" - }, - { - "id": "BT-00-StartDate", - "type": "date", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/StartDateField", - "xpathRelative": "PathNode/StartDateField" - }, - { - "id": "BT-00-EndDate", - "type": "date", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/EndDateField", - "xpathRelative": "PathNode/EndDateField" - }, - { - "id": "BT-00-StartTime", - "type": "time", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/StartTimeField", - "xpathRelative": "PathNode/StartTimeField" - }, - { - "id": "BT-00-EndTime", - "type": "time", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/EndTimeField", - "xpathRelative": "PathNode/EndTimeField" - }, - { - "id": "BT-00-Measure", - "type": "measure", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/MeasureField", - "xpathRelative": "PathNode/MeasureField" - }, - { - "id": "BT-00-Integer", - "type": "integer", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/IntegerField", - "xpathRelative": "PathNode/IntegerField" - }, - { - "id": "BT-00-Amount", - "type": "amount", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/AmountField", - "xpathRelative": "PathNode/AmountField" - }, - { - "id": "BT-00-Url", - "type": "url", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/UrlField", - "xpathRelative": "PathNode/UrlField" - }, - { - "id": "BT-00-Zoned-Date", - "type": "zoned-date", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/ZonedDateField", - "xpathRelative": "PathNode/ZonedDateField" - }, - { - "id": "BT-00-Zoned-Time", - "type": "zoned-time", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/ZonedTimeField", - "xpathRelative": "PathNode/ZonedTimeField" - }, - { - "id": "BT-00-Id-Ref", - "type": "id-ref", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/IdRefField", - "xpathRelative": "PathNode/IdRefField" - }, - { - "id": "BT-00-Number", - "type": "number", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/NumberField", - "xpathRelative": "PathNode/NumberField" - }, - { - "id": "BT-00-Phone", - "type": "phone", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/PhoneField", - "xpathRelative": "PathNode/PhoneField" - }, - { - "id": "BT-00-Email", - "type": "email", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/EmailField", - "xpathRelative": "PathNode/EmailField" - }, - { - "id": "BT-01-SubLevel-Text", - "type": "text", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/ChildNode/SubLevelTextField", - "xpathRelative": "PathNode/ChildNode/SubLevelTextField" - }, - { - "id": "BT-01-SubNode-Text", - "type": "text", - "parentNodeId": "ND-SubNode", - "xpathAbsolute": "/*/SubNode/SubTextField", - "xpathRelative": "SubTextField" - } -] diff --git a/src/test/resources/json/fields-sdk2.json b/src/test/resources/json/fields-sdk2.json deleted file mode 100644 index 85338bc2..00000000 --- a/src/test/resources/json/fields-sdk2.json +++ /dev/null @@ -1,230 +0,0 @@ -[ - { - "id": "BT-00-Text", - "alias": "textField", - "type": "text", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/TextField", - "xpathRelative": "PathNode/TextField" - }, - { - "id": "BT-00-Attribute", - "alias": "attributeField", - "type": "text", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/TextField/@Attribute", - "xpathRelative": "PathNode/TextField/@Attribute" - }, - { - "id": "BT-00-Indicator", - "alias": "indicatorField", - "type": "indicator", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/IndicatorField", - "xpathRelative": "PathNode/IndicatorField" - }, - { - "id": "BT-00-Code", - "alias": "codeField", - "type": "code", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/CodeField", - "xpathRelative": "PathNode/CodeField", - "codeList": { - "value": { - "id": "authority-activity", - "type": "flat", - "parentId": "main-activity" - } - } - }, - { - "id": "BT-00-Internal-Code", - "alias": "internalCodeField", - "type": "internal-code", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/InternalCodeField", - "xpathRelative": "PathNode/CodeField", - "codeList": { - "value": { - "id": "authority-activity", - "type": "flat", - "parentId": "main-activity" - } - } - }, - { - "id": "BT-00-CodeAttribute", - "alias": "codeAttributeField", - "type": "code", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/CodeField/@attribute", - "xpathRelative": "PathNode/CodeField/@attribute", - "codeList": { - "value": { - "id": "authority-activity", - "type": "flat", - "parentId": "main-activity" - } - } - }, - { - "id": "BT-00-Text-Multilingual", - "alias": "textMultilingualField", - "type": "text-multilingual", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/TextMultilingualField", - "xpathRelative": "PathNode/TextMultilingualField" - }, - { - "id": "BT-00-StartDate", - "alias": "startDateField", - "type": "date", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/StartDateField", - "xpathRelative": "PathNode/StartDateField" - }, - { - "id": "BT-00-EndDate", - "alias": "endDateField", - "type": "date", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/EndDateField", - "xpathRelative": "PathNode/EndDateField" - }, - { - "id": "BT-00-StartTime", - "alias": "startTimeField", - "type": "time", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/StartTimeField", - "xpathRelative": "PathNode/StartTimeField" - }, - { - "id": "BT-00-EndTime", - "alias": "endTimeField", - "type": "time", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/EndTimeField", - "xpathRelative": "PathNode/EndTimeField" - }, - { - "id": "BT-00-Measure", - "alias": "measureField", - "type": "measure", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/MeasureField", - "xpathRelative": "PathNode/MeasureField" - }, - { - "id": "BT-00-Integer", - "alias": "integerField", - "type": "integer", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/IntegerField", - "xpathRelative": "PathNode/IntegerField" - }, - { - "id": "BT-00-Amount", - "alias": "amountField", - "type": "amount", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/AmountField", - "xpathRelative": "PathNode/AmountField" - }, - { - "id": "BT-00-Url", - "alias": "urlField", - "type": "url", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/UrlField", - "xpathRelative": "PathNode/UrlField" - }, - { - "id": "BT-00-Zoned-Date", - "alias": "zonedDateField", - "type": "zoned-date", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/ZonedDateField", - "xpathRelative": "PathNode/ZonedDateField" - }, - { - "id": "BT-00-Zoned-Time", - "alias": "zonedTimeField", - "type": "zoned-time", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/ZonedTimeField", - "xpathRelative": "PathNode/ZonedTimeField" - }, - { - "id": "BT-00-Identifier", - "alias": "identifierField", - "type": "id", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/IdField", - "xpathRelative": "PathNode/IdField" - }, - { - "id": "BT-00-Id-Ref", - "alias": "idRefField", - "type": "id-ref", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/IdRefField", - "xpathRelative": "PathNode/IdRefField" - }, - { - "id": "BT-00-Number", - "alias": "numberField", - "type": "number", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/NumberField", - "xpathRelative": "PathNode/NumberField" - }, - { - "id": "BT-00-Phone", - "alias": "phoneField", - "type": "phone", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/PhoneField", - "xpathRelative": "PathNode/PhoneField" - }, - { - "id": "BT-00-Email", - "alias": "emailField", - "type": "email", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/EmailField", - "xpathRelative": "PathNode/EmailField" - }, - { - "id": "BT-01-SubLevel-Text", - "alias": "subLevel_textField", - "type": "text", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/ChildNode/SubLevelTextField", - "xpathRelative": "PathNode/ChildNode/SubLevelTextField" - }, - { - "id": "BT-01-SubNode-Text", - "alias": "subNode_textField", - "type": "text", - "parentNodeId": "ND-SubNode", - "xpathAbsolute": "/*/SubNode/SubTextField", - "xpathRelative": "SubTextField" - }, - { - "id": "BT-01-SubSubNode-Text", - "alias": "subSubNode_textField", - "type": "text", - "parentNodeId": "ND-SubSubNode", - "xpathAbsolute": "/*/SubNode/SubSubNode/SubTextField[0 = 0]", - "xpathRelative": "SubTextField" - }, - { - "id": "BT-00(a)-Text", - "alias": "textFieldWithParens", - "type": "text", - "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/TextFieldA", - "xpathRelative": "PathNode/TextFieldA" - }] diff --git a/src/test/resources/json/sdk1-fields.json b/src/test/resources/json/sdk1-fields.json new file mode 100644 index 00000000..f2f38702 --- /dev/null +++ b/src/test/resources/json/sdk1-fields.json @@ -0,0 +1,202 @@ +{ + "xmlStructure": [ + { + "id": "ND-Root", + "parentId": null, + "xpathAbsolute": "/*", + "xpathRelative": "/*", + "repeatable": false + }, + { + "id": "ND-SubNode", + "parentId": "ND-Root", + "xpathAbsolute": "/*/SubNode", + "xpathRelative": "SubNode", + "repeatable": false + } + ], + "fields": [ + { + "id": "BT-00-Text", + "type": "text", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/TextField", + "xpathRelative": "PathNode/TextField" + }, + { + "id": "BT-00-Attribute", + "type": "text", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/TextField/@Attribute", + "xpathRelative": "PathNode/TextField/@Attribute" + }, + { + "id": "BT-00-Indicator", + "type": "indicator", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/IndicatorField", + "xpathRelative": "PathNode/IndicatorField" + }, + { + "id": "BT-00-Code", + "type": "code", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/CodeField", + "xpathRelative": "PathNode/CodeField", + "codeList": { + "value": { + "id": "authority-activity", + "type": "flat", + "parentId": "main-activity" + } + } + }, + { + "id": "BT-00-Internal-Code", + "type": "internal-code", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/InternalCodeField", + "xpathRelative": "PathNode/CodeField", + "codeList": { + "value": { + "id": "authority-activity", + "type": "flat", + "parentId": "main-activity" + } + } + }, + { + "id": "BT-00-CodeAttribute", + "type": "code", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/CodeField/@attribute", + "xpathRelative": "PathNode/CodeField/@attribute", + "codeList": { + "value": { + "id": "authority-activity", + "type": "flat", + "parentId": "main-activity" + } + } + }, + { + "id": "BT-00-Text-Multilingual", + "type": "text-multilingual", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/TextMultilingualField", + "xpathRelative": "PathNode/TextMultilingualField" + }, + { + "id": "BT-00-StartDate", + "type": "date", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/StartDateField", + "xpathRelative": "PathNode/StartDateField" + }, + { + "id": "BT-00-EndDate", + "type": "date", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/EndDateField", + "xpathRelative": "PathNode/EndDateField" + }, + { + "id": "BT-00-StartTime", + "type": "time", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/StartTimeField", + "xpathRelative": "PathNode/StartTimeField" + }, + { + "id": "BT-00-EndTime", + "type": "time", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/EndTimeField", + "xpathRelative": "PathNode/EndTimeField" + }, + { + "id": "BT-00-Measure", + "type": "measure", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/MeasureField", + "xpathRelative": "PathNode/MeasureField" + }, + { + "id": "BT-00-Integer", + "type": "integer", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/IntegerField", + "xpathRelative": "PathNode/IntegerField" + }, + { + "id": "BT-00-Amount", + "type": "amount", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/AmountField", + "xpathRelative": "PathNode/AmountField" + }, + { + "id": "BT-00-Url", + "type": "url", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/UrlField", + "xpathRelative": "PathNode/UrlField" + }, + { + "id": "BT-00-Zoned-Date", + "type": "zoned-date", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/ZonedDateField", + "xpathRelative": "PathNode/ZonedDateField" + }, + { + "id": "BT-00-Zoned-Time", + "type": "zoned-time", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/ZonedTimeField", + "xpathRelative": "PathNode/ZonedTimeField" + }, + { + "id": "BT-00-Id-Ref", + "type": "id-ref", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/IdRefField", + "xpathRelative": "PathNode/IdRefField" + }, + { + "id": "BT-00-Number", + "type": "number", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/NumberField", + "xpathRelative": "PathNode/NumberField" + }, + { + "id": "BT-00-Phone", + "type": "phone", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/PhoneField", + "xpathRelative": "PathNode/PhoneField" + }, + { + "id": "BT-00-Email", + "type": "email", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/EmailField", + "xpathRelative": "PathNode/EmailField" + }, + { + "id": "BT-01-SubLevel-Text", + "type": "text", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/ChildNode/SubLevelTextField", + "xpathRelative": "PathNode/ChildNode/SubLevelTextField" + }, + { + "id": "BT-01-SubNode-Text", + "type": "text", + "parentNodeId": "ND-SubNode", + "xpathAbsolute": "/*/SubNode/SubTextField", + "xpathRelative": "SubTextField" + } + ] +} diff --git a/src/test/resources/json/sdk2-fields.json b/src/test/resources/json/sdk2-fields.json new file mode 100644 index 00000000..2dd59909 --- /dev/null +++ b/src/test/resources/json/sdk2-fields.json @@ -0,0 +1,854 @@ +{ + "xmlStructure": [ + { + "id": "ND-Root", + "parentId": null, + "xpathAbsolute": "/*", + "xpathRelative": "/*", + "repeatable": false, + "alias": "Root" + }, + { + "id": "ND-SubNode", + "parentId": "ND-Root", + "xpathAbsolute": "/*/SubNode", + "xpathRelative": "SubNode", + "repeatable": false, + "alias": "SubNode" + }, + { + "id": "ND-SubSubNode", + "parentId": "ND-SubNode", + "xpathAbsolute": "/*/SubNode/SubSubNode", + "xpathRelative": "SubSubNode", + "repeatable": false, + "alias": "SubSubNode" + }, + { + "id": "ND-SubSubNode2", + "parentId": "ND-SubNode", + "xpathAbsolute": "/*/SubNode/SubSubNode2", + "xpathRelative": "SubSubNode2", + "repeatable": false, + "alias": "SubSubNode2" + }, + { + "id": "ND-RepeatableInSubNode", + "parentId": "ND-SubNode", + "xpathAbsolute": "/*/SubNode/RepeatableInSubNode", + "xpathRelative": "RepeatableInSubNode", + "repeatable": true, + "alias": "RepeatableInSubNode" + }, + { + "id": "ND-RepeatableInSubNode2", + "parentId": "ND-SubNode", + "xpathAbsolute": "/*/SubNode/RepeatableInSubNode2", + "xpathRelative": "RepeatableInSubNode2", + "repeatable": true, + "alias": "RepeatableInSubNode2" + }, + { + "id": "ND-RepeatableNode", + "parentId": "ND-Root", + "xpathAbsolute": "/*/RepeatableNode", + "xpathRelative": "RepeatableNode", + "repeatable": true, + "alias": "RepeatableNode" + }, + { + "id": "ND-NonRepeatableSubNode", + "parentId": "ND-RepeatableNode", + "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode", + "xpathRelative": "NonRepeatableSubNode", + "repeatable": false, + "alias": "NonRepeatableSubNode" + }, + { + "id": "ND-NonRepeatableSubNode2", + "parentId": "ND-RepeatableNode", + "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode2", + "xpathRelative": "NonRepeatableSubNode2", + "repeatable": false, + "alias": "NonRepeatableSubNode2" + }, + { + "id": "ND-RepeatableSubSubNode", + "parentId": "ND-NonRepeatableSubNode", + "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode/RepeatableSubSubNode", + "xpathRelative": "RepeatableSubSubNode", + "repeatable": true, + "alias": "RepeatableSubSubNode" + }, + { + "id": "ND-RepeatableInRepeatableNode", + "parentId": "ND-RepeatableNode", + "xpathAbsolute": "/*/RepeatableNode/RepeatableInRepeatableNode", + "xpathRelative": "RepeatableInRepeatableNode", + "repeatable": true, + "alias": "RepeatableInRepeatableNode" + }, + { + "id": "ND-RepeatableInRepeatableNode2", + "parentId": "ND-RepeatableNode", + "xpathAbsolute": "/*/RepeatableNode/RepeatableInRepeatableNode2", + "xpathRelative": "RepeatableInRepeatableNode2", + "repeatable": true, + "alias": "RepeatableInRepeatableNode2" + } + ], + "fields": [ + { + "id": "BT-00-Text", + "alias": "textField", + "type": "text", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/TextField", + "xpathRelative": "PathNode/TextField" + }, + { + "id": "BT-00-Attribute", + "alias": "attributeField", + "type": "text", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/TextField/@Attribute", + "xpathRelative": "PathNode/TextField/@Attribute" + }, + { + "id": "BT-00-Indicator", + "alias": "indicatorField", + "type": "indicator", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/IndicatorField", + "xpathRelative": "PathNode/IndicatorField" + }, + { + "id": "BT-00-Code", + "alias": "codeField", + "type": "code", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/CodeField", + "xpathRelative": "PathNode/CodeField", + "codeList": { + "value": { + "id": "authority-activity", + "type": "flat", + "parentId": "main-activity" + } + } + }, + { + "id": "BT-00-Internal-Code", + "alias": "internalCodeField", + "type": "internal-code", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/InternalCodeField", + "xpathRelative": "PathNode/CodeField", + "codeList": { + "value": { + "id": "authority-activity", + "type": "flat", + "parentId": "main-activity" + } + } + }, + { + "id": "BT-00-CodeAttribute", + "alias": "codeAttributeField", + "type": "code", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/CodeField/@attribute", + "xpathRelative": "PathNode/CodeField/@attribute", + "codeList": { + "value": { + "id": "authority-activity", + "type": "flat", + "parentId": "main-activity" + } + } + }, + { + "id": "BT-00-Text-Multilingual", + "alias": "textMultilingualField", + "type": "text-multilingual", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/TextMultilingualField", + "xpathRelative": "PathNode/TextMultilingualField" + }, + { + "id": "BT-00-StartDate", + "alias": "startDateField", + "type": "date", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/StartDateField", + "xpathRelative": "PathNode/StartDateField" + }, + { + "id": "BT-00-EndDate", + "alias": "endDateField", + "type": "date", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/EndDateField", + "xpathRelative": "PathNode/EndDateField" + }, + { + "id": "BT-00-StartTime", + "alias": "startTimeField", + "type": "time", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/StartTimeField", + "xpathRelative": "PathNode/StartTimeField" + }, + { + "id": "BT-00-EndTime", + "alias": "endTimeField", + "type": "time", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/EndTimeField", + "xpathRelative": "PathNode/EndTimeField" + }, + { + "id": "BT-00-Measure", + "alias": "measureField", + "type": "measure", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/MeasureField", + "xpathRelative": "PathNode/MeasureField" + }, + { + "id": "BT-00-Integer", + "alias": "integerField", + "type": "integer", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/IntegerField", + "xpathRelative": "PathNode/IntegerField" + }, + { + "id": "BT-00-Amount", + "alias": "amountField", + "type": "amount", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/AmountField", + "xpathRelative": "PathNode/AmountField" + }, + { + "id": "BT-00-Url", + "alias": "urlField", + "type": "url", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/UrlField", + "xpathRelative": "PathNode/UrlField" + }, + { + "id": "BT-00-Zoned-Date", + "alias": "zonedDateField", + "type": "zoned-date", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/ZonedDateField", + "xpathRelative": "PathNode/ZonedDateField" + }, + { + "id": "BT-00-Zoned-Time", + "alias": "zonedTimeField", + "type": "zoned-time", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/ZonedTimeField", + "xpathRelative": "PathNode/ZonedTimeField" + }, + { + "id": "BT-00-Identifier", + "alias": "identifierField", + "type": "id", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/IdField", + "xpathRelative": "PathNode/IdField" + }, + { + "id": "BT-00-Id-Ref", + "alias": "idRefField", + "type": "id-ref", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/IdRefField", + "xpathRelative": "PathNode/IdRefField" + }, + { + "id": "BT-00-Number", + "alias": "numberField", + "type": "number", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/NumberField", + "xpathRelative": "PathNode/NumberField" + }, + { + "id": "BT-00-Phone", + "alias": "phoneField", + "type": "phone", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/PhoneField", + "xpathRelative": "PathNode/PhoneField" + }, + { + "id": "BT-00-Email", + "alias": "emailField", + "type": "email", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/EmailField", + "xpathRelative": "PathNode/EmailField" + }, + { + "id": "BT-01-SubLevel-Text", + "alias": "subLevel_textField", + "type": "text", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/ChildNode/SubLevelTextField", + "xpathRelative": "PathNode/ChildNode/SubLevelTextField" + }, + { + "id": "BT-01-SubNode-Text", + "alias": "subNode_textField", + "type": "text", + "parentNodeId": "ND-SubNode", + "xpathAbsolute": "/*/SubNode/SubTextField", + "xpathRelative": "SubTextField" + }, + { + "id": "BT-01-SubSubNode-Text", + "alias": "subSubNode_textField", + "type": "text", + "parentNodeId": "ND-SubSubNode", + "xpathAbsolute": "/*/SubNode/SubSubNode/SubTextField[0 = 0]", + "xpathRelative": "SubTextField" + }, + { + "id": "BT-00(a)-Text", + "alias": "textFieldWithParens", + "type": "text", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/TextFieldA", + "xpathRelative": "PathNode/TextFieldA" + }, + { + "id": "BT-00-Repeatable-Text", + "alias": "repeatableTextField", + "type": "text", + "parentNodeId": "ND-Root", + "xpathAbsolute": "/*/PathNode/RepeatableTextField", + "xpathRelative": "PathNode/RepeatableTextField", + "repeatable": { + "value": true + } + }, + { + "id": "BT-00-Text-In-Repeatable-Node", + "alias": "textInRepeatableNode", + "type": "text", + "parentNodeId": "ND-RepeatableNode", + "xpathAbsolute": "/*/RepeatableNode/TextField", + "xpathRelative": "TextField" + }, + { + "id": "BT-00-Attribute-In-Repeatable-Node", + "alias": "attributeInRepeatableNode", + "type": "text", + "parentNodeId": "ND-RepeatableNode", + "xpathAbsolute": "/*/RepeatableNode/TextField/@attribute", + "xpathRelative": "TextField/@attribute" + }, + { + "id": "BT-00-Text-In-NonRepeatableSubNode", + "alias": "textInNonRepeatableSubNode", + "type": "text", + "parentNodeId": "ND-NonRepeatableSubNode", + "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode/TextField", + "xpathRelative": "TextField" + }, + { + "id": "BT-00-Text-In-RepeatableSubSubNode", + "alias": "textInRepeatableSubSubNode", + "type": "text", + "parentNodeId": "ND-RepeatableSubSubNode", + "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode/RepeatableSubSubNode/TextField", + "xpathRelative": "TextField" + }, + { + "id": "BT-11-TextMultilingual", + "alias": "bt11TextMultilingual", + "type": "text-multilingual", + "parentNodeId": "ND-SubSubNode", + "xpathAbsolute": "/*/SubNode/SubSubNode/TextMultilingual", + "xpathRelative": "TextMultilingual" + }, + { + "id": "BT-11-Indicator", + "alias": "bt11Indicator", + "type": "indicator", + "parentNodeId": "ND-SubSubNode", + "xpathAbsolute": "/*/SubNode/SubSubNode/Indicator", + "xpathRelative": "Indicator" + }, + { + "id": "BT-11-Number", + "alias": "bt11Number", + "type": "number", + "parentNodeId": "ND-SubSubNode", + "xpathAbsolute": "/*/SubNode/SubSubNode/Number", + "xpathRelative": "Number" + }, + { + "id": "BT-11-Date", + "alias": "bt11Date", + "type": "date", + "parentNodeId": "ND-SubSubNode", + "xpathAbsolute": "/*/SubNode/SubSubNode/Date", + "xpathRelative": "Date" + }, + { + "id": "BT-11-Time", + "alias": "bt11Time", + "type": "time", + "parentNodeId": "ND-SubSubNode", + "xpathAbsolute": "/*/SubNode/SubSubNode/Time", + "xpathRelative": "Time" + }, + { + "id": "BT-11-Measure", + "alias": "bt11Measure", + "type": "measure", + "parentNodeId": "ND-SubSubNode", + "xpathAbsolute": "/*/SubNode/SubSubNode/Measure", + "xpathRelative": "Measure" + }, + { + "id": "BT-12-Text", + "alias": "bt12Text", + "type": "text", + "parentNodeId": "ND-SubSubNode2", + "xpathAbsolute": "/*/SubNode/SubSubNode2/Text", + "xpathRelative": "Text" + }, + { + "id": "BT-12-TextMultilingual", + "alias": "bt12TextMultilingual", + "type": "text-multilingual", + "parentNodeId": "ND-SubSubNode2", + "xpathAbsolute": "/*/SubNode/SubSubNode2/TextMultilingual", + "xpathRelative": "TextMultilingual" + }, + { + "id": "BT-12-Indicator", + "alias": "bt12Indicator", + "type": "indicator", + "parentNodeId": "ND-SubSubNode2", + "xpathAbsolute": "/*/SubNode/SubSubNode2/Indicator", + "xpathRelative": "Indicator" + }, + { + "id": "BT-12-Number", + "alias": "bt12Number", + "type": "number", + "parentNodeId": "ND-SubSubNode2", + "xpathAbsolute": "/*/SubNode/SubSubNode2/Number", + "xpathRelative": "Number" + }, + { + "id": "BT-12-Date", + "alias": "bt12Date", + "type": "date", + "parentNodeId": "ND-SubSubNode2", + "xpathAbsolute": "/*/SubNode/SubSubNode2/Date", + "xpathRelative": "Date" + }, + { + "id": "BT-12-Time", + "alias": "bt12Time", + "type": "time", + "parentNodeId": "ND-SubSubNode2", + "xpathAbsolute": "/*/SubNode/SubSubNode2/Time", + "xpathRelative": "Time" + }, + { + "id": "BT-12-Measure", + "alias": "bt12Measure", + "type": "measure", + "parentNodeId": "ND-SubSubNode2", + "xpathAbsolute": "/*/SubNode/SubSubNode2/Measure", + "xpathRelative": "Measure" + }, + { + "id": "BT-13-Text", + "alias": "bt13Text", + "type": "text", + "parentNodeId": "ND-RepeatableInSubNode", + "xpathAbsolute": "/*/SubNode/RepeatableInSubNode/Text", + "xpathRelative": "Text" + }, + { + "id": "BT-13-TextMultilingual", + "alias": "bt13TextMultilingual", + "type": "text-multilingual", + "parentNodeId": "ND-RepeatableInSubNode", + "xpathAbsolute": "/*/SubNode/RepeatableInSubNode/TextMultilingual", + "xpathRelative": "TextMultilingual" + }, + { + "id": "BT-13-Indicator", + "alias": "bt13Indicator", + "type": "indicator", + "parentNodeId": "ND-RepeatableInSubNode", + "xpathAbsolute": "/*/SubNode/RepeatableInSubNode/Indicator", + "xpathRelative": "Indicator" + }, + { + "id": "BT-13-Number", + "alias": "bt13Number", + "type": "number", + "parentNodeId": "ND-RepeatableInSubNode", + "xpathAbsolute": "/*/SubNode/RepeatableInSubNode/Number", + "xpathRelative": "Number" + }, + { + "id": "BT-13-Date", + "alias": "bt13Date", + "type": "date", + "parentNodeId": "ND-RepeatableInSubNode", + "xpathAbsolute": "/*/SubNode/RepeatableInSubNode/Date", + "xpathRelative": "Date" + }, + { + "id": "BT-13-Time", + "alias": "bt13Time", + "type": "time", + "parentNodeId": "ND-RepeatableInSubNode", + "xpathAbsolute": "/*/SubNode/RepeatableInSubNode/Time", + "xpathRelative": "Time" + }, + { + "id": "BT-13-Measure", + "alias": "bt13Measure", + "type": "measure", + "parentNodeId": "ND-RepeatableInSubNode", + "xpathAbsolute": "/*/SubNode/RepeatableInSubNode/Measure", + "xpathRelative": "Measure" + }, + { + "id": "BT-14-Text", + "alias": "bt14Text", + "type": "text", + "parentNodeId": "ND-RepeatableInSubNode2", + "xpathAbsolute": "/*/SubNode/RepeatableInSubNode2/Text", + "xpathRelative": "Text" + }, + { + "id": "BT-14-TextMultilingual", + "alias": "bt14TextMultilingual", + "type": "text-multilingual", + "parentNodeId": "ND-RepeatableInSubNode2", + "xpathAbsolute": "/*/SubNode/RepeatableInSubNode2/TextMultilingual", + "xpathRelative": "TextMultilingual" + }, + { + "id": "BT-14-Indicator", + "alias": "bt14Indicator", + "type": "indicator", + "parentNodeId": "ND-RepeatableInSubNode2", + "xpathAbsolute": "/*/SubNode/RepeatableInSubNode2/Indicator", + "xpathRelative": "Indicator" + }, + { + "id": "BT-14-Number", + "alias": "bt14Number", + "type": "number", + "parentNodeId": "ND-RepeatableInSubNode2", + "xpathAbsolute": "/*/SubNode/RepeatableInSubNode2/Number", + "xpathRelative": "Number" + }, + { + "id": "BT-14-Date", + "alias": "bt14Date", + "type": "date", + "parentNodeId": "ND-RepeatableInSubNode2", + "xpathAbsolute": "/*/SubNode/RepeatableInSubNode2/Date", + "xpathRelative": "Date" + }, + { + "id": "BT-14-Time", + "alias": "bt14Time", + "type": "time", + "parentNodeId": "ND-RepeatableInSubNode2", + "xpathAbsolute": "/*/SubNode/RepeatableInSubNode2/Time", + "xpathRelative": "Time" + }, + { + "id": "BT-14-Measure", + "alias": "bt14Measure", + "type": "measure", + "parentNodeId": "ND-RepeatableInSubNode2", + "xpathAbsolute": "/*/SubNode/RepeatableInSubNode2/Measure", + "xpathRelative": "Measure" + }, + { + "id": "BT-21-TextMultilingual", + "alias": "bt21TextMultilingual", + "type": "text-multilingual", + "parentNodeId": "ND-NonRepeatableSubNode", + "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode/TextMultilingual", + "xpathRelative": "TextMultilingual" + }, + { + "id": "BT-21-Indicator", + "alias": "bt21Indicator", + "type": "indicator", + "parentNodeId": "ND-NonRepeatableSubNode", + "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode/Indicator", + "xpathRelative": "Indicator" + }, + { + "id": "BT-21-Number", + "alias": "bt21Number", + "type": "number", + "parentNodeId": "ND-NonRepeatableSubNode", + "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode/Number", + "xpathRelative": "Number" + }, + { + "id": "BT-21-Date", + "alias": "bt21Date", + "type": "date", + "parentNodeId": "ND-NonRepeatableSubNode", + "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode/Date", + "xpathRelative": "Date" + }, + { + "id": "BT-21-Time", + "alias": "bt21Time", + "type": "time", + "parentNodeId": "ND-NonRepeatableSubNode", + "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode/Time", + "xpathRelative": "Time" + }, + { + "id": "BT-21-Measure", + "alias": "bt21Measure", + "type": "measure", + "parentNodeId": "ND-NonRepeatableSubNode", + "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode/Measure", + "xpathRelative": "Measure" + }, + { + "id": "BT-22-Text", + "alias": "bt22Text", + "type": "text", + "parentNodeId": "ND-NonRepeatableSubNode2", + "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode2/Text", + "xpathRelative": "Text" + }, + { + "id": "BT-22-TextMultilingual", + "alias": "bt22TextMultilingual", + "type": "text-multilingual", + "parentNodeId": "ND-NonRepeatableSubNode2", + "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode2/TextMultilingual", + "xpathRelative": "TextMultilingual" + }, + { + "id": "BT-22-Indicator", + "alias": "bt22Indicator", + "type": "indicator", + "parentNodeId": "ND-NonRepeatableSubNode2", + "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode2/Indicator", + "xpathRelative": "Indicator" + }, + { + "id": "BT-22-Number", + "alias": "bt22Number", + "type": "number", + "parentNodeId": "ND-NonRepeatableSubNode2", + "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode2/Number", + "xpathRelative": "Number" + }, + { + "id": "BT-22-Date", + "alias": "bt22Date", + "type": "date", + "parentNodeId": "ND-NonRepeatableSubNode2", + "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode2/Date", + "xpathRelative": "Date" + }, + { + "id": "BT-22-Time", + "alias": "bt22Time", + "type": "time", + "parentNodeId": "ND-NonRepeatableSubNode2", + "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode2/Time", + "xpathRelative": "Time" + }, + { + "id": "BT-22-Measure", + "alias": "bt22Measure", + "type": "measure", + "parentNodeId": "ND-NonRepeatableSubNode2", + "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode2/Measure", + "xpathRelative": "Measure" + }, + { + "id": "BT-23-Text", + "alias": "bt23Text", + "type": "text", + "parentNodeId": "ND-RepeatableInRepeatableNode", + "xpathAbsolute": "/*/RepeatableNode/RepeatableInRepeatableNode/Text", + "xpathRelative": "Text" + }, + { + "id": "BT-23-TextMultilingual", + "alias": "bt23TextMultilingual", + "type": "text-multilingual", + "parentNodeId": "ND-RepeatableInRepeatableNode", + "xpathAbsolute": "/*/RepeatableNode/RepeatableInRepeatableNode/TextMultilingual", + "xpathRelative": "TextMultilingual" + }, + { + "id": "BT-23-Indicator", + "alias": "bt23Indicator", + "type": "indicator", + "parentNodeId": "ND-RepeatableInRepeatableNode", + "xpathAbsolute": "/*/RepeatableNode/RepeatableInRepeatableNode/Indicator", + "xpathRelative": "Indicator" + }, + { + "id": "BT-23-Number", + "alias": "bt23Number", + "type": "number", + "parentNodeId": "ND-RepeatableInRepeatableNode", + "xpathAbsolute": "/*/RepeatableNode/RepeatableInRepeatableNode/Number", + "xpathRelative": "Number" + }, + { + "id": "BT-23-Date", + "alias": "bt23Date", + "type": "date", + "parentNodeId": "ND-RepeatableInRepeatableNode", + "xpathAbsolute": "/*/RepeatableNode/RepeatableInRepeatableNode/Date", + "xpathRelative": "Date" + }, + { + "id": "BT-23-Time", + "alias": "bt23Time", + "type": "time", + "parentNodeId": "ND-RepeatableInRepeatableNode", + "xpathAbsolute": "/*/RepeatableNode/RepeatableInRepeatableNode/Time", + "xpathRelative": "Time" + }, + { + "id": "BT-23-Measure", + "alias": "bt23Measure", + "type": "measure", + "parentNodeId": "ND-RepeatableInRepeatableNode", + "xpathAbsolute": "/*/RepeatableNode/RepeatableInRepeatableNode/Measure", + "xpathRelative": "Measure" + }, + { + "id": "BT-24-Text", + "alias": "bt24Text", + "type": "text", + "parentNodeId": "ND-RepeatableInRepeatableNode2", + "xpathAbsolute": "/*/RepeatableNode/RepeatableInRepeatableNode2/Text", + "xpathRelative": "Text" + }, + { + "id": "BT-24-TextMultilingual", + "alias": "bt24TextMultilingual", + "type": "text-multilingual", + "parentNodeId": "ND-RepeatableInRepeatableNode2", + "xpathAbsolute": "/*/RepeatableNode/RepeatableInRepeatableNode2/TextMultilingual", + "xpathRelative": "TextMultilingual" + }, + { + "id": "BT-24-Indicator", + "alias": "bt24Indicator", + "type": "indicator", + "parentNodeId": "ND-RepeatableInRepeatableNode2", + "xpathAbsolute": "/*/RepeatableNode/RepeatableInRepeatableNode2/Indicator", + "xpathRelative": "Indicator" + }, + { + "id": "BT-24-Number", + "alias": "bt24Number", + "type": "number", + "parentNodeId": "ND-RepeatableInRepeatableNode2", + "xpathAbsolute": "/*/RepeatableNode/RepeatableInRepeatableNode2/Number", + "xpathRelative": "Number" + }, + { + "id": "BT-24-Date", + "alias": "bt24Date", + "type": "date", + "parentNodeId": "ND-RepeatableInRepeatableNode2", + "xpathAbsolute": "/*/RepeatableNode/RepeatableInRepeatableNode2/Date", + "xpathRelative": "Date" + }, + { + "id": "BT-24-Time", + "alias": "bt24Time", + "type": "time", + "parentNodeId": "ND-RepeatableInRepeatableNode2", + "xpathAbsolute": "/*/RepeatableNode/RepeatableInRepeatableNode2/Time", + "xpathRelative": "Time" + }, + { + "id": "BT-24-Measure", + "alias": "bt24Measure", + "type": "measure", + "parentNodeId": "ND-RepeatableInRepeatableNode2", + "xpathAbsolute": "/*/RepeatableNode/RepeatableInRepeatableNode2/Measure", + "xpathRelative": "Measure" + }, + { + "id": "BT-25-TextMultilingual", + "alias": "bt25TextMultilingual", + "type": "text-multilingual", + "parentNodeId": "ND-RepeatableSubSubNode", + "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode/RepeatableSubSubNode/TextMultilingual", + "xpathRelative": "TextMultilingual" + }, + { + "id": "BT-25-Indicator", + "alias": "bt25Indicator", + "type": "indicator", + "parentNodeId": "ND-RepeatableSubSubNode", + "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode/RepeatableSubSubNode/Indicator", + "xpathRelative": "Indicator" + }, + { + "id": "BT-25-Number", + "alias": "bt25Number", + "type": "number", + "parentNodeId": "ND-RepeatableSubSubNode", + "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode/RepeatableSubSubNode/Number", + "xpathRelative": "Number" + }, + { + "id": "BT-25-Date", + "alias": "bt25Date", + "type": "date", + "parentNodeId": "ND-RepeatableSubSubNode", + "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode/RepeatableSubSubNode/Date", + "xpathRelative": "Date" + }, + { + "id": "BT-25-Time", + "alias": "bt25Time", + "type": "time", + "parentNodeId": "ND-RepeatableSubSubNode", + "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode/RepeatableSubSubNode/Time", + "xpathRelative": "Time" + }, + { + "id": "BT-25-Measure", + "alias": "bt25Measure", + "type": "measure", + "parentNodeId": "ND-RepeatableSubSubNode", + "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode/RepeatableSubSubNode/Measure", + "xpathRelative": "Measure" + } + ] +}