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 2304001c..96c62848 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/SdkSymbolResolver.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/SdkSymbolResolver.java @@ -22,7 +22,7 @@ import java.util.function.Function; import java.util.stream.Collectors; -import eu.europa.ted.efx.exceptions.ConsistencyCheckException; +import eu.europa.ted.efx.exceptions.TranslatorConfigurationException; import eu.europa.ted.efx.exceptions.SdkInconsistencyException; import eu.europa.ted.efx.exceptions.SymbolResolutionException; @@ -304,6 +304,17 @@ public String getTypeOfField(String fieldId) { if (sdkField == null) { throw SymbolResolutionException.unknownSymbol(fieldId); } + + // Temporary: the SDK does not yet distinguish duration from measure. + // Both are "measure" in the SDK, but durations use the "duration-unit" codelist. + // Remove this when the SDK adds "duration" as a proper data type. + if (FieldTypes.MEASURE.getName().equals(sdkField.getType())) { + SdkField unitCodeField = sdkField.getAttributeField("unitCode"); + if (unitCodeField != null && "duration-unit".equals(unitCodeField.getCodelistId())) { + return FieldTypes.DURATION.getName(); + } + } + return sdkField.getType(); } @@ -662,7 +673,7 @@ public String getPrivacySettingOfField(final String fieldId, final PrivacySettin case JUSTIFICATION_DESCRIPTION_FIELD: return privacy.getJustificationDescriptionFieldId(); default: - throw ConsistencyCheckException.unhandledPrivacySetting(privacyField); + throw TranslatorConfigurationException.unhandledPrivacySetting(privacyField); } } diff --git a/src/main/java/eu/europa/ted/efx/exceptions/ConsistencyCheckException.java b/src/main/java/eu/europa/ted/efx/exceptions/ConsistencyCheckException.java deleted file mode 100644 index b2a8cc17..00000000 --- a/src/main/java/eu/europa/ted/efx/exceptions/ConsistencyCheckException.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * 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, - UNHANDLED_PRIVACY_SETTING, - UNHANDLED_LINKED_FIELD_PROPERTY - } - - 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 static final String UNHANDLED_PRIVACY_SETTING = - "Privacy setting '%s' is not handled. " + - "This indicates a bug in the translator. " + - "Add the missing case to the switch in getPrivacySettingOfField()."; - - private static final String UNHANDLED_LINKED_FIELD_PROPERTY = - "Linked field property '%s' is not handled. " + - "This indicates a bug in the translator. " + - "Add the missing case to getLinkedFieldId()."; - - 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); - } - - public static ConsistencyCheckException unhandledPrivacySetting(Object setting) { - return new ConsistencyCheckException(ErrorCode.UNHANDLED_PRIVACY_SETTING, - String.format(UNHANDLED_PRIVACY_SETTING, setting)); - } - - public static ConsistencyCheckException unhandledLinkedFieldProperty(String property) { - return new ConsistencyCheckException(ErrorCode.UNHANDLED_LINKED_FIELD_PROPERTY, - String.format(UNHANDLED_LINKED_FIELD_PROPERTY, property)); - } -} diff --git a/src/main/java/eu/europa/ted/efx/exceptions/EfxCompilationException.java b/src/main/java/eu/europa/ted/efx/exceptions/EfxCompilationException.java new file mode 100644 index 00000000..5f791ce4 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/exceptions/EfxCompilationException.java @@ -0,0 +1,51 @@ +/* + * 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.ParserRuleContext; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.misc.ParseCancellationException; + +/** + * Abstract base class for all EFX compilation exceptions that represent user errors in EFX code. + * Extends ParseCancellationException to properly stop ANTLR4 parsing and bypass error recovery + * mechanisms. + * + * Provides a shared utility for formatting source position information from ANTLR tokens. + */ +@SuppressWarnings("squid:MaximumInheritanceDepth") +public abstract class EfxCompilationException extends ParseCancellationException { + + protected EfxCompilationException(String template, Object... args) { + super(args.length > 0 ? String.format(template, args) : template); + } + + protected EfxCompilationException(ParserRuleContext ctx, String template, Object... args) { + super(formatMessage(ctx, template, args)); + } + + /** + * Formats a complete error message with source position prefix. + * + * @param ctx the ANTLR parser rule context indicating the source position of the error + * @param template the message template (as used by {@link String#format}) + * @param args the arguments to substitute into the template + * @return the formatted message prefixed with "line X:Y " + */ + protected static String formatMessage(ParserRuleContext ctx, String template, Object... args) { + Token token = ctx.getStart(); + return String.format("line %d:%d ", token.getLine(), token.getCharPositionInLine()) + + String.format(template, args); + } +} 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 78411621..007a0361 100644 --- a/src/main/java/eu/europa/ted/efx/exceptions/InvalidArgumentException.java +++ b/src/main/java/eu/europa/ted/efx/exceptions/InvalidArgumentException.java @@ -13,18 +13,15 @@ */ package eu.europa.ted.efx.exceptions; -import org.antlr.v4.runtime.misc.ParseCancellationException; +import org.antlr.v4.runtime.ParserRuleContext; import eu.europa.ted.efx.model.expressions.TypedExpression; import eu.europa.ted.efx.model.variables.Parametrised; /** * Exception thrown when invalid arguments are detected in EFX templates. - * Extends ParseCancellationException to properly stop ANTLR4 parsing - * and bypass error recovery mechanisms. */ -@SuppressWarnings("squid:MaximumInheritanceDepth") // Necessary to integrate with ANTLR4 parser cancellation -public class InvalidArgumentException extends ParseCancellationException { +public class InvalidArgumentException extends EfxCompilationException { public enum ErrorCode { ARGUMENT_NUMBER_MISMATCH, @@ -40,19 +37,24 @@ public enum ErrorCode { private final ErrorCode errorCode; - private InvalidArgumentException(ErrorCode errorCode, String message) { - super(message); + private InvalidArgumentException(ErrorCode errorCode, String template, Object... args) { + super(template, args); + this.errorCode = errorCode; + } + + private InvalidArgumentException(ErrorCode errorCode, ParserRuleContext ctx, String template, Object... args) { + super(ctx, template, args); this.errorCode = errorCode; } public ErrorCode getErrorCode() { - return errorCode; + return this.errorCode; } public static InvalidArgumentException argumentNumberMismatch(Parametrised identifier, int expectedNumber, int actualNumber) { return new InvalidArgumentException(ErrorCode.ARGUMENT_NUMBER_MISMATCH, - String.format(ARGUMENT_NUMBER_MISMATCH, identifier.getClass().getSimpleName().toLowerCase(), - identifier.name, expectedNumber, actualNumber)); + ARGUMENT_NUMBER_MISMATCH, identifier.getClass().getSimpleName().toLowerCase(), + identifier.name, expectedNumber, actualNumber); } public static InvalidArgumentException argumentNumberMismatch(Parametrised identifier, int expectedNumber) { @@ -62,16 +64,16 @@ public static InvalidArgumentException argumentNumberMismatch(Parametrised ident public static InvalidArgumentException argumentTypeMismatch(int position, Parametrised identifier, Class expectedType, Class actualType) { 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())); + 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(ErrorCode.UNSUPPORTED_SEQUENCE_TYPE, String.format(UNSUPPORTED_SEQUENCE_TYPE, type, functionName)); + public static InvalidArgumentException unsupportedSequenceType(ParserRuleContext ctx, String type, String functionName) { + return new InvalidArgumentException(ErrorCode.UNSUPPORTED_SEQUENCE_TYPE, ctx, UNSUPPORTED_SEQUENCE_TYPE, type, functionName); } - public static InvalidArgumentException missingArgument(String parameterName) { - return new InvalidArgumentException(ErrorCode.MISSING_ARGUMENT, String.format(MISSING_ARGUMENT, parameterName)); + public static InvalidArgumentException missingArgument(ParserRuleContext ctx, String parameterName) { + return new InvalidArgumentException(ErrorCode.MISSING_ARGUMENT, ctx, 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 2d87373f..3f1e7d8d 100644 --- a/src/main/java/eu/europa/ted/efx/exceptions/InvalidIdentifierException.java +++ b/src/main/java/eu/europa/ted/efx/exceptions/InvalidIdentifierException.java @@ -13,16 +13,13 @@ */ package eu.europa.ted.efx.exceptions; -import org.antlr.v4.runtime.misc.ParseCancellationException; +import org.antlr.v4.runtime.ParserRuleContext; /** * Exception thrown when identifier-related errors occur during EFX template processing. * This includes undeclared identifiers, duplicate declarations, and scope violations. - * Extends ParseCancellationException to properly stop ANTLR4 parsing - * and bypass error recovery mechanisms. */ -@SuppressWarnings("squid:MaximumInheritanceDepth") // Necessary to integrate with ANTLR4 parser cancellation -public class InvalidIdentifierException extends ParseCancellationException { +public class InvalidIdentifierException extends EfxCompilationException { public enum ErrorCode { UNDECLARED_IDENTIFIER, @@ -36,24 +33,29 @@ public enum ErrorCode { private final ErrorCode errorCode; - private InvalidIdentifierException(ErrorCode errorCode, String message) { - super(message); + private InvalidIdentifierException(ErrorCode errorCode, String template, Object... args) { + super(template, args); + this.errorCode = errorCode; + } + + private InvalidIdentifierException(ErrorCode errorCode, ParserRuleContext ctx, String template, Object... args) { + super(ctx, template, args); this.errorCode = errorCode; } public ErrorCode getErrorCode() { - return errorCode; + return this.errorCode; } public static InvalidIdentifierException undeclaredIdentifier(String identifierName) { - return new InvalidIdentifierException(ErrorCode.UNDECLARED_IDENTIFIER, String.format(UNDECLARED_IDENTIFIER, identifierName)); + return new InvalidIdentifierException(ErrorCode.UNDECLARED_IDENTIFIER, UNDECLARED_IDENTIFIER, identifierName); } public static InvalidIdentifierException alreadyDeclared(String identifierName) { - return new InvalidIdentifierException(ErrorCode.IDENTIFIER_ALREADY_DECLARED, String.format(IDENTIFIER_ALREADY_DECLARED, identifierName)); + return new InvalidIdentifierException(ErrorCode.IDENTIFIER_ALREADY_DECLARED, 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)); + public static InvalidIdentifierException notAContextVariable(ParserRuleContext ctx, String variableName) { + return new InvalidIdentifierException(ErrorCode.NOT_A_CONTEXT_VARIABLE, ctx, 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 1f87a226..e2c26ce7 100644 --- a/src/main/java/eu/europa/ted/efx/exceptions/InvalidIndentationException.java +++ b/src/main/java/eu/europa/ted/efx/exceptions/InvalidIndentationException.java @@ -13,15 +13,12 @@ */ package eu.europa.ted.efx.exceptions; -import org.antlr.v4.runtime.misc.ParseCancellationException; +import org.antlr.v4.runtime.ParserRuleContext; /** * Exception thrown when invalid indentation is detected in EFX templates. - * Extends ParseCancellationException to properly stop ANTLR4 parsing - * and bypass error recovery mechanisms. */ -@SuppressWarnings("squid:MaximumInheritanceDepth") // Necessary to integrate with ANTLR4 parser cancellation -public class InvalidIndentationException extends ParseCancellationException { +public class InvalidIndentationException extends EfxCompilationException { public enum ErrorCode { INCONSISTENT_INDENTATION_SPACES, @@ -41,38 +38,41 @@ public enum ErrorCode { private final ErrorCode errorCode; - private InvalidIndentationException(ErrorCode errorCode, String message) { - super(message); + private InvalidIndentationException(ErrorCode errorCode, String template, Object... args) { + super(template, args); + this.errorCode = errorCode; + } + + private InvalidIndentationException(ErrorCode errorCode, ParserRuleContext ctx, String template, Object... args) { + super(ctx, template, args); this.errorCode = errorCode; } public ErrorCode getErrorCode() { - return errorCode; + return this.errorCode; } - public static InvalidIndentationException inconsistentSpaces(int spaces) { - return new InvalidIndentationException(ErrorCode.INCONSISTENT_INDENTATION_SPACES, - String.format(INCONSISTENT_INDENTATION_SPACES, spaces)); + public static InvalidIndentationException inconsistentSpaces(ParserRuleContext ctx, int spaces) { + return new InvalidIndentationException(ErrorCode.INCONSISTENT_INDENTATION_SPACES, ctx, INCONSISTENT_INDENTATION_SPACES, spaces); } - public static InvalidIndentationException indentationLevelSkipped() { - return new InvalidIndentationException(ErrorCode.INDENTATION_LEVEL_SKIPPED, INDENTATION_LEVEL_SKIPPED); + public static InvalidIndentationException indentationLevelSkipped(ParserRuleContext ctx) { + return new InvalidIndentationException(ErrorCode.INDENTATION_LEVEL_SKIPPED, ctx, INDENTATION_LEVEL_SKIPPED); } - public static InvalidIndentationException startIndentAtZero() { - return new InvalidIndentationException(ErrorCode.START_INDENT_AT_ZERO, START_INDENT_AT_ZERO); + public static InvalidIndentationException startIndentAtZero(ParserRuleContext ctx) { + return new InvalidIndentationException(ErrorCode.START_INDENT_AT_ZERO, ctx, START_INDENT_AT_ZERO); } - public static InvalidIndentationException mixedIndentation() { - return new InvalidIndentationException(ErrorCode.MIXED_INDENTATION, MIXED_INDENTATION); + public static InvalidIndentationException mixedIndentation(ParserRuleContext ctx) { + return new InvalidIndentationException(ErrorCode.MIXED_INDENTATION, ctx, MIXED_INDENTATION); } - public static InvalidIndentationException noNestingOnInvocations() { - return new InvalidIndentationException(ErrorCode.NO_NESTING_ON_INVOCATIONS, NO_NESTING_ON_INVOCATIONS); + public static InvalidIndentationException noNestingOnInvocations(ParserRuleContext ctx) { + return new InvalidIndentationException(ErrorCode.NO_NESTING_ON_INVOCATIONS, ctx, NO_NESTING_ON_INVOCATIONS); } - public static InvalidIndentationException noIndentOnTemplateDeclarations() { - return new InvalidIndentationException(ErrorCode.NO_INDENT_ON_TEMPLATE_DECLARATIONS, - NO_INDENT_ON_TEMPLATE_DECLARATIONS); + public static InvalidIndentationException noIndentOnTemplateDeclarations(ParserRuleContext ctx) { + return new InvalidIndentationException(ErrorCode.NO_INDENT_ON_TEMPLATE_DECLARATIONS, ctx, 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 8f62a23d..5b99cf44 100644 --- a/src/main/java/eu/europa/ted/efx/exceptions/InvalidUsageException.java +++ b/src/main/java/eu/europa/ted/efx/exceptions/InvalidUsageException.java @@ -13,15 +13,14 @@ */ package eu.europa.ted.efx.exceptions; -import org.antlr.v4.runtime.misc.ParseCancellationException; +import org.antlr.v4.runtime.ParserRuleContext; /** * Exception thrown when an EFX construct is used incorrectly, such as referencing * a non-withholdable field for privacy properties, or calling a template-only * function in an expression or validation rule. */ -@SuppressWarnings("squid:MaximumInheritanceDepth") // Necessary to integrate with ANTLR4 parser cancellation -public class InvalidUsageException extends ParseCancellationException { +public class InvalidUsageException extends EfxCompilationException { public enum ErrorCode { SHORTHAND_REQUIRES_CODE_OR_INDICATOR, @@ -43,40 +42,45 @@ public enum ErrorCode { private final ErrorCode errorCode; - private InvalidUsageException(ErrorCode errorCode, String message) { - super(message); + private InvalidUsageException(ErrorCode errorCode, String template, Object... args) { + super(template, args); + this.errorCode = errorCode; + } + + private InvalidUsageException(ErrorCode errorCode, ParserRuleContext ctx, String template, Object... args) { + super(ctx, template, args); this.errorCode = errorCode; } public ErrorCode getErrorCode() { - return errorCode; + return this.errorCode; } - public static InvalidUsageException shorthandRequiresCodeOrIndicator(String fieldName, String fieldType) { - return new InvalidUsageException(ErrorCode.SHORTHAND_REQUIRES_CODE_OR_INDICATOR, String.format(SHORTHAND_REQUIRES_CODE_OR_INDICATOR, fieldName, fieldType)); + public static InvalidUsageException shorthandRequiresCodeOrIndicator(ParserRuleContext ctx, String fieldName, String fieldType) { + return new InvalidUsageException(ErrorCode.SHORTHAND_REQUIRES_CODE_OR_INDICATOR, ctx, SHORTHAND_REQUIRES_CODE_OR_INDICATOR, fieldName, fieldType); } - public static InvalidUsageException shorthandRequiresFieldContext(String shorthandType) { - return new InvalidUsageException(ErrorCode.SHORTHAND_REQUIRES_FIELD_CONTEXT, String.format(SHORTHAND_REQUIRES_FIELD_CONTEXT, shorthandType)); + public static InvalidUsageException shorthandRequiresFieldContext(ParserRuleContext ctx, String shorthandType) { + return new InvalidUsageException(ErrorCode.SHORTHAND_REQUIRES_FIELD_CONTEXT, ctx, 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)); + return new InvalidUsageException(ErrorCode.INVALID_NOTICE_SUBTYPE_RANGE_ORDER, 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)); + public static InvalidUsageException invalidNoticeSubtypeToken(String tokenText) { + return new InvalidUsageException(ErrorCode.INVALID_NOTICE_SUBTYPE_TOKEN, INVALID_NOTICE_SUBTYPE_TOKEN, tokenText); } - public static InvalidUsageException fieldNotWithholdable(String fieldId) { - return new InvalidUsageException(ErrorCode.FIELD_NOT_WITHHOLDABLE, String.format(FIELD_NOT_WITHHOLDABLE, fieldId)); + public static InvalidUsageException fieldNotWithholdable(ParserRuleContext ctx, String fieldId) { + return new InvalidUsageException(ErrorCode.FIELD_NOT_WITHHOLDABLE, ctx, FIELD_NOT_WITHHOLDABLE, fieldId); } - public static InvalidUsageException templateOnlyFunction(String functionName) { - return new InvalidUsageException(ErrorCode.TEMPLATE_ONLY_FUNCTION, String.format(TEMPLATE_ONLY_FUNCTION, functionName)); + public static InvalidUsageException templateOnlyFunction(ParserRuleContext ctx, String functionName) { + return new InvalidUsageException(ErrorCode.TEMPLATE_ONLY_FUNCTION, ctx, TEMPLATE_ONLY_FUNCTION, functionName); } public static InvalidUsageException unsupportedRegexConstruct(String pattern, int position, String reason) { - return new InvalidUsageException(ErrorCode.UNSUPPORTED_REGEX_CONSTRUCT, String.format(UNSUPPORTED_REGEX_CONSTRUCT, pattern, position, reason)); + return new InvalidUsageException(ErrorCode.UNSUPPORTED_REGEX_CONSTRUCT, UNSUPPORTED_REGEX_CONSTRUCT, pattern, position, reason); } } 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 a28ecaff..a50b3581 100644 --- a/src/main/java/eu/europa/ted/efx/exceptions/SymbolResolutionException.java +++ b/src/main/java/eu/europa/ted/efx/exceptions/SymbolResolutionException.java @@ -13,16 +13,13 @@ */ package eu.europa.ted.efx.exceptions; -import org.antlr.v4.runtime.misc.ParseCancellationException; +import org.antlr.v4.runtime.ParserRuleContext; /** * Exception thrown when symbol resolution fails during EFX template processing. * This includes unknown fields, nodes, codelists, or other symbol lookup failures. - * Extends ParseCancellationException to properly stop ANTLR4 parsing - * and bypass error recovery mechanisms. */ -@SuppressWarnings("squid:MaximumInheritanceDepth") // Necessary to integrate with ANTLR4 parser cancellation -public class SymbolResolutionException extends ParseCancellationException { +public class SymbolResolutionException extends EfxCompilationException { public enum ErrorCode { UNKNOWN_SYMBOL, @@ -40,25 +37,30 @@ public enum ErrorCode { private final ErrorCode errorCode; - private SymbolResolutionException(ErrorCode errorCode, String message) { - super(message); + private SymbolResolutionException(ErrorCode errorCode, String template, Object... args) { + super(template, args); + this.errorCode = errorCode; + } + + private SymbolResolutionException(ErrorCode errorCode, ParserRuleContext ctx, String template, Object... args) { + super(ctx, template, args); this.errorCode = errorCode; } public ErrorCode getErrorCode() { - return errorCode; + return this.errorCode; } public static SymbolResolutionException unknownCodelist(String codelistId) { - return new SymbolResolutionException(ErrorCode.UNKNOWN_CODELIST, String.format(UNKNOWN_CODELIST, codelistId)); + return new SymbolResolutionException(ErrorCode.UNKNOWN_CODELIST, UNKNOWN_CODELIST, codelistId); } public static SymbolResolutionException unknownSymbol(String symbol) { - return new SymbolResolutionException(ErrorCode.UNKNOWN_SYMBOL, String.format(UNKNOWN_SYMBOL, symbol)); + return new SymbolResolutionException(ErrorCode.UNKNOWN_SYMBOL, UNKNOWN_SYMBOL, symbol); } public static SymbolResolutionException noCodelistForField(String fieldId) { - return new SymbolResolutionException(ErrorCode.NO_CODELIST_FOR_FIELD, String.format(NO_CODELIST_FOR_FIELD, fieldId)); + return new SymbolResolutionException(ErrorCode.NO_CODELIST_FOR_FIELD, NO_CODELIST_FOR_FIELD, fieldId); } public static SymbolResolutionException rootNodeNotFound() { @@ -66,6 +68,6 @@ public static SymbolResolutionException rootNodeNotFound() { } public static SymbolResolutionException unknownNoticeSubtype(String noticeSubtype, String rangeString) { - return new SymbolResolutionException(ErrorCode.UNKNOWN_NOTICE_SUBTYPE, String.format(UNKNOWN_NOTICE_SUBTYPE, noticeSubtype, rangeString)); + return new SymbolResolutionException(ErrorCode.UNKNOWN_NOTICE_SUBTYPE, UNKNOWN_NOTICE_SUBTYPE, noticeSubtype, rangeString); } -} \ No newline at end of file +} diff --git a/src/main/java/eu/europa/ted/efx/exceptions/TranslatorConfigurationException.java b/src/main/java/eu/europa/ted/efx/exceptions/TranslatorConfigurationException.java new file mode 100644 index 00000000..e4807117 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/exceptions/TranslatorConfigurationException.java @@ -0,0 +1,127 @@ +/* + * 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 translator's internal configuration is incomplete. + * This typically means a new type, enum value, or case was added without updating the + * corresponding map, switch, or handler in the translator code. Not a user error. + */ +public class TranslatorConfigurationException extends IllegalStateException { + + public enum ErrorCode { + TYPE_NOT_REGISTERED, + UNSUPPORTED_TYPE_IN_CONDITIONAL, + MISSING_TYPE_MAPPING, + MISSING_TYPE_ANNOTATION, + UNKNOWN_EXPRESSION_TYPE, + UNHANDLED_VARIABLE_CONTEXT, + UNHANDLED_PRIVACY_SETTING, + UNHANDLED_LINKED_FIELD_PROPERTY, + UNHANDLED_PREDICATE_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 UNHANDLED_VARIABLE_CONTEXT = + "Variable context type '%s' is not handled. " + + "Add a handler for this context type in exitContextVariableSpecifier()."; + + private static final String UNHANDLED_PRIVACY_SETTING = + "Privacy setting '%s' is not handled. " + + "This indicates a bug in the translator. " + + "Add the missing case to the switch in getPrivacySettingOfField()."; + + private static final String UNHANDLED_LINKED_FIELD_PROPERTY = + "Linked field property '%s' is not handled. " + + "This indicates a bug in the translator. " + + "Add the missing case to getLinkedFieldId()."; + + private static final String UNHANDLED_PREDICATE_CONTEXT = + "Predicate used in unhandled context '%s'. " + + "If the grammar was updated to allow predicates in new contexts, " + + "add a handler for this case in enterPredicate()."; + + private final ErrorCode errorCode; + + private TranslatorConfigurationException(ErrorCode errorCode, String template, Object... args) { + super(args.length > 0 ? String.format(template, args) : template); + this.errorCode = errorCode; + } + + public ErrorCode getErrorCode() { + return errorCode; + } + + public static TranslatorConfigurationException typeNotRegistered(Class type) { + return new TranslatorConfigurationException(ErrorCode.TYPE_NOT_REGISTERED, TYPE_NOT_REGISTERED, type.getName()); + } + + public static TranslatorConfigurationException unsupportedTypeInConditional(Class type) { + return new TranslatorConfigurationException(ErrorCode.UNSUPPORTED_TYPE_IN_CONDITIONAL, UNSUPPORTED_TYPE_IN_CONDITIONAL, type.getName()); + } + + public static TranslatorConfigurationException missingTypeMapping(Class type, String mapName) { + return new TranslatorConfigurationException(ErrorCode.MISSING_TYPE_MAPPING, MISSING_TYPE_MAPPING, type.getName(), mapName); + } + + public static TranslatorConfigurationException missingTypeAnnotation(Class type) { + return new TranslatorConfigurationException(ErrorCode.MISSING_TYPE_ANNOTATION, MISSING_TYPE_ANNOTATION, type.getName()); + } + + public static TranslatorConfigurationException unknownExpressionType(Class type) { + return new TranslatorConfigurationException(ErrorCode.UNKNOWN_EXPRESSION_TYPE, UNKNOWN_EXPRESSION_TYPE, type.getName()); + } + + public static TranslatorConfigurationException unhandledVariableContext(String contextClassName) { + return new TranslatorConfigurationException(ErrorCode.UNHANDLED_VARIABLE_CONTEXT, UNHANDLED_VARIABLE_CONTEXT, contextClassName); + } + + public static TranslatorConfigurationException unhandledPrivacySetting(Object setting) { + return new TranslatorConfigurationException(ErrorCode.UNHANDLED_PRIVACY_SETTING, UNHANDLED_PRIVACY_SETTING, setting); + } + + public static TranslatorConfigurationException unhandledLinkedFieldProperty(String property) { + return new TranslatorConfigurationException(ErrorCode.UNHANDLED_LINKED_FIELD_PROPERTY, UNHANDLED_LINKED_FIELD_PROPERTY, property); + } + + public static TranslatorConfigurationException unhandledPredicateContext(String contextClassName) { + return new TranslatorConfigurationException(ErrorCode.UNHANDLED_PREDICATE_CONTEXT, UNHANDLED_PREDICATE_CONTEXT, contextClassName); + } +} 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 381a0b9e..fbc0681f 100644 --- a/src/main/java/eu/europa/ted/efx/exceptions/TypeMismatchException.java +++ b/src/main/java/eu/europa/ted/efx/exceptions/TypeMismatchException.java @@ -13,7 +13,7 @@ */ package eu.europa.ted.efx.exceptions; -import org.antlr.v4.runtime.misc.ParseCancellationException; +import org.antlr.v4.runtime.ParserRuleContext; import eu.europa.ted.efx.model.ParsedEntity; import eu.europa.ted.efx.model.expressions.Expression; @@ -21,11 +21,8 @@ /** * Exception thrown when type mismatches are detected in EFX templates. - * Extends ParseCancellationException to properly stop ANTLR4 parsing - * and bypass error recovery mechanisms. */ -@SuppressWarnings("squid:MaximumInheritanceDepth") // Necessary to integrate with ANTLR4 parser cancellation -public class TypeMismatchException extends ParseCancellationException { +public class TypeMismatchException extends EfxCompilationException { public enum ErrorCode { CANNOT_CONVERT, @@ -41,13 +38,18 @@ public enum ErrorCode { private final ErrorCode errorCode; - private TypeMismatchException(ErrorCode errorCode, String message) { - super(message); + private TypeMismatchException(ErrorCode errorCode, String template, Object... args) { + super(template, args); + this.errorCode = errorCode; + } + + private TypeMismatchException(ErrorCode errorCode, ParserRuleContext ctx, String template, Object... args) { + super(ctx, template, args); this.errorCode = errorCode; } public ErrorCode getErrorCode() { - return errorCode; + return this.errorCode; } public static TypeMismatchException cannotConvert(Class expectedType, @@ -57,26 +59,25 @@ public static TypeMismatchException cannotConvert(Class var actual = actualType.asSubclass(TypedExpression.class); var expected = expectedType.asSubclass(TypedExpression.class); - return new TypeMismatchException(ErrorCode.CANNOT_CONVERT, String.format(CANNOT_CONVERT, + return new TypeMismatchException(ErrorCode.CANNOT_CONVERT, CANNOT_CONVERT, TypedExpression.getEfxDataType(expected).getSimpleName(), - TypedExpression.getEfxDataType(actual).getSimpleName())); + TypedExpression.getEfxDataType(actual).getSimpleName()); } - return new TypeMismatchException(ErrorCode.CANNOT_CONVERT, String.format(CANNOT_CONVERT, - expectedType.getSimpleName(), actualType.getSimpleName())); + return new TypeMismatchException(ErrorCode.CANNOT_CONVERT, CANNOT_CONVERT, + expectedType.getSimpleName(), actualType.getSimpleName()); } - public static TypeMismatchException cannotCompare(Expression left, Expression right) { - return new TypeMismatchException(ErrorCode.CANNOT_COMPARE, String.format(CANNOT_COMPARE, - left.getClass().getSimpleName(), right.getClass().getSimpleName())); + public static TypeMismatchException cannotCompare(ParserRuleContext ctx, Expression left, Expression right) { + return new TypeMismatchException(ErrorCode.CANNOT_COMPARE, ctx, CANNOT_COMPARE, + left.getClass().getSimpleName(), right.getClass().getSimpleName()); } - public static TypeMismatchException fieldMayRepeat(String fieldId, String contextSymbol) { - return new TypeMismatchException(ErrorCode.EXPECTED_SCALAR, String.format(EXPECTED_SCALAR, fieldId, - contextSymbol != null ? contextSymbol : "root")); + public static TypeMismatchException fieldMayRepeat(ParserRuleContext ctx, String fieldId, String contextSymbol) { + return new TypeMismatchException(ErrorCode.EXPECTED_SCALAR, ctx, EXPECTED_SCALAR, 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)); + public static TypeMismatchException nodesHaveNoValue(ParserRuleContext ctx, String variableName, String nodeId) { + return new TypeMismatchException(ErrorCode.EXPECTED_FIELD_CONTEXT, ctx, EXPECTED_FIELD_CONTEXT, variableName, nodeId); } } - diff --git a/src/main/java/eu/europa/ted/efx/interfaces/ScriptGenerator.java b/src/main/java/eu/europa/ted/efx/interfaces/ScriptGenerator.java index 687b88f0..f5366cc2 100644 --- a/src/main/java/eu/europa/ted/efx/interfaces/ScriptGenerator.java +++ b/src/main/java/eu/europa/ted/efx/interfaces/ScriptGenerator.java @@ -99,6 +99,15 @@ default PathExpression composeFieldReferenceWithAxis(final PathExpression fieldR */ public PathExpression composeFieldValueReference(final PathExpression fieldReference); + /** + * Given a PathExpression, this method should return the target language script for retrieving + * the raw text content of the field, without any type-specific processing. + * + * @param fieldReference The PathExpression that points to the field. + * @return A StringExpression that retrieves the raw text value of the field. + */ + public StringExpression composeFieldRawValueReference(final PathExpression fieldReference); + /** * Given a PathExpression and an attribute name, this method should return the target language * script for retrieving the value of the attribute. @@ -497,6 +506,30 @@ public NumericExpression composeNumericOperation(NumericExpression leftOperand, */ public NumericExpression composeSecondsFunction(TimeExpression time); + /** + * Returns the target language script that extracts the years component from a duration. + * + * @param duration The duration expression to extract the years from. + * @return A numeric expression representing the years component. + */ + public NumericExpression composeYearsFromDurationFunction(DurationExpression duration); + + /** + * Returns the target language script that extracts the total months from a duration. + * + * @param duration The duration expression to extract the total months from. + * @return A numeric expression representing the total months (years * 12 + months). + */ + public NumericExpression composeMonthsFromDurationFunction(DurationExpression duration); + + /** + * Returns the target language script that extracts the total days from a duration. + * + * @param duration The duration expression to extract the total days from. + * @return A numeric expression representing the total days. + */ + public NumericExpression composeDaysFromDurationFunction(DurationExpression duration); + /** * Returns the target language script that computes the absolute value of a number. * 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 292fc001..dd25d57e 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 @@ -13,7 +13,7 @@ */ package eu.europa.ted.efx.model.expressions; -import eu.europa.ted.efx.exceptions.ConsistencyCheckException; +import eu.europa.ted.efx.exceptions.TranslatorConfigurationException; 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; @@ -39,7 +39,7 @@ public interface TypedExpression extends Expression { static Class getEfxDataType(Class clazz) { EfxDataTypeAssociation annotation = clazz.getAnnotation(EfxDataTypeAssociation.class); if (annotation == null) { - throw ConsistencyCheckException.missingTypeAnnotation(clazz); + throw TranslatorConfigurationException.missingTypeAnnotation(clazz); } return annotation.dataType(); } @@ -73,7 +73,7 @@ public static T from(TypedExpression source, Class toPrimitive(Class type) { if (type == null) { @@ -101,7 +101,7 @@ public static Class toPrimitive(Class toPrimitive(Class toScalar(Class type) { if (type == null) { @@ -121,7 +121,7 @@ public static Class toScalar(Class toScalar(Class toSequence(Class type) { if (type == null) { @@ -141,7 +141,7 @@ public static Class toSequence(Class parameterType) { + private void exitParameterDeclaration(ParserRuleContext ctx, String parameterName, Class parameterType) { if (this.expressionParameters.isEmpty()) { - throw InvalidArgumentException.missingArgument(parameterName); + throw InvalidArgumentException.missingArgument(ctx, parameterName); } ParsedParameter parameter = new ParsedParameter(parameterName, @@ -1426,7 +1426,7 @@ public void exitDistinctValuesFunction(DistinctValuesFunctionContext ctx) { } else if (EfxDataType.Duration.class.isAssignableFrom(sequenceType)) { this.exitDistinctValuesFunction(DurationSequenceExpression.class); } else { - throw InvalidArgumentException.unsupportedSequenceType(sequenceType.getSimpleName(), + throw InvalidArgumentException.unsupportedSequenceType(ctx, sequenceType.getSimpleName(), EfxLexer.VOCABULARY.getLiteralName(EfxLexer.DistinctValuesFunction)); } } @@ -1455,7 +1455,7 @@ public void exitUnionFunction(UnionFunctionContext ctx) { } else if (EfxDataType.Duration.class.isAssignableFrom(sequenceType)) { this.exitUnionFunction(DurationSequenceExpression.class); } else { - throw InvalidArgumentException.unsupportedSequenceType(sequenceType.getSimpleName(), + throw InvalidArgumentException.unsupportedSequenceType(ctx, sequenceType.getSimpleName(), EfxLexer.VOCABULARY.getLiteralName(EfxLexer.UnionFunction)); } } @@ -1485,7 +1485,7 @@ public void exitIntersectFunction(IntersectFunctionContext ctx) { } else if (EfxDataType.Duration.class.isAssignableFrom(sequenceType)) { this.exitIntersectFunction(DurationSequenceExpression.class); } else { - throw InvalidArgumentException.unsupportedSequenceType(sequenceType.getSimpleName(), + throw InvalidArgumentException.unsupportedSequenceType(ctx, sequenceType.getSimpleName(), EfxLexer.VOCABULARY.getLiteralName(EfxLexer.IntersectFunction)); } } @@ -1515,7 +1515,7 @@ public void exitExceptFunction(ExceptFunctionContext ctx) { } else if (EfxDataType.Duration.class.isAssignableFrom(sequenceType)) { this.exitExceptFunction(DurationSequenceExpression.class); } else { - throw InvalidArgumentException.unsupportedSequenceType(sequenceType.getSimpleName(), + throw InvalidArgumentException.unsupportedSequenceType(ctx, sequenceType.getSimpleName(), EfxLexer.VOCABULARY.getLiteralName(EfxLexer.ExceptFunction)); } } 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 1d21346b..286a695e 100644 --- a/src/main/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1.java +++ b/src/main/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1.java @@ -22,6 +22,7 @@ import org.antlr.v4.runtime.CharStream; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.ParserRuleContext; import org.antlr.v4.runtime.tree.ParseTree; import org.antlr.v4.runtime.tree.ParseTreeWalker; import org.slf4j.Logger; @@ -336,7 +337,7 @@ public void exitShorthandFieldLabelReference(ShorthandFieldLabelReferenceContext : this.script.getStringLiteralFromUnquotedString(""); if (labelType.getScript().equals("value")) { - this.shorthandIndirectLabelReference(fieldId); + this.shorthandIndirectLabelReference(ctx, fieldId); } else { this.stack.push(this.markup.renderLabelFromKey(this.script.composeStringConcatenation( List.of(this.script.getStringLiteralFromUnquotedString(ASSET_TYPE_FIELD), @@ -348,10 +349,10 @@ public void exitShorthandFieldLabelReference(ShorthandFieldLabelReferenceContext @Override public void exitShorthandIndirectLabelReference(ShorthandIndirectLabelReferenceContext ctx) { - this.shorthandIndirectLabelReference(ctx.FieldId().getText()); + this.shorthandIndirectLabelReference(ctx, ctx.FieldId().getText()); } - private void shorthandIndirectLabelReference(final String fieldId) { + private void shorthandIndirectLabelReference(ParserRuleContext ctx, final String fieldId) { final Context currentContext = this.efxContext.peek(); final String fieldType = this.symbols.getTypeOfField(fieldId); final PathExpression valueReference = this.symbols.isAttributeField(fieldId) @@ -403,7 +404,7 @@ private void shorthandIndirectLabelReference(final String fieldId) { StringSequenceExpression.class), TranslatorContext.DEFAULT)); break; default: - throw InvalidUsageException.shorthandRequiresCodeOrIndicator(fieldId, fieldType); + throw InvalidUsageException.shorthandRequiresCodeOrIndicator(ctx, fieldId, fieldType); } } @@ -420,7 +421,7 @@ public void exitShorthandLabelReferenceFromContext(ShorthandLabelReferenceFromCo final String labelType = ctx.LabelType().getText(); if (this.efxContext.isFieldContext()) { if (labelType.equals(SHORTHAND_CONTEXT_FIELD_LABEL_REFERENCE)) { - this.shorthandIndirectLabelReference(this.efxContext.symbol()); + this.shorthandIndirectLabelReference(ctx, this.efxContext.symbol()); } else { this.stack.push(this.markup.renderLabelFromKey(this.script.composeStringConcatenation( List.of(this.script.getStringLiteralFromUnquotedString(ASSET_TYPE_FIELD), @@ -448,9 +449,9 @@ public void exitShorthandLabelReferenceFromContext(ShorthandLabelReferenceFromCo public void exitShorthandIndirectLabelReferenceFromContextField( ShorthandIndirectLabelReferenceFromContextFieldContext ctx) { if (!this.efxContext.isFieldContext()) { - throw InvalidUsageException.shorthandRequiresFieldContext("#value"); + throw InvalidUsageException.shorthandRequiresFieldContext(ctx, "#value"); } - this.shorthandIndirectLabelReference(this.efxContext.symbol()); + this.shorthandIndirectLabelReference(ctx, this.efxContext.symbol()); } @Override @@ -529,7 +530,7 @@ public void exitStandardExpressionBlock(StandardExpressionBlockContext ctx) { public void exitShorthandFieldValueReferenceFromContextField( ShorthandFieldValueReferenceFromContextFieldContext ctx) { if (!this.efxContext.isFieldContext()) { - throw InvalidUsageException.shorthandRequiresFieldContext("$value"); + throw InvalidUsageException.shorthandRequiresFieldContext(ctx, "$value"); } this.stack.push(this.script.composeFieldValueReference( this.symbols.getRelativePathOfField(this.efxContext.symbol(), this.efxContext.symbol()))); @@ -569,10 +570,10 @@ public void enterTemplateLine(TemplateLineContext ctx) { final int indentLevel = this.getIndentLevel(ctx); final int indentChange = indentLevel - this.blockStack.currentIndentationLevel(); if (indentChange > 1) { - throw InvalidIndentationException.indentationLevelSkipped(); + throw InvalidIndentationException.indentationLevelSkipped(ctx); } else if (indentChange == 1) { if (this.blockStack.isEmpty()) { - throw InvalidIndentationException.startIndentAtZero(); + throw InvalidIndentationException.startIndentAtZero(ctx); } this.stack.pushStackFrame(); // Create a stack frame for the new template line. } else if (indentChange < 0) { @@ -606,10 +607,10 @@ public void exitTemplateLine(TemplateLineContext ctx) { this.stack.clear(); // Variable scope boundary. Clear declared variables if (indentChange > 1) { - throw InvalidIndentationException.indentationLevelSkipped(); + throw InvalidIndentationException.indentationLevelSkipped(ctx); } else if (indentChange == 1) { if (this.blockStack.isEmpty()) { - throw InvalidIndentationException.startIndentAtZero(); + throw InvalidIndentationException.startIndentAtZero(ctx); } this.blockStack.pushChild(outlineNumber, this.relativizeContext(lineContext, this.blockStack.currentContext()), variables, new Conditionals(), content); @@ -655,7 +656,7 @@ private Context relativizeContext(Context childContext, Context parentContext) { private int getIndentLevel(TemplateLineContext ctx) { if (ctx.MixedIndent() != null) { - throw InvalidIndentationException.mixedIndentation(); + throw InvalidIndentationException.mixedIndentation(ctx); } if (ctx.Spaces() != null) { @@ -663,18 +664,18 @@ private int getIndentLevel(TemplateLineContext ctx) { this.indentWith = Indent.SPACES; this.indentSpaces = ctx.Spaces().getText().length(); } else if (this.indentWith == Indent.TABS) { - throw InvalidIndentationException.mixedIndentation(); + throw InvalidIndentationException.mixedIndentation(ctx); } if (ctx.Spaces().getText().length() % this.indentSpaces != 0) { - throw InvalidIndentationException.inconsistentSpaces(this.indentSpaces); + throw InvalidIndentationException.inconsistentSpaces(ctx, this.indentSpaces); } return ctx.Spaces().getText().length() / this.indentSpaces; } else if (ctx.Tabs() != null) { if (this.indentWith == Indent.UNDETERMINED) { this.indentWith = Indent.TABS; } else if (this.indentWith == Indent.SPACES) { - throw InvalidIndentationException.mixedIndentation(); + throw InvalidIndentationException.mixedIndentation(ctx); } return ctx.Tabs().getText().length(); diff --git a/src/main/java/eu/europa/ted/efx/sdk2/EfxErrorStrategy.java b/src/main/java/eu/europa/ted/efx/sdk2/EfxErrorStrategy.java new file mode 100644 index 00000000..9749acf5 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/sdk2/EfxErrorStrategy.java @@ -0,0 +1,203 @@ +/* + * 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.sdk2; + +import static eu.europa.ted.efx.sdk2.EfxParser.*; + +import java.util.Map; + +import org.antlr.v4.runtime.DefaultErrorStrategy; +import org.antlr.v4.runtime.InputMismatchException; +import org.antlr.v4.runtime.NoViableAltException; +import org.antlr.v4.runtime.Parser; +import org.antlr.v4.runtime.Token; + +/** + * Custom ANTLR error strategy that produces human-readable syntax error messages for EFX code. + * Replaces cryptic ANTLR-generated messages like "no viable alternative at input '...'" with + * clearer descriptions of what went wrong. + */ +public class EfxErrorStrategy extends DefaultErrorStrategy { + + private static final Map RULE_NAMES = Map.ofEntries( + // Expression entry points + Map.entry(RULE_singleExpression, "expression"), + Map.entry(RULE_expression, "expression"), + Map.entry(RULE_scalarExpression, "expression"), + Map.entry(RULE_standardExpressionBlock, "expression block"), + Map.entry(RULE_expressionBlock, "expression block"), + Map.entry(RULE_shorthandFieldValueReferenceFromContextField, "field value shorthand"), + + // Typed expressions + Map.entry(RULE_booleanExpression, "boolean expression"), + Map.entry(RULE_stringExpression, "text expression"), + Map.entry(RULE_numericExpression, "numeric expression"), + Map.entry(RULE_dateExpression, "date expression"), + Map.entry(RULE_timeExpression, "time expression"), + Map.entry(RULE_durationExpression, "duration expression"), + Map.entry(RULE_sequenceExpression, "sequence expression"), + + // Sequences + Map.entry(RULE_stringSequence, "text sequence"), + Map.entry(RULE_booleanSequence, "boolean sequence"), + Map.entry(RULE_numericSequence, "numeric sequence"), + Map.entry(RULE_dateSequence, "date sequence"), + Map.entry(RULE_timeSequence, "time sequence"), + Map.entry(RULE_durationSequence, "duration sequence"), + + // Literals + Map.entry(RULE_stringLiteral, "text literal"), + Map.entry(RULE_numericLiteral, "number"), + Map.entry(RULE_booleanLiteral, "boolean value"), + Map.entry(RULE_dateLiteral, "date"), + Map.entry(RULE_timeLiteral, "time"), + Map.entry(RULE_durationLiteral, "duration"), + + // Functions and arguments + Map.entry(RULE_functionInvocation, "function call"), + Map.entry(RULE_argumentList, "argument list"), + Map.entry(RULE_argument, "argument"), + Map.entry(RULE_parameterList, "parameter list"), + Map.entry(RULE_parameterDeclaration, "parameter declaration"), + Map.entry(RULE_parameterValue, "parameter value"), + + // Iterators + Map.entry(RULE_predicate, "condition"), + Map.entry(RULE_iteratorList, "iterator list"), + Map.entry(RULE_iteratorExpression, "iterator"), + Map.entry(RULE_indexer, "indexer"), + + // References + Map.entry(RULE_fieldReference, "field reference"), + Map.entry(RULE_simpleFieldReference, "field reference"), + Map.entry(RULE_absoluteFieldReference, "field reference"), + Map.entry(RULE_linkedFieldReference, "field reference"), + Map.entry(RULE_fieldReferenceWithPredicate, "field reference"), + Map.entry(RULE_fieldContext, "field reference"), + Map.entry(RULE_nodeReference, "node reference"), + Map.entry(RULE_simpleNodeReference, "node reference"), + Map.entry(RULE_absoluteNodeReference, "node reference"), + Map.entry(RULE_nodeReferenceWithPredicate, "node reference"), + Map.entry(RULE_nodeContext, "node reference"), + Map.entry(RULE_noticeReference, "notice reference"), + Map.entry(RULE_codelistReference, "codelist reference"), + Map.entry(RULE_variableReference, "variable reference"), + + // Variables + Map.entry(RULE_variableDeclaration, "variable declaration"), + Map.entry(RULE_contextDeclarationBlock, "context declaration"), + Map.entry(RULE_contextDeclaration, "context declaration"), + Map.entry(RULE_variableList, "variable list"), + + // Template structure + Map.entry(RULE_templateFile, "template"), + Map.entry(RULE_templateLine, "template line"), + Map.entry(RULE_templateDeclaration, "template declaration"), + Map.entry(RULE_templateDefinition, "template definition"), + Map.entry(RULE_template, "template content"), + Map.entry(RULE_templateFragment, "template content"), + Map.entry(RULE_indentation, "indentation"), + Map.entry(RULE_lineBreak, "line break"), + Map.entry(RULE_summarySection, "summary section"), + Map.entry(RULE_navigationSection, "navigation section"), + + // Template blocks + Map.entry(RULE_labelBlock, "label block"), + Map.entry(RULE_textBlock, "text"), + Map.entry(RULE_linkBlock, "link block"), + Map.entry(RULE_chooseTemplate, "choose block"), + Map.entry(RULE_displayTemplate, "display clause"), + Map.entry(RULE_invokeTemplate, "invoke clause"), + Map.entry(RULE_whenBlock, "when clause"), + Map.entry(RULE_otherwiseBlock, "otherwise clause"), + + // Global declarations + Map.entry(RULE_globalDeclaration, "global declaration"), + Map.entry(RULE_functionDeclaration, "function declaration"), + Map.entry(RULE_dictionaryDeclaration, "dictionary declaration"), + Map.entry(RULE_dictionaryLookup, "dictionary lookup"), + + // Rules (validation) + Map.entry(RULE_rulesFile, "rules file"), + Map.entry(RULE_ruleSet, "rule set"), + Map.entry(RULE_simpleRule, "rule"), + Map.entry(RULE_conditionalRule, "rule"), + Map.entry(RULE_fallbackRule, "rule"), + Map.entry(RULE_whenClause, "when clause"), + Map.entry(RULE_assertClause, "assert clause"), + Map.entry(RULE_reportClause, "report clause"), + Map.entry(RULE_asClause, "as clause"), + Map.entry(RULE_severity, "severity"), + Map.entry(RULE_ruleId, "rule identifier"), + Map.entry(RULE_forClause, "for clause"), + Map.entry(RULE_inClause, "in clause"), + Map.entry(RULE_noticeTypeList, "notice type list"), + Map.entry(RULE_noticeTypeRange, "notice type range"), + Map.entry(RULE_noticeType, "notice type") + ); + + @Override + protected void reportNoViableAlternative(Parser recognizer, NoViableAltException e) { + String ruleName = getHumanReadableRuleName(recognizer); + String msg; + if (e.getStartToken().getType() == Token.EOF) { + msg = "Syntax error: unexpected end of " + ruleName; + } else { + String input = getInputText(e.getStartToken(), e.getOffendingToken(), recognizer); + msg = "Syntax error at " + input + " in " + ruleName; + } + recognizer.notifyErrorListeners(e.getOffendingToken(), msg, e); + } + + @Override + protected void reportInputMismatch(Parser recognizer, InputMismatchException e) { + String expected = e.getExpectedTokens().toString(recognizer.getVocabulary()); + String found = getTokenErrorDisplay(e.getOffendingToken()); + String msg = "Syntax error: expected " + expected + " but found " + found; + recognizer.notifyErrorListeners(e.getOffendingToken(), msg, e); + } + + @Override + protected String getTokenErrorDisplay(Token t) { + if (t == null) { + return "''"; + } + if (t.getType() == Token.EOF) { + return "end of input"; + } + return super.getTokenErrorDisplay(t); + } + + private String getHumanReadableRuleName(Parser recognizer) { + int ruleIndex = recognizer.getContext().getRuleIndex(); + String name = RULE_NAMES.get(ruleIndex); + if (name != null) { + return name; + } + String[] ruleNames = recognizer.getRuleNames(); + if (ruleIndex >= 0 && ruleIndex < ruleNames.length) { + return ruleNames[ruleIndex]; + } + return "unknown"; + } + + private String getInputText(Token startToken, Token offendingToken, Parser recognizer) { + if (startToken.getType() == Token.EOF) { + return "end of input"; + } + String text = recognizer.getTokenStream() + .getText(startToken, offendingToken); + return escapeWSAndQuote(text); + } +} 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 335465b1..128b90a2 100644 --- a/src/main/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2.java +++ b/src/main/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2.java @@ -30,7 +30,6 @@ import org.antlr.v4.runtime.ParserRuleContext; import org.antlr.v4.runtime.Token; import org.antlr.v4.runtime.TokenStreamRewriter; -import org.antlr.v4.runtime.misc.ParseCancellationException; import org.antlr.v4.runtime.tree.ParseTree; import org.antlr.v4.runtime.tree.ParseTreeWalker; import org.antlr.v4.runtime.tree.TerminalNode; @@ -42,7 +41,7 @@ import eu.europa.ted.efx.exceptions.SdkInconsistencyException; 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.exceptions.TranslatorConfigurationException; import eu.europa.ted.efx.util.EfxRegexValidator; import eu.europa.ted.efx.interfaces.EfxExpressionTranslator; import eu.europa.ted.efx.interfaces.ScriptGenerator; @@ -161,6 +160,7 @@ public String translateExpression(final String expression, final String... argum new EfxLexer(CharStreams.fromString(preprocessedExpression)); final CommonTokenStream tokens = new CommonTokenStream(lexer); final EfxParser parser = new EfxParser(tokens); + parser.setErrorHandler(new EfxErrorStrategy()); if (errorListener != null) { lexer.removeErrorListeners(); @@ -188,6 +188,7 @@ private T translateArgument(final String parameterValue, CharStreams.fromString(BEGIN_EXPRESSION_BLOCK + parameterValue + END_EXPRESSION_BLOCK)); final CommonTokenStream tokens = new CommonTokenStream(lexer); final EfxParser parser = new EfxParser(tokens); + parser.setErrorHandler(new EfxErrorStrategy()); if (errorListener != null) { lexer.removeErrorListeners(); @@ -225,7 +226,7 @@ else if (ctx.JustificationCode() != null) else if (ctx.JustificationDescription() != null) return this.symbols.getPrivacySettingOfField(baseFieldId, PrivacySetting.JUSTIFICATION_DESCRIPTION_FIELD); else - throw ConsistencyCheckException.unhandledLinkedFieldProperty(ctx.getText()); + throw TranslatorConfigurationException.unhandledLinkedFieldProperty(ctx.getText()); } protected String getFieldId(LinkedFieldReferenceContext ctx) { @@ -1386,7 +1387,7 @@ public void enterPredicate(PredicateContext ctx) { final String fieldId = getFieldId((FieldReferenceWithPredicateContext) parent); this.efxContext.pushFieldContext(fieldId); } else { - throw new ParseCancellationException("Unexpected parent context for predicate: " + parent.getClass().getSimpleName()); + throw TranslatorConfigurationException.unhandledPredicateContext(parent.getClass().getSimpleName()); } } @@ -1540,7 +1541,7 @@ public void exitContextVariableSpecifier(ContextVariableSpecifierContext ctx) { String variableName = ctx.variableReference().variableName.getText(); Context variableContext = this.efxContext.getContextFromVariable(variableName); if (variableContext == null) { - throw InvalidIdentifierException.notAContextVariable(variableName); + throw InvalidIdentifierException.notAContextVariable(ctx, variableName); } if (variableContext.isFieldContext()) { this.efxContext.push(new FieldContext(variableContext.symbol(), @@ -1723,7 +1724,7 @@ public void exitSequenceFromFunctionInvocation(SequenceFromFunctionInvocationCon var baseType = EfxTypeLattice.toPrimitive(function.dataType); var sequenceExpressionType = efxDataTypeToSequenceExpressionMap.get(baseType); if (sequenceExpressionType == null) { - throw ConsistencyCheckException.missingTypeMapping(function.dataType, "efxDataTypeToSequenceExpressionMap"); + throw TranslatorConfigurationException.missingTypeMapping(function.dataType, "efxDataTypeToSequenceExpressionMap"); } this.stack.push(this.script.composeFunctionInvocation( ctx.functionInvocation().functionName.getText(), @@ -1738,68 +1739,68 @@ public void exitSequenceFromFunctionInvocation(SequenceFromFunctionInvocationCon @Override public void exitStringParameterDeclaration(StringParameterDeclarationContext ctx) { - this.exitParameterDeclaration(ctx.parameterName.getText(), StringExpression.class); + this.exitParameterDeclaration(ctx, ctx.parameterName.getText(), StringExpression.class); } @Override public void exitNumericParameterDeclaration(NumericParameterDeclarationContext ctx) { - this.exitParameterDeclaration(ctx.parameterName.getText(), NumericExpression.class); + this.exitParameterDeclaration(ctx, ctx.parameterName.getText(), NumericExpression.class); } @Override public void exitBooleanParameterDeclaration(BooleanParameterDeclarationContext ctx) { - this.exitParameterDeclaration(ctx.parameterName.getText(), BooleanExpression.class); + this.exitParameterDeclaration(ctx, ctx.parameterName.getText(), BooleanExpression.class); } @Override public void exitDateParameterDeclaration(DateParameterDeclarationContext ctx) { - this.exitParameterDeclaration(ctx.parameterName.getText(), DateExpression.class); + this.exitParameterDeclaration(ctx, ctx.parameterName.getText(), DateExpression.class); } @Override public void exitTimeParameterDeclaration(TimeParameterDeclarationContext ctx) { - this.exitParameterDeclaration(ctx.parameterName.getText(), TimeExpression.class); + this.exitParameterDeclaration(ctx, ctx.parameterName.getText(), TimeExpression.class); } @Override public void exitDurationParameterDeclaration(DurationParameterDeclarationContext ctx) { - this.exitParameterDeclaration(ctx.parameterName.getText(), DurationExpression.class); + this.exitParameterDeclaration(ctx, ctx.parameterName.getText(), DurationExpression.class); } // Sequence parameter declarations @Override public void exitStringSequenceParameterDeclaration(StringSequenceParameterDeclarationContext ctx) { - this.exitParameterDeclaration(ctx.parameterName.getText(), StringSequenceExpression.class); + this.exitParameterDeclaration(ctx, ctx.parameterName.getText(), StringSequenceExpression.class); } @Override public void exitNumericSequenceParameterDeclaration(NumericSequenceParameterDeclarationContext ctx) { - this.exitParameterDeclaration(ctx.parameterName.getText(), NumericSequenceExpression.class); + this.exitParameterDeclaration(ctx, ctx.parameterName.getText(), NumericSequenceExpression.class); } @Override public void exitBooleanSequenceParameterDeclaration(BooleanSequenceParameterDeclarationContext ctx) { - this.exitParameterDeclaration(ctx.parameterName.getText(), BooleanSequenceExpression.class); + this.exitParameterDeclaration(ctx, ctx.parameterName.getText(), BooleanSequenceExpression.class); } @Override public void exitDateSequenceParameterDeclaration(DateSequenceParameterDeclarationContext ctx) { - this.exitParameterDeclaration(ctx.parameterName.getText(), DateSequenceExpression.class); + this.exitParameterDeclaration(ctx, ctx.parameterName.getText(), DateSequenceExpression.class); } @Override public void exitTimeSequenceParameterDeclaration(TimeSequenceParameterDeclarationContext ctx) { - this.exitParameterDeclaration(ctx.parameterName.getText(), TimeSequenceExpression.class); + this.exitParameterDeclaration(ctx, ctx.parameterName.getText(), TimeSequenceExpression.class); } @Override public void exitDurationSequenceParameterDeclaration(DurationSequenceParameterDeclarationContext ctx) { - this.exitParameterDeclaration(ctx.parameterName.getText(), DurationSequenceExpression.class); + this.exitParameterDeclaration(ctx, ctx.parameterName.getText(), DurationSequenceExpression.class); } - private void exitParameterDeclaration(String parameterName, Class parameterType) { + private void exitParameterDeclaration(ParserRuleContext ctx, String parameterName, Class parameterType) { if (this.expressionArguments.isEmpty()) { - throw InvalidArgumentException.missingArgument(parameterName); + throw InvalidArgumentException.missingArgument(ctx, parameterName); } ParsedParameter parameter = new ParsedParameter(parameterName, @@ -1848,12 +1849,12 @@ public void exitEndsWithFunction(EndsWithFunctionContext ctx) { public void exitFieldWasWithheldProperty(FieldWasWithheldPropertyContext ctx) { final String fieldId = getFieldId(ctx.fieldMention()); if (this.isFieldRepeatableFromContext(fieldId, this.efxContext.peek())) { - throw TypeMismatchException.fieldMayRepeat(fieldId, this.efxContext.symbol()); + throw TypeMismatchException.fieldMayRepeat(ctx, fieldId, this.efxContext.symbol()); } final String privacyCode = this.symbols.getPrivacyCodeOfField(fieldId); if (privacyCode == null || privacyCode.isEmpty()) { - throw InvalidUsageException.fieldNotWithholdable(fieldId); + throw InvalidUsageException.fieldNotWithholdable(ctx, fieldId); } this.stack.push(this.composeWasWithheldCondition(fieldId, privacyCode)); @@ -1863,12 +1864,12 @@ public void exitFieldWasWithheldProperty(FieldWasWithheldPropertyContext ctx) { public void exitFieldIsWithheldProperty(FieldIsWithheldPropertyContext ctx) { final String fieldId = getFieldId(ctx.fieldMention()); if (this.isFieldRepeatableFromContext(fieldId, this.efxContext.peek())) { - throw TypeMismatchException.fieldMayRepeat(fieldId, this.efxContext.symbol()); + throw TypeMismatchException.fieldMayRepeat(ctx, fieldId, this.efxContext.symbol()); } final String privacyCode = this.symbols.getPrivacyCodeOfField(fieldId); if (privacyCode == null || privacyCode.isEmpty()) { - throw InvalidUsageException.fieldNotWithholdable(fieldId); + throw InvalidUsageException.fieldNotWithholdable(ctx, fieldId); } this.stack.push(this.script.composeLogicalAnd( @@ -1888,12 +1889,12 @@ public void exitFieldIsWithholdableProperty(FieldIsWithholdablePropertyContext c public void exitFieldIsDisclosedProperty(FieldIsDisclosedPropertyContext ctx) { final String fieldId = getFieldId(ctx.fieldMention()); if (this.isFieldRepeatableFromContext(fieldId, this.efxContext.peek())) { - throw TypeMismatchException.fieldMayRepeat(fieldId, this.efxContext.symbol()); + throw TypeMismatchException.fieldMayRepeat(ctx, fieldId, this.efxContext.symbol()); } final String privacyCode = this.symbols.getPrivacyCodeOfField(fieldId); if (privacyCode == null || privacyCode.isEmpty()) { - throw InvalidUsageException.fieldNotWithholdable(fieldId); + throw InvalidUsageException.fieldNotWithholdable(ctx, fieldId); } // "isDisclosed" = "was withheld" AND NOT "still withheld" AND NOT "masked" @@ -1901,25 +1902,25 @@ public void exitFieldIsDisclosedProperty(FieldIsDisclosedPropertyContext ctx) { this.script.composeLogicalAnd( this.composeWasWithheldCondition(fieldId, privacyCode), this.script.composeLogicalNot(this.composeStillWithheldCondition(fieldId))), - this.script.composeLogicalNot(this.composeIsMaskedCondition(fieldId)))); + this.script.composeLogicalNot(this.composeIsMaskedCondition(ctx, fieldId)))); } @Override public void exitFieldIsMaskedProperty(FieldIsMaskedPropertyContext ctx) { final String fieldId = getFieldId(ctx.fieldMention()); if (this.isFieldRepeatableFromContext(fieldId, this.efxContext.peek())) { - throw TypeMismatchException.fieldMayRepeat(fieldId, this.efxContext.symbol()); + throw TypeMismatchException.fieldMayRepeat(ctx, fieldId, this.efxContext.symbol()); } final String privacyCode = this.symbols.getPrivacyCodeOfField(fieldId); if (privacyCode == null || privacyCode.isEmpty()) { - throw InvalidUsageException.fieldNotWithholdable(fieldId); + throw InvalidUsageException.fieldNotWithholdable(ctx, fieldId); } // "isMasked" = was withheld AND field value equals the privacy mask this.stack.push(this.script.composeLogicalAnd( this.composeWasWithheldCondition(fieldId, privacyCode), - this.composeIsMaskedCondition(fieldId))); + this.composeIsMaskedCondition(ctx, fieldId))); } @Override @@ -1927,11 +1928,22 @@ public void exitFieldPrivacyCodeProperty(FieldPrivacyCodePropertyContext ctx) { final String fieldId = getFieldId(ctx.fieldMention()); final String privacyCode = this.symbols.getPrivacyCodeOfField(fieldId); if (privacyCode == null || privacyCode.isEmpty()) { - throw InvalidUsageException.fieldNotWithholdable(fieldId); + throw InvalidUsageException.fieldNotWithholdable(ctx, fieldId); } this.stack.push(this.script.getStringLiteralFromUnquotedString(privacyCode)); } + @Override + public void exitFieldRawValueProperty(FieldRawValuePropertyContext ctx) { + final String fieldId = getFieldId(ctx.fieldMention()); + if (this.isFieldRepeatableFromContext(fieldId, this.efxContext.peek())) { + throw TypeMismatchException.fieldMayRepeat(ctx, fieldId, this.efxContext.symbol()); + } + final PathExpression fieldPath = + this.symbols.getRelativePathOfField(fieldId, this.efxContext.symbol()); + this.stack.push(this.script.composeFieldRawValueReference(fieldPath)); + } + private boolean isFieldRepeatableFromContext(String fieldId, Context context) { String contextNodeId = context.isFieldContext() ? this.symbols.getParentNodeOfField(context.symbol()) @@ -1970,12 +1982,12 @@ private BooleanExpression composeStillWithheldCondition(String fieldId) { BooleanExpression.class); } - private BooleanExpression composeIsMaskedCondition(String fieldId) { + private BooleanExpression composeIsMaskedCondition(ParserRuleContext ctx, String fieldId) { final String maskingValue = this.symbols.getPrivacyMask(fieldId); final PathExpression fieldValue = this.script.composeFieldValueReference(this.symbols.getRelativePathOfField(fieldId, this.efxContext.symbol())); if (!(fieldValue instanceof ScalarExpression)) { - throw TypeMismatchException.fieldMayRepeat(fieldId, this.efxContext.symbol()); + throw TypeMismatchException.fieldMayRepeat(ctx, fieldId, this.efxContext.symbol()); } return this.script.composeComparisonOperation( @@ -2139,6 +2151,21 @@ public void exitSecondsFromTimeFunction(SecondsFromTimeFunctionContext ctx) { this.stack.push(this.script.composeSecondsFunction(this.stack.pop(TimeExpression.class))); } + @Override + public void exitYearsFromDurationFunction(YearsFromDurationFunctionContext ctx) { + this.stack.push(this.script.composeYearsFromDurationFunction(this.stack.pop(DurationExpression.class))); + } + + @Override + public void exitMonthsFromDurationFunction(MonthsFromDurationFunctionContext ctx) { + this.stack.push(this.script.composeMonthsFromDurationFunction(this.stack.pop(DurationExpression.class))); + } + + @Override + public void exitDaysFromDurationFunction(DaysFromDurationFunctionContext ctx) { + this.stack.push(this.script.composeDaysFromDurationFunction(this.stack.pop(DurationExpression.class))); + } + @Override public void exitAbsoluteFunction(AbsoluteFunctionContext ctx) { this.stack.push(this.script.composeAbsFunction(this.stack.pop(NumericExpression.class))); @@ -2238,47 +2265,47 @@ public void exitFormatNumberFunction(FormatNumberFunctionContext ctx) { @Override public void exitFormatShortDateFunction(FormatShortDateFunctionContext ctx) { - throw InvalidUsageException.templateOnlyFunction("format-short"); + throw InvalidUsageException.templateOnlyFunction(ctx, "format-short"); } @Override public void exitFormatShortTimeFunction(FormatShortTimeFunctionContext ctx) { - throw InvalidUsageException.templateOnlyFunction("format-short"); + throw InvalidUsageException.templateOnlyFunction(ctx, "format-short"); } @Override public void exitFormatMediumDateFunction(FormatMediumDateFunctionContext ctx) { - throw InvalidUsageException.templateOnlyFunction("format-medium"); + throw InvalidUsageException.templateOnlyFunction(ctx, "format-medium"); } @Override public void exitFormatMediumTimeFunction(FormatMediumTimeFunctionContext ctx) { - throw InvalidUsageException.templateOnlyFunction("format-medium"); + throw InvalidUsageException.templateOnlyFunction(ctx, "format-medium"); } @Override public void exitFormatLongDateFunction(FormatLongDateFunctionContext ctx) { - throw InvalidUsageException.templateOnlyFunction("format-long"); + throw InvalidUsageException.templateOnlyFunction(ctx, "format-long"); } @Override public void exitFormatLongTimeFunction(FormatLongTimeFunctionContext ctx) { - throw InvalidUsageException.templateOnlyFunction("format-long"); + throw InvalidUsageException.templateOnlyFunction(ctx, "format-long"); } @Override public void exitFormatShortDateTimeFunction(FormatShortDateTimeFunctionContext ctx) { - throw InvalidUsageException.templateOnlyFunction("format-short"); + throw InvalidUsageException.templateOnlyFunction(ctx, "format-short"); } @Override public void exitFormatMediumDateTimeFunction(FormatMediumDateTimeFunctionContext ctx) { - throw InvalidUsageException.templateOnlyFunction("format-medium"); + throw InvalidUsageException.templateOnlyFunction(ctx, "format-medium"); } @Override public void exitFormatLongDateTimeFunction(FormatLongDateTimeFunctionContext ctx) { - throw InvalidUsageException.templateOnlyFunction("format-long"); + throw InvalidUsageException.templateOnlyFunction(ctx, "format-long"); } // #region New in EFX-2 ----------------------------------------------------- @@ -2376,12 +2403,12 @@ public void exitStringJoinFunction(StringJoinFunctionContext ctx) { @Override public void exitPreferredLanguageFunction(PreferredLanguageFunctionContext ctx) { - throw InvalidUsageException.templateOnlyFunction("preferred-language"); + throw InvalidUsageException.templateOnlyFunction(ctx, "preferred-language"); } @Override public void exitPreferredLanguageTextFunction(PreferredLanguageTextFunctionContext ctx) { - throw InvalidUsageException.templateOnlyFunction("preferred-language-text"); + throw InvalidUsageException.templateOnlyFunction(ctx, "preferred-language-text"); } @Override @@ -2406,14 +2433,14 @@ public void exitDateFromStringFunction(DateFromStringFunctionContext ctx) { } @Override - public void exitDatePlusMeasureFunction(DatePlusMeasureFunctionContext ctx) { + public void exitDatePlusDurationFunction(DatePlusDurationFunctionContext ctx) { DurationExpression right = this.stack.pop(DurationExpression.class); DateExpression left = this.stack.pop(DateExpression.class); this.stack.push(this.script.composeAddition(left, right)); } @Override - public void exitDateMinusMeasureFunction(DateMinusMeasureFunctionContext ctx) { + public void exitDateMinusDurationFunction(DateMinusDurationFunctionContext ctx) { DurationExpression right = this.stack.pop(DurationExpression.class); DateExpression left = this.stack.pop(DateExpression.class); this.stack.push(this.script.composeSubtraction(left, right)); @@ -2816,7 +2843,7 @@ public void exitLateBoundScalar(LateBoundScalarContext ctx) { protected static String numericTypeName = getLexerSymbol(EfxLexer.Number); protected static String dateTypeName = getLexerSymbol(EfxLexer.Date); protected static String timeTypeName = getLexerSymbol(EfxLexer.Time); - protected static String durationTypeName = getLexerSymbol(EfxLexer.Measure); + protected static String durationTypeName = getLexerSymbol(EfxLexer.Duration); // Map from eForms field types to EFX type names - made protected for reuse in subclasses protected static final Map eFormsToEfxTypeMap = Map.ofEntries( // @@ -2827,7 +2854,8 @@ public void exitLateBoundScalar(LateBoundScalarContext ctx) { entry(FieldTypes.INDICATOR.getName(), booleanTypeName), // entry(FieldTypes.AMOUNT.getName(), numericTypeName), // entry(FieldTypes.NUMBER.getName(), numericTypeName), // - entry(FieldTypes.MEASURE.getName(), durationTypeName), // + entry(FieldTypes.MEASURE.getName(), numericTypeName), // + entry(FieldTypes.DURATION.getName(), durationTypeName), // entry(FieldTypes.CODE.getName(), textTypeName), // entry(FieldTypes.INTERNAL_CODE.getName(), textTypeName), // entry(FieldTypes.INTEGER.getName(), numericTypeName), // @@ -2890,6 +2918,7 @@ class ExpressionPreprocessor extends EfxBaseListener { this.lexer = new EfxLexer(charStream); this.tokens = new CommonTokenStream(lexer); this.parser = new EfxParser(tokens); + this.parser.setErrorHandler(new EfxErrorStrategy()); this.rewriter = new TokenStreamRewriter(tokens); if (this.errorListener != null) { @@ -2952,7 +2981,7 @@ public void enterPredicate(PredicateContext ctx) { final String fieldId = getFieldId((FieldReferenceWithPredicateContext) parent); this.efxContext.pushFieldContext(fieldId); } else { - throw new ParseCancellationException("Unexpected parent context for predicate: " + parent.getClass().getSimpleName()); + throw TranslatorConfigurationException.unhandledPredicateContext(parent.getClass().getSimpleName()); } } @@ -3040,7 +3069,7 @@ public void exitContextVariableSpecifier(ContextVariableSpecifierContext ctx) { String variableName = ctx.variableReference().variableName.getText(); Context variableContext = this.efxContext.getContextFromVariable(variableName); if (variableContext == null) { - throw InvalidIdentifierException.notAContextVariable(variableName); + throw InvalidIdentifierException.notAContextVariable(ctx, variableName); } if (variableContext.isFieldContext()) { this.efxContext.push(new FieldContext(variableContext.symbol(), @@ -3088,7 +3117,7 @@ public void exitScalarFromFieldReference(ScalarFromFieldReferenceContext ctx) { this.rewriter.insertBefore(ctx.getStart(), "(" + fieldType + "*)"); return; } - throw TypeMismatchException.fieldMayRepeat(fieldId, this.efxContext.symbol()); + throw TypeMismatchException.fieldMayRepeat(ctx, fieldId, this.efxContext.symbol()); } } @@ -3192,14 +3221,14 @@ public void exitScalarFromVariableReference(ScalarFromVariableReferenceContext c // Guard: Node context variables cannot be used as values if (variableContext != null && variableContext.isNodeContext()) { - throw TypeMismatchException.nodesHaveNoValue(variableName, variableContext.symbol()); + throw TypeMismatchException.nodesHaveNoValue(ctx, 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()); + throw TypeMismatchException.fieldMayRepeat(ctx, fieldId, this.efxContext.symbol()); } } @@ -3225,7 +3254,7 @@ public void exitSequenceFromVariableReference(SequenceFromVariableReferenceConte // Guard: Node context variables cannot be used as values if (variableContext != null && variableContext.isNodeContext()) { - throw TypeMismatchException.nodesHaveNoValue(variableName, variableContext.symbol()); + throw TypeMismatchException.nodesHaveNoValue(ctx, variableName, variableContext.symbol()); } // No repeatability check needed - sequences can have multiple values diff --git a/src/main/java/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2.java b/src/main/java/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2.java index 8f9b0bcc..c85b463c 100644 --- a/src/main/java/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2.java +++ b/src/main/java/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2.java @@ -160,6 +160,7 @@ private Map translateRulesFromCharStream(CharStream input, CommonTokenStream tokens = new CommonTokenStream(lexer); EfxParser parser = new EfxParser(tokens); + parser.setErrorHandler(new EfxErrorStrategy()); parser.removeErrorListeners(); parser.addErrorListener(this.errorListener); 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 95a3ac0e..b1578a06 100644 --- a/src/main/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2.java +++ b/src/main/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2.java @@ -27,6 +27,7 @@ import org.antlr.v4.runtime.CharStream; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.ParserRuleContext; import org.antlr.v4.runtime.atn.DecisionInfo; import org.antlr.v4.runtime.atn.ParseInfo; import org.antlr.v4.runtime.atn.ProfilingATNSimulator; @@ -196,6 +197,7 @@ private String renderTemplate(final CharStream charStream, TranslatorOptions opt final EfxLexer lexer = new EfxLexer(CharStreams.fromString(preprocessedTemplate)); final CommonTokenStream tokens = new CommonTokenStream(lexer); final EfxParser parser = new EfxParser(tokens); + parser.setErrorHandler(new EfxErrorStrategy()); // Enable profiling if requested if (options != null && options.isProfilerEnabled()) { @@ -644,7 +646,7 @@ public void exitShorthandFieldLabelReference(ShorthandFieldLabelReferenceContext : this.script.getStringLiteralFromUnquotedString(""); if (labelType.getScript().equals("value")) { - this.shorthandIndirectLabelReference(fieldId, quantity); + this.shorthandIndirectLabelReference(ctx, fieldId, quantity); } else { this.stack.push(this.markup.renderLabelFromKey(this.script.composeStringConcatenation( List.of(this.script.getStringLiteralFromUnquotedString(ASSET_TYPE_FIELD), @@ -658,10 +660,10 @@ public void exitShorthandFieldLabelReference(ShorthandFieldLabelReferenceContext public void exitShorthandIndirectLabelReference(ShorthandIndirectLabelReferenceContext ctx) { // New in EFX-2: Pluralisation of labels based on a supplied quantity NumericExpression quantity = ctx.pluraliser() != null ? this.stack.pop(NumericExpression.class) : NumericExpression.empty(); - this.shorthandIndirectLabelReference(ctx.FieldId().getText(), quantity); + this.shorthandIndirectLabelReference(ctx, ctx.FieldId().getText(), quantity); } - private void shorthandIndirectLabelReference(final String fieldId, final NumericExpression quantity) { + private void shorthandIndirectLabelReference(ParserRuleContext ctx, final String fieldId, final NumericExpression quantity) { final Context currentContext = this.efxContext.peek(); final String fieldType = this.symbols.getTypeOfField(fieldId); final PathExpression valueReference = this.symbols.isAttributeField(fieldId) @@ -715,7 +717,7 @@ private void shorthandIndirectLabelReference(final String fieldId, final Numeric quantity, this.translatorContext)); break; default: - throw InvalidUsageException.shorthandRequiresCodeOrIndicator(fieldId, fieldType); + throw InvalidUsageException.shorthandRequiresCodeOrIndicator(ctx, fieldId, fieldType); } } @@ -736,7 +738,7 @@ public void exitShorthandLabelReferenceFromContext(ShorthandLabelReferenceFromCo final String labelType = ctx.LabelType().getText(); if (this.efxContext.isFieldContext()) { if (labelType.equals(SHORTHAND_CONTEXT_FIELD_LABEL_REFERENCE)) { - this.shorthandIndirectLabelReference(this.efxContext.symbol(), quantity); + this.shorthandIndirectLabelReference(ctx, this.efxContext.symbol(), quantity); } else { this.stack.push(this.markup.renderLabelFromKey(this.script.composeStringConcatenation( List.of(this.script.getStringLiteralFromUnquotedString(ASSET_TYPE_FIELD), @@ -764,9 +766,9 @@ public void exitShorthandLabelReferenceFromContext(ShorthandLabelReferenceFromCo public void exitShorthandIndirectLabelReferenceFromContextField( ShorthandIndirectLabelReferenceFromContextFieldContext ctx) { if (!this.efxContext.isFieldContext()) { - throw InvalidUsageException.shorthandRequiresFieldContext("#value"); + throw InvalidUsageException.shorthandRequiresFieldContext(ctx, "#value"); } - this.shorthandIndirectLabelReference(this.efxContext.symbol(), NumericExpression.empty()); + this.shorthandIndirectLabelReference(ctx, this.efxContext.symbol(), NumericExpression.empty()); } @Override @@ -955,7 +957,7 @@ public void exitPreferredLanguageTextFunction(PreferredLanguageTextFunctionConte public void exitShorthandFieldValueReferenceFromContextField( ShorthandFieldValueReferenceFromContextFieldContext ctx) { if (!this.efxContext.isFieldContext()) { - throw InvalidUsageException.shorthandRequiresFieldContext("$value"); + throw InvalidUsageException.shorthandRequiresFieldContext(ctx, "$value"); } this.stack.push(this.script.composeFieldValueReference( this.symbols.getRelativePathOfField(this.efxContext.symbol(), this.efxContext.symbol()))); @@ -1356,7 +1358,7 @@ public void exitNavigationSection(NavigationSectionContext ctx) { @Override public void enterTemplateLine(TemplateLineContext ctx) { - this.enterTemplateLine(this.getIndentLevel(ctx.indentation())); + this.enterTemplateLine(ctx, this.getIndentLevel(ctx.indentation())); if (ctx.contextDeclarationBlock() == null) { this.exitRootContextDeclaration(); this.stack.push(new Variables()); @@ -1365,16 +1367,16 @@ public void enterTemplateLine(TemplateLineContext ctx) { @Override public void enterTemplateDeclaration(TemplateDeclarationContext ctx) { - this.enterTemplateLine(0); + this.enterTemplateLine(ctx, 0); } - private void enterTemplateLine(final int indentLevel) { + private void enterTemplateLine(ParserRuleContext ctx, final int indentLevel) { final int indentChange = indentLevel - this.blockStack.currentIndentationLevel(); if (indentChange > 1) { - throw InvalidIndentationException.indentationLevelSkipped(); + throw InvalidIndentationException.indentationLevelSkipped(ctx); } else if (indentChange == 1) { if (this.blockStack.isEmpty()) { - throw InvalidIndentationException.startIndentAtZero(); + throw InvalidIndentationException.startIndentAtZero(ctx); } this.stack.pushStackFrame(); // Create a stack frame for the new template line. } else if (indentChange < 0) { @@ -1408,13 +1410,13 @@ public void exitTemplateLine(TemplateLineContext ctx) { assert this.stack.empty() : "Stack should be empty at this point."; if (indentChange > 1) { - throw InvalidIndentationException.indentationLevelSkipped(); + throw InvalidIndentationException.indentationLevelSkipped(ctx); } else if (indentChange == 1) { if (this.blockStack.isEmpty()) { - throw InvalidIndentationException.startIndentAtZero(); + throw InvalidIndentationException.startIndentAtZero(ctx); } if (this.blockStack.peek() instanceof TemplateInvocation) { - throw InvalidIndentationException.noNestingOnInvocations(); + throw InvalidIndentationException.noNestingOnInvocations(ctx); } this.blockStack.pushChild(outlineNumber, this.relativizeContext(lineContext, this.blockStack.currentContext()), variables, conditionals, defaultContent); @@ -1441,7 +1443,7 @@ public void exitTemplateDeclaration(TemplateDeclarationContext ctx) { assert this.stack.empty() : "Stack should be empty at this point."; if (this.getIndentLevel(ctx.indentation()) != 0) { - throw InvalidIndentationException.noIndentOnTemplateDeclarations(); + throw InvalidIndentationException.noIndentOnTemplateDeclarations(ctx); } this.blockStack @@ -1479,7 +1481,7 @@ private int getIndentLevel(IndentationContext ctx) { } if (ctx.MixedIndent() != null) { - throw InvalidIndentationException.mixedIndentation(); + throw InvalidIndentationException.mixedIndentation(ctx); } if (ctx.Spaces() != null) { @@ -1487,18 +1489,18 @@ private int getIndentLevel(IndentationContext ctx) { this.indentWith = Indent.SPACES; this.indentSpaces = ctx.Spaces().getText().length(); } else if (this.indentWith == Indent.TABS) { - throw InvalidIndentationException.mixedIndentation(); + throw InvalidIndentationException.mixedIndentation(ctx); } if (ctx.Spaces().getText().length() % this.indentSpaces != 0) { - throw InvalidIndentationException.inconsistentSpaces(this.indentSpaces); + throw InvalidIndentationException.inconsistentSpaces(ctx, this.indentSpaces); } return ctx.Spaces().getText().length() / this.indentSpaces; } else if (ctx.Tabs() != null) { if (this.indentWith == Indent.UNDETERMINED) { this.indentWith = Indent.TABS; } else if (this.indentWith == Indent.SPACES) { - throw InvalidIndentationException.mixedIndentation(); + throw InvalidIndentationException.mixedIndentation(ctx); } return ctx.Tabs().getText().length(); @@ -1720,7 +1722,7 @@ public void exitDurationSequenceVariableInitializer(DurationSequenceVariableInit @Override public void enterTemplateLine(TemplateLineContext ctx) { final int indentLevel = EfxTemplateTranslatorV2.this.getIndentLevel(ctx.indentation()); - this.enterTemplateLine(indentLevel); + this.enterTemplateLine(ctx, indentLevel); // Push root context if no context declaration block (same as main translator) if (ctx.contextDeclarationBlock() == null) { this.exitRootContextDeclaration(); @@ -1729,16 +1731,16 @@ public void enterTemplateLine(TemplateLineContext ctx) { @Override public void enterTemplateDeclaration(TemplateDeclarationContext ctx) { - this.enterTemplateLine(0); + this.enterTemplateLine(ctx, 0); } - private void enterTemplateLine(final int indentLevel) { + private void enterTemplateLine(ParserRuleContext ctx, final int indentLevel) { final int indentChange = indentLevel - (this.levels.isEmpty() ? 0 : this.levels.peek()); if (indentChange > 1) { - throw InvalidIndentationException.indentationLevelSkipped(); + throw InvalidIndentationException.indentationLevelSkipped(ctx); } else if (indentChange == 1) { if (this.levels.isEmpty()) { - throw InvalidIndentationException.startIndentAtZero(); + throw InvalidIndentationException.startIndentAtZero(ctx); } this.stack.pushStackFrame(); // Create a stack frame for the new template line. } else if (indentChange < 0) { @@ -1764,25 +1766,25 @@ public void exitTemplateLine(TemplateLineContext ctx) { this.efxContext.pop(); } final int indentLevel = EfxTemplateTranslatorV2.this.getIndentLevel(ctx.indentation()); - this.exitTemplateLine(indentLevel); + this.exitTemplateLine(ctx, 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); + this.exitTemplateLine(ctx, 0); } - private void exitTemplateLine(final int indentLevel) { + private void exitTemplateLine(ParserRuleContext ctx, 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."; if (indentChange > 1) { - throw InvalidIndentationException.indentationLevelSkipped(); + throw InvalidIndentationException.indentationLevelSkipped(ctx); } else if (indentChange == 1) { if (this.levels.isEmpty()) { - throw InvalidIndentationException.startIndentAtZero(); + throw InvalidIndentationException.startIndentAtZero(ctx); } this.levels.push(this.levels.peek() + 1); } else if (indentChange == 0 && this.levels.isEmpty()) { 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 bedfc5a5..893156d9 100644 --- a/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java +++ b/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java @@ -126,6 +126,11 @@ public PathExpression composeFieldValueReference(PathExpression fieldReference) return Expression.instantiate(fieldReference.getScript(), fieldReference.getClass()); } + @Override + public StringExpression composeFieldRawValueReference(PathExpression fieldReference) { + return new StringExpression(fieldReference.getScript() + "/normalize-space(text())"); + } + @Override public T composeFieldAttributeReference(PathExpression fieldReference, String attribute, Class type) { @@ -531,6 +536,22 @@ public NumericExpression composeSecondsFunction(TimeExpression time) { return new NumericExpression("seconds-from-time(" + time.getScript() + ")"); } + @Override + public NumericExpression composeYearsFromDurationFunction(DurationExpression duration) { + return new NumericExpression("years-from-duration(" + duration.getScript() + ")"); + } + + @Override + public NumericExpression composeMonthsFromDurationFunction(DurationExpression duration) { + String d = duration.getScript(); + return new NumericExpression("(years-from-duration(" + d + ") * 12 + months-from-duration(" + d + "))"); + } + + @Override + public NumericExpression composeDaysFromDurationFunction(DurationExpression duration) { + return new NumericExpression("days-from-duration(" + duration.getScript() + ")"); + } + @Override public NumericExpression composeAbsFunction(NumericExpression number) { return new NumericExpression("abs(" + number.getScript() + ")"); diff --git a/src/test/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1Test.java b/src/test/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1Test.java index b165937c..61142bfd 100644 --- a/src/test/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1Test.java +++ b/src/test/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1Test.java @@ -122,10 +122,10 @@ void testFieldValueComparison_UsingTimeFields() { } @Test - void testFieldValueComparison_UsingMeasureFields() { + void testFieldValueComparison_UsingDurationFields() { assertEquals( - "boolean(for $T in (current-date()) return ($T + (for $F in PathNode/MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) <= $T + (for $F in PathNode/MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ()))))", - translateExpressionWithContext("ND-Root", "BT-00-Measure <= BT-00-Measure")); + "boolean(for $T in (current-date()) return ($T + (for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) <= $T + (for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ()))))", + translateExpressionWithContext("ND-Root", "BT-00-Duration <= BT-00-Duration")); } @Test @@ -267,8 +267,8 @@ void testNegativeDuration_ViaMultiplication() { @Test void testNegativeDuration_ViaMultiplicationWithField() { assertEquals( - "(-3 * (2 * (for $F in PathNode/MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ()))))", - translateExpressionWithContext("ND-Root", "2 * measure:BT-00-Measure * -3")); + "(-3 * (2 * (for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ()))))", + translateExpressionWithContext("ND-Root", "2 * measure:BT-00-Duration * -3")); } @Test @@ -382,8 +382,8 @@ void testDurationQuantifiedExpression_UsingLiterals() { @Test void testDurationQuantifiedExpression_UsingFieldReference() { testExpressionTranslationWithContext( - "every $x in (for $F in PathNode/MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) satisfies boolean(for $T in (current-date()) return ($T + $x <= $T + xs:dayTimeDuration('P1D')))", - "ND-Root", "every measure:$x in BT-00-Measure satisfies $x <= P1D"); + "every $x in (for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) satisfies boolean(for $T in (current-date()) return ($T + $x <= $T + xs:dayTimeDuration('P1D')))", + "ND-Root", "every measure:$x in BT-00-Duration satisfies $x <= P1D"); } // #endregion: Quantified expressions @@ -466,8 +466,8 @@ void testConditionalTimeExpression() { @Test void testConditionalDurationExpression() { assertEquals( - "(if boolean(for $T in (current-date()) return ($T + xs:dayTimeDuration('P1D') > $T + (for $F in PathNode/MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())))) then xs:dayTimeDuration('P1D') else xs:dayTimeDuration('P2D'))", - translateExpressionWithContext("ND-Root", "if P1D > BT-00-Measure then P1D else P2D")); + "(if boolean(for $T in (current-date()) return ($T + xs:dayTimeDuration('P1D') > $T + (for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())))) then xs:dayTimeDuration('P1D') else xs:dayTimeDuration('P2D'))", + translateExpressionWithContext("ND-Root", "if P1D > BT-00-Duration then P1D else P2D")); } // #endregion: Conditional expressions @@ -575,8 +575,8 @@ void testStringsFromDurationIteration_UsingLiterals() { @Test void testStringsFromDurationIteration_UsingFieldReference() { - testExpressionTranslationWithContext("'a' = (for $x in (for $F in PathNode/MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return 'y')", - "ND-Root", "'a' in (for measure:$x in BT-00-Measure return 'y')"); + testExpressionTranslationWithContext("'a' = (for $x in (for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return 'y')", + "ND-Root", "'a' in (for measure:$x in BT-00-Duration return 'y')"); } // Numbers from iteration --------------------------------------------------- @@ -655,8 +655,8 @@ void testNumbersFromDurationIteration_UsingLiterals() { @Test void testNumbersFromDurationIteration_UsingFieldReference() { - testExpressionTranslationWithContext("123 = (for $x in (for $F in PathNode/MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return 0)", - "ND-Root", "123 in (for measure:$x in BT-00-Measure return 0)"); + testExpressionTranslationWithContext("123 = (for $x in (for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return 0)", + "ND-Root", "123 in (for measure:$x in BT-00-Duration return 0)"); } // Dates from iteration --------------------------------------------------- @@ -746,8 +746,8 @@ void testDatesFromDurationIteration_UsingLiterals() { @Test void testDatesFromDurationIteration_UsingFieldReference() { testExpressionTranslationWithContext( - "xs:date('2022-01-01Z') = (for $x in (for $F in PathNode/MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return xs:date('2022-01-01Z'))", - "ND-Root", "2022-01-01Z in (for measure:$x in BT-00-Measure return 2022-01-01Z)"); + "xs:date('2022-01-01Z') = (for $x in (for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return xs:date('2022-01-01Z'))", + "ND-Root", "2022-01-01Z in (for measure:$x in BT-00-Duration return 2022-01-01Z)"); } // Times from iteration --------------------------------------------------- @@ -837,8 +837,8 @@ void testTimesFromDurationIteration_UsingLiterals() { @Test void testTimesFromDurationIteration_UsingFieldReference() { testExpressionTranslationWithContext( - "xs:time('12:00:00Z') = (for $x in (for $F in PathNode/MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return xs:time('12:00:00Z'))", - "ND-Root", "12:00:00Z in (for measure:$x in BT-00-Measure return 12:00:00Z)"); + "xs:time('12:00:00Z') = (for $x in (for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return xs:time('12:00:00Z'))", + "ND-Root", "12:00:00Z in (for measure:$x in BT-00-Duration return 12:00:00Z)"); } // Durations from iteration --------------------------------------------------- @@ -925,8 +925,8 @@ void testDurationsFromDurationIteration_UsingLiterals() { @Test void testDurationsFromDurationIteration_UsingFieldReference() { testExpressionTranslationWithContext( - "xs:dayTimeDuration('P1D') = (for $x in (for $F in PathNode/MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return xs:dayTimeDuration('P1D'))", - "ND-Root", "P1D in (for measure:$x in BT-00-Measure return P1D)"); + "xs:dayTimeDuration('P1D') = (for $x in (for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return xs:dayTimeDuration('P1D'))", + "ND-Root", "P1D in (for measure:$x in BT-00-Duration return P1D)"); } // #endregion: Iteration expressions @@ -1012,8 +1012,8 @@ void testDurationList_UsingDurationLiterals() { @Test void testDurationList_UsingDurationField() { assertEquals( - "(for $F in ../MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) = (xs:yearMonthDuration('P1M'),xs:yearMonthDuration('P3M'),xs:yearMonthDuration('P6M'))", - translateExpressionWithContext("BT-00-Text", "BT-00-Measure in (P1M, P3M, P6M)")); + "(for $F in ../DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) = (xs:yearMonthDuration('P1M'),xs:yearMonthDuration('P3M'),xs:yearMonthDuration('P6M'))", + translateExpressionWithContext("BT-00-Text", "BT-00-Duration in (P1M, P3M, P6M)")); } @Test @@ -1107,8 +1107,8 @@ void testSimpleFieldReference() { @Test void testFieldReference_ForDurationFields() { testExpressionTranslationWithContext( - "(for $F in PathNode/MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ()))", - "ND-Root", "BT-00-Measure"); + "(for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ()))", + "ND-Root", "BT-00-Duration"); } @Test @@ -1246,14 +1246,14 @@ void testDateFromStringFunction() { @Test void testDatePlusMeasureFunction() { - testExpressionTranslationWithContext("(PathNode/StartDateField/xs:date(text()) + (for $F in PathNode/MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())))", - "ND-Root", "add-measure(BT-00-StartDate, BT-00-Measure)"); + testExpressionTranslationWithContext("(PathNode/StartDateField/xs:date(text()) + (for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())))", + "ND-Root", "add-measure(BT-00-StartDate, BT-00-Duration)"); } @Test void testDateMinusMeasureFunction() { - testExpressionTranslationWithContext("(PathNode/StartDateField/xs:date(text()) - (for $F in PathNode/MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())))", - "ND-Root", "subtract-measure(BT-00-StartDate, BT-00-Measure)"); + testExpressionTranslationWithContext("(PathNode/StartDateField/xs:date(text()) - (for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())))", + "ND-Root", "subtract-measure(BT-00-StartDate, BT-00-Duration)"); } // #endregion: Date functions @@ -1517,7 +1517,7 @@ void testExceptFunction_WithTimeFieldReferences() { @Test void testExceptFunction_WithDurationFieldReferences() { testExpressionTranslationWithContext( - "distinct-values(for $L1 in (for $F in PathNode/MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return if (every $L2 in (for $F in PathNode/MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) satisfies $L1 != $L2) then $L1 else ())", "ND-Root", "value-except(BT-00-Measure, BT-00-Measure)"); + "distinct-values(for $L1 in (for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return if (every $L2 in (for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) satisfies $L1 != $L2) then $L1 else ())", "ND-Root", "value-except(BT-00-Duration, BT-00-Duration)"); } @Test 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 35b168d1..45410207 100644 --- a/src/test/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2Test.java +++ b/src/test/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2Test.java @@ -15,6 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; 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; @@ -176,6 +177,7 @@ void testPreferredLanguage_ThrowsInExpressionContext() { InvalidUsageException exception = assertThrows(InvalidUsageException.class, () -> translateExpressionWithContext("ND-Root", "preferred-language(BT-00-Text-Multilingual)")); assertEquals(InvalidUsageException.ErrorCode.TEMPLATE_ONLY_FUNCTION, exception.getErrorCode()); + assertTrue(exception.getMessage().startsWith("line "), "Error message should include source position"); } @Test @@ -220,10 +222,10 @@ void testFieldValueComparison_UsingTimeFields() { } @Test - void testFieldValueComparison_UsingMeasureFields() { + void testFieldValueComparison_UsingDurationFields() { assertEquals( - "boolean(for $T in (current-date()) return ($T + (for $F in PathNode/MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) <= $T + (for $F in PathNode/MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ()))))", - translateExpressionWithContext("ND-Root", "BT-00-Measure <= BT-00-Measure")); + "boolean(for $T in (current-date()) return ($T + (for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) <= $T + (for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ()))))", + translateExpressionWithContext("ND-Root", "BT-00-Duration <= BT-00-Duration")); } @Test @@ -370,8 +372,8 @@ void testNegativeDuration_ViaMultiplication() { @Test void testNegativeDuration_ViaMultiplicationWithField() { assertEquals( - "(-3 * (2 * (for $F in PathNode/MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ()))))", - translateExpressionWithContext("ND-Root", "2 * (measure)BT-00-Measure * -3")); + "(-3 * (2 * (for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ()))))", + translateExpressionWithContext("ND-Root", "2 * (duration)BT-00-Duration * -3")); } @Test @@ -479,14 +481,14 @@ void testTimeQuantifiedExpression_UsingFieldReference() { void testDurationQuantifiedExpression_UsingLiterals() { testExpressionTranslationWithContext( "every $x in (xs:dayTimeDuration('P1D'),xs:dayTimeDuration('P2D'),xs:dayTimeDuration('P3D')) satisfies boolean(for $T in (current-date()) return ($T + $x <= $T + xs:dayTimeDuration('P1D')))", - "ND-Root", "every measure:$x in [P1D, P2D, P3D] satisfies $x <= P1D"); + "ND-Root", "every duration:$x in [P1D, P2D, P3D] satisfies $x <= P1D"); } @Test void testDurationQuantifiedExpression_UsingFieldReference() { testExpressionTranslationWithContext( - "every $x in (for $F in PathNode/MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) satisfies boolean(for $T in (current-date()) return ($T + $x <= $T + xs:dayTimeDuration('P1D')))", - "ND-Root", "every measure:$x in BT-00-Measure satisfies $x <= P1D"); + "every $x in (for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) satisfies boolean(for $T in (current-date()) return ($T + $x <= $T + xs:dayTimeDuration('P1D')))", + "ND-Root", "every duration:$x in BT-00-Duration satisfies $x <= P1D"); } // #endregion: Quantified expressions @@ -569,8 +571,8 @@ void testConditionalTimeExpression() { @Test void testConditionalDurationExpression() { assertEquals( - "(if boolean(for $T in (current-date()) return ($T + xs:dayTimeDuration('P1D') > $T + (for $F in PathNode/MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())))) then xs:dayTimeDuration('P1D') else xs:dayTimeDuration('P2D'))", - translateExpressionWithContext("ND-Root", "if P1D > BT-00-Measure then P1D else P2D")); + "(if boolean(for $T in (current-date()) return ($T + xs:dayTimeDuration('P1D') > $T + (for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())))) then xs:dayTimeDuration('P1D') else xs:dayTimeDuration('P2D'))", + translateExpressionWithContext("ND-Root", "if P1D > BT-00-Duration then P1D else P2D")); } // #endregion: Conditional expressions @@ -681,14 +683,14 @@ void testStringsFromTimeIteration_UsingFieldReference() { void testStringsFromDurationIteration_UsingLiterals() { testExpressionTranslationWithContext( "'a' = (for $x in (xs:dayTimeDuration('P1D'),xs:yearMonthDuration('P1Y'),xs:yearMonthDuration('P2M')) return 'y')", - "ND-Root", "'a' in (for measure:$x in [P1D, P1Y, P2M] return 'y')"); + "ND-Root", "'a' in (for duration:$x in [P1D, P1Y, P2M] return 'y')"); } @Test void testStringsFromDurationIteration_UsingFieldReference() { - testExpressionTranslationWithContext("'a' = (for $x in (for $F in PathNode/MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return 'y')", - "ND-Root", "'a' in (for measure:$x in BT-00-Measure return 'y')"); + testExpressionTranslationWithContext("'a' = (for $x in (for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return 'y')", + "ND-Root", "'a' in (for duration:$x in BT-00-Duration return 'y')"); } // Numbers from iteration --------------------------------------------------- @@ -761,14 +763,14 @@ void testNumbersFromTimeIteration_UsingFieldReference() { void testNumbersFromDurationIteration_UsingLiterals() { testExpressionTranslationWithContext( "123 = (for $x in (xs:dayTimeDuration('P1D'),xs:yearMonthDuration('P1Y'),xs:yearMonthDuration('P2M')) return 0)", - "ND-Root", "123 in (for measure:$x in [P1D, P1Y, P2M] return 0)"); + "ND-Root", "123 in (for duration:$x in [P1D, P1Y, P2M] return 0)"); } @Test void testNumbersFromDurationIteration_UsingFieldReference() { - testExpressionTranslationWithContext("123 = (for $x in (for $F in PathNode/MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return 0)", - "ND-Root", "123 in (for measure:$x in BT-00-Measure return 0)"); + testExpressionTranslationWithContext("123 = (for $x in (for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return 0)", + "ND-Root", "123 in (for duration:$x in BT-00-Duration return 0)"); } // Dates from iteration --------------------------------------------------- @@ -851,15 +853,15 @@ void testDatesFromTimeIteration_UsingFieldReference() { void testDatesFromDurationIteration_UsingLiterals() { testExpressionTranslationWithContext( "xs:date('2022-01-01Z') = (for $x in (xs:dayTimeDuration('P1D'),xs:yearMonthDuration('P1Y'),xs:yearMonthDuration('P2M')) return xs:date('2022-01-01Z'))", - "ND-Root", "2022-01-01Z in (for measure:$x in [P1D, P1Y, P2M] return 2022-01-01Z)"); + "ND-Root", "2022-01-01Z in (for duration:$x in [P1D, P1Y, P2M] return 2022-01-01Z)"); } @Test void testDatesFromDurationIteration_UsingFieldReference() { testExpressionTranslationWithContext( - "xs:date('2022-01-01Z') = (for $x in (for $F in PathNode/MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return xs:date('2022-01-01Z'))", - "ND-Root", "2022-01-01Z in (for measure:$x in BT-00-Measure return 2022-01-01Z)"); + "xs:date('2022-01-01Z') = (for $x in (for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return xs:date('2022-01-01Z'))", + "ND-Root", "2022-01-01Z in (for duration:$x in BT-00-Duration return 2022-01-01Z)"); } // Times from iteration --------------------------------------------------- @@ -942,15 +944,15 @@ void testTimesFromTimeIteration_UsingFieldReference() { void testTimesFromDurationIteration_UsingLiterals() { testExpressionTranslationWithContext( "xs:time('12:00:00Z') = (for $x in (xs:dayTimeDuration('P1D'),xs:yearMonthDuration('P1Y'),xs:yearMonthDuration('P2M')) return xs:time('12:00:00Z'))", - "ND-Root", "12:00:00Z in (for measure:$x in [P1D, P1Y, P2M] return 12:00:00Z)"); + "ND-Root", "12:00:00Z in (for duration:$x in [P1D, P1Y, P2M] return 12:00:00Z)"); } @Test void testTimesFromDurationIteration_UsingFieldReference() { testExpressionTranslationWithContext( - "xs:time('12:00:00Z') = (for $x in (for $F in PathNode/MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return xs:time('12:00:00Z'))", - "ND-Root", "12:00:00Z in (for measure:$x in BT-00-Measure return 12:00:00Z)"); + "xs:time('12:00:00Z') = (for $x in (for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return xs:time('12:00:00Z'))", + "ND-Root", "12:00:00Z in (for duration:$x in BT-00-Duration return 12:00:00Z)"); } // Durations from iteration --------------------------------------------------- @@ -959,7 +961,7 @@ void testTimesFromDurationIteration_UsingFieldReference() { void testDurationsFromStringIteration_UsingLiterals() { testExpressionTranslationWithContext( "xs:dayTimeDuration('P1D') = (for $x in (xs:dayTimeDuration('P1D'),xs:dayTimeDuration('P2D'),xs:dayTimeDuration('P7D')) return $x)", - "ND-Root", "P1D in (for measure:$x in [P1D, P2D, P1W] return $x)"); + "ND-Root", "P1D in (for duration:$x in [P1D, P2D, P1W] return $x)"); } @Test @@ -1031,14 +1033,14 @@ void testDurationsFromTimeIteration_UsingFieldReference() { void testDurationsFromDurationIteration_UsingLiterals() { testExpressionTranslationWithContext( "xs:dayTimeDuration('P1D') = (for $x in (xs:dayTimeDuration('P1D'),xs:yearMonthDuration('P1Y'),xs:yearMonthDuration('P2M')) return xs:dayTimeDuration('P1D'))", - "ND-Root", "P1D in (for measure:$x in [P1D, P1Y, P2M] return P1D)"); + "ND-Root", "P1D in (for duration:$x in [P1D, P1Y, P2M] return P1D)"); } @Test void testDurationsFromDurationIteration_UsingFieldReference() { testExpressionTranslationWithContext( - "xs:dayTimeDuration('P1D') = (for $x in (for $F in PathNode/MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return xs:dayTimeDuration('P1D'))", - "ND-Root", "P1D in (for measure:$x in BT-00-Measure return P1D)"); + "xs:dayTimeDuration('P1D') = (for $x in (for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return xs:dayTimeDuration('P1D'))", + "ND-Root", "P1D in (for duration:$x in BT-00-Duration return P1D)"); } // Strings from concatenated iterations ----------------------------------- @@ -1159,8 +1161,8 @@ void testDurationList_UsingDurationLiterals() { @Test void testDurationList_UsingDurationField() { assertEquals( - "(for $F in ../MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) = (xs:yearMonthDuration('P1M'),xs:yearMonthDuration('P3M'),xs:yearMonthDuration('P6M'))", - translateExpressionWithContext("BT-00-Text", "BT-00-Measure in [P1M, P3M, P6M]")); + "(for $F in ../DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) = (xs:yearMonthDuration('P1M'),xs:yearMonthDuration('P3M'),xs:yearMonthDuration('P6M'))", + translateExpressionWithContext("BT-00-Text", "BT-00-Duration in [P1M, P3M, P6M]")); } @Test @@ -1273,8 +1275,8 @@ void testSimpleFieldReference() { @Test void testFieldReference_ForDurationFields() { testExpressionTranslationWithContext( - "(for $F in PathNode/MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ()))", - "ND-Root", "BT-00-Measure"); + "(for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ()))", + "ND-Root", "BT-00-Duration"); } /** @@ -1402,6 +1404,7 @@ void testComputedProperty_wasWithheld_onNonWithholdableField() { () -> translateExpressionWithContext("BT-00-Text", "BT-00-Text:wasWithheld")); assertEquals(InvalidUsageException.ErrorCode.FIELD_NOT_WITHHOLDABLE, exception.getErrorCode()); + assertTrue(exception.getMessage().startsWith("line "), "Error message should include source position"); } @Test @@ -1565,6 +1568,32 @@ void testMetadataProperty_privacyCode_onNonWithholdableField() { assertEquals(InvalidUsageException.ErrorCode.FIELD_NOT_WITHHOLDABLE, exception.getErrorCode()); } + // rawValue property tests + + @Test + void testFieldRawValue_Duration() { + testExpressionTranslationWithContext( + "PathNode/DurationField/normalize-space(text())", + "ND-Root", + "BT-00-Duration:rawValue"); + } + + @Test + void testFieldRawValue_Text() { + testExpressionTranslationWithContext( + "PathNode/TextField/normalize-space(text())", + "ND-Root", + "BT-00-Text:rawValue"); + } + + @Test + void testFieldRawValue_Number() { + testExpressionTranslationWithContext( + "PathNode/NumberField/normalize-space(text())", + "ND-Root", + "BT-00-Number:rawValue"); + } + // #endregion: Boolean functions // #region: Numeric functions ----------------------------------------------- @@ -1599,6 +1628,30 @@ void testNumberFromBooleanFunction_WithFieldReference() { "number(BT-00-Indicator)"); } + @Test + void testNumberFromDurationField_ExplicitRawValue() { + testExpressionTranslationWithContext( + "number(PathNode/DurationField/normalize-space(text()))", + "ND-Root", + "number(BT-00-Duration:rawValue)"); + } + + @Test + void testFieldRawValue_NumberLikePattern() { + testExpressionTranslationWithContext( + "fn:matches(normalize-space(PathNode/NumberField/normalize-space(text())), '[0-9]+\\.[0-9]+')", + "ND-Root", + "BT-00-Number:rawValue like '[0-9]+\\.[0-9]+'"); + } + + @Test + void testNumberFromDurationRawValue_Comparison() { + testExpressionTranslationWithContext( + "number(PathNode/DurationField/normalize-space(text())) > 3", + "ND-Root", + "number(BT-00-Duration:rawValue) > 3"); + } + @Test void testSumFunction_UsingFieldReference() { testExpressionTranslationWithContext("sum(PathNode/NumberField/number())", "ND-Root", @@ -1720,6 +1773,62 @@ void testSecondsFromTimeFunction_WithFieldReference() { "seconds(BT-00-StartTime)"); } + @Test + void testYearsFromDurationFunction() { + testExpressionTranslationWithContext( + "years-from-duration(xs:yearMonthDuration('P2Y'))", "ND-Root", + "years(P2Y)"); + } + + @Test + void testYearsFromDurationFunction_WithDayTimeDuration() { + testExpressionTranslationWithContext( + "years-from-duration(xs:dayTimeDuration('P10D'))", "ND-Root", + "years(P10D)"); + } + + @Test + void testYearsFromDurationFunction_WithFieldReference() { + testExpressionTranslationWithContext( + "years-from-duration((for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())))", + "ND-Root", "years(BT-00-Duration)"); + } + + @Test + void testMonthsFromDurationFunction() { + testExpressionTranslationWithContext( + "(years-from-duration(xs:yearMonthDuration('P3M')) * 12 + months-from-duration(xs:yearMonthDuration('P3M')))", + "ND-Root", "months(P3M)"); + } + + @Test + void testMonthsFromDurationFunction_WithYears() { + testExpressionTranslationWithContext( + "(years-from-duration(xs:yearMonthDuration('P2Y')) * 12 + months-from-duration(xs:yearMonthDuration('P2Y')))", + "ND-Root", "months(P2Y)"); + } + + @Test + void testMonthsFromDurationFunction_TotalMonths() { + testExpressionTranslationWithContext( + "(years-from-duration(xs:yearMonthDuration('P12M')) * 12 + months-from-duration(xs:yearMonthDuration('P12M')))", + "ND-Root", "months(P12M)"); + } + + @Test + void testDaysFromDurationFunction() { + testExpressionTranslationWithContext( + "days-from-duration(xs:dayTimeDuration('P10D'))", "ND-Root", + "days(P10D)"); + } + + @Test + void testDaysFromDurationFunction_WithWeeks() { + testExpressionTranslationWithContext( + "days-from-duration(xs:dayTimeDuration('P21D'))", "ND-Root", + "days(P3W)"); + } + @Test void testAbsoluteFunction() { testExpressionTranslationWithContext("abs(-5)", "ND-Root", "absolute(-5)"); @@ -2126,8 +2235,8 @@ void testDurationToStringFunction() { @Test void testDurationToStringFunction_WithFieldReference() { testExpressionTranslationWithContext( - "string((for $F in PathNode/MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())))", - "ND-Root", "string(BT-00-Measure)"); + "string((for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())))", + "ND-Root", "string(BT-00-Duration)"); } // text() variants - verify that 'text' keyword works as alias for 'string' conversion @@ -2474,7 +2583,7 @@ void testExceptFunction_WithTimeFieldReferences() { @Test void testExceptFunction_WithDurationFieldReferences() { testExpressionTranslationWithContext( - "distinct-values(for $L1 in (for $F in PathNode/MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return if (every $L2 in (for $F in PathNode/MeasureField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) satisfies $L1 != $L2) then $L1 else ())", "ND-Root", "value-except(BT-00-Measure, BT-00-Measure)"); + "distinct-values(for $L1 in (for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return if (every $L2 in (for $F in PathNode/DurationField 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')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) satisfies $L1 != $L2) then $L1 else ())", "ND-Root", "value-except(BT-00-Duration, BT-00-Duration)"); } @Test @@ -2944,7 +3053,7 @@ void testParameterizedExpression_WithBooleanParameter() { void testParameterizedExpression_WithDurationParameter() { testExpressionTranslation( "boolean(for $T in (current-date()) return ($T + xs:yearMonthDuration('P1Y') = $T + xs:yearMonthDuration('P2Y')))", - "{ND-Root, measure:$p1, measure:$p2} ${$p1 == $p2}", "P1Y", "P2Y"); + "{ND-Root, duration:$p1, duration:$p2} ${$p1 == $p2}", "P1Y", "P2Y"); } @Test @@ -2980,7 +3089,7 @@ void testParameterizedExpression_WithTimeSequenceParameter() { @Test void testParameterizedExpression_WithDurationSequenceParameter() { testExpressionTranslation("count((xs:yearMonthDuration('P1Y'),xs:yearMonthDuration('P2M')))", - "{ND-Root, measure*:$items} ${count($items)}", "[P1Y, P2M]"); + "{ND-Root, duration*:$items} ${count($items)}", "[P1Y, P2M]"); } // #endregion: Compare sequences @@ -3036,6 +3145,7 @@ void testScalarFromRepeatableField_ThrowsError() { TypeMismatchException ex = assertThrows(TypeMismatchException.class, () -> translateExpressionWithContext("ND-Root", "BT-00-Repeatable-Text == 'test'")); assertEquals(TypeMismatchException.ErrorCode.EXPECTED_SCALAR, ex.getErrorCode()); + assertTrue(ex.getMessage().startsWith("line "), "Error message should include source position"); } @Test @@ -3109,6 +3219,7 @@ void testContextSpecifier_WithRegularVariable_ThrowsNotAContextVariable() { 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()); + assertTrue(ex.getMessage().startsWith("line "), "Error message should include source position"); } @Test 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 e398c24f..f987e285 100644 --- a/src/test/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2Test.java +++ b/src/test/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2Test.java @@ -150,7 +150,7 @@ void testTemplateDeclaration_AllParameterTypes() { "MAIN:", "for-each(/*).call(body02())"), translateTemplate(lines( - "let template:all-types-template(text:$str, number:$num, indicator:$bool, date:$dt, time:$tm, measure:$dur) display Params: ${$str}, ${$num}, ${$bool}, ${$dt}, ${$tm}, ${$dur};", + "let template:all-types-template(text:$str, number:$num, indicator:$bool, date:$dt, time:$tm, duration:$dur) display Params: ${$str}, ${$num}, ${$bool}, ${$dt}, ${$tm}, ${$dur};", "invoke all-types-template('text', 123, TRUE, date('2023-01-01'), time('12:00:00'), day-time-duration('P1D'));"))); } @@ -590,7 +590,7 @@ void testDisplayTemplate_WithDurationSequenceVariable() { "MAIN:", "for-each(/*).call(body01())"), translateTemplate(lines( - "let measure*:$durs = [P1Y, P2M];", + "let duration*:$durs = [P1Y, P2M];", "display count: ${count($durs)};"))); } @@ -684,7 +684,7 @@ void testDisplayTemplate_WithDurationSequenceFunction() { "MAIN:", "for-each(/*).call(body01())"), translateTemplate(lines( - "let measure*:?getDurations() = [P1Y, P2M];", + "let duration*:?getDurations() = [P1Y, P2M];", "display count: ${count(?getDurations())};"))); } @@ -778,7 +778,7 @@ void testDisplayTemplate_WithDurationSequenceParameter() { "MAIN:", "for-each(/*).call(body01())"), translateTemplate(lines( - "let measure*:?processDurations(measure*:$durs) = $durs;", + "let duration*:?processDurations(duration*:$durs) = $durs;", "display done;"))); } @@ -1768,7 +1768,7 @@ void testVariableList_WithAllDataTypes() { "MAIN:", "for-each(/*).call(body01(string:str='text', decimal:num=42, boolean:bool=true(), date:dt=xs:date('2023-01-01'), time:tm=xs:time('12:00:00'), duration:dur=xs:dayTimeDuration('P1D')))"), translateTemplate( - "{/, text:$str='text', number:$num=42, indicator:$bool=TRUE, date:$dt=date('2023-01-01'), time:$tm=time('12:00:00'), measure:$dur=day-time-duration('P1D')} All types: ${$str}, ${$num}, ${$bool}, ${$dt}, ${$tm}, ${$dur}")); + "{/, text:$str='text', number:$num=42, indicator:$bool=TRUE, date:$dt=date('2023-01-01'), time:$tm=time('12:00:00'), duration:$dur=day-time-duration('P1D')} All types: ${$str}, ${$num}, ${$bool}, ${$dt}, ${$tm}, ${$dur}")); } @Test @@ -1819,8 +1819,10 @@ void testTemplateLine_InvalidIndentation_FirstIndentation() { @Test void testTemplateLine_InvalidIndentation_MixedIndentation() { - assertThrows(InvalidIndentationException.class, + InvalidIndentationException ex = assertThrows(InvalidIndentationException.class, () -> translateTemplate("{BT-00-Text} foo\n\t {BT-00-Text} bar")); + assertEquals(InvalidIndentationException.ErrorCode.MIXED_INDENTATION, ex.getErrorCode()); + assertTrue(ex.getMessage().startsWith("line "), "Error message should include source position"); } @Test diff --git a/src/test/java/eu/europa/ted/efx/sdk2/SdkSymbolResolverTest.java b/src/test/java/eu/europa/ted/efx/sdk2/SdkSymbolResolverTest.java index 07b3ba0c..9240e80f 100644 --- a/src/test/java/eu/europa/ted/efx/sdk2/SdkSymbolResolverTest.java +++ b/src/test/java/eu/europa/ted/efx/sdk2/SdkSymbolResolverTest.java @@ -389,11 +389,11 @@ void timeField_shouldReturnTimePath() { } @Test - @DisplayName("Measure field returns DurationPath") - void measureField_shouldReturnDurationPath() { - PathExpression path = resolver.getAbsolutePathOfField("BT-00-Measure"); + @DisplayName("Duration field returns DurationPath") + void durationField_shouldReturnDurationPath() { + PathExpression path = resolver.getAbsolutePathOfField("BT-00-Duration"); assertEquals(DurationPath.class, path.getClass(), - "Measure field should return DurationPath"); + "Duration field should return DurationPath"); } @Test 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 index 5ab74e28..9f43cf50 100644 --- 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 @@ -1,4 +1,4 @@ -LET measure* : $durations = [P1D, P2D]; +LET duration* : $durations = [P1D, P2D]; ---- STAGE 1a ---- diff --git a/src/test/resources/json/sdk1-fields.json b/src/test/resources/json/sdk1-fields.json index f2f38702..1a5b5a72 100644 --- a/src/test/resources/json/sdk1-fields.json +++ b/src/test/resources/json/sdk1-fields.json @@ -115,11 +115,11 @@ "xpathRelative": "PathNode/EndTimeField" }, { - "id": "BT-00-Measure", - "type": "measure", + "id": "BT-00-Duration", + "type": "duration", "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/MeasureField", - "xpathRelative": "PathNode/MeasureField" + "xpathAbsolute": "/*/PathNode/DurationField", + "xpathRelative": "PathNode/DurationField" }, { "id": "BT-00-Integer", diff --git a/src/test/resources/json/sdk2-fields.json b/src/test/resources/json/sdk2-fields.json index 9dcf766d..04bde8b9 100644 --- a/src/test/resources/json/sdk2-fields.json +++ b/src/test/resources/json/sdk2-fields.json @@ -222,12 +222,12 @@ "xpathRelative": "PathNode/EndTimeField" }, { - "id": "BT-00-Measure", - "alias": "measureField", - "type": "measure", + "id": "BT-00-Duration", + "alias": "durationField", + "type": "duration", "parentNodeId": "ND-Root", - "xpathAbsolute": "/*/PathNode/MeasureField", - "xpathRelative": "PathNode/MeasureField" + "xpathAbsolute": "/*/PathNode/DurationField", + "xpathRelative": "PathNode/DurationField" }, { "id": "BT-00-Integer", @@ -502,12 +502,12 @@ "xpathRelative": "Time" }, { - "id": "BT-11-Measure", - "alias": "bt11Measure", - "type": "measure", + "id": "BT-11-Duration", + "alias": "bt11Duration", + "type": "duration", "parentNodeId": "ND-SubSubNode", - "xpathAbsolute": "/*/SubNode/SubSubNode/Measure", - "xpathRelative": "Measure" + "xpathAbsolute": "/*/SubNode/SubSubNode/Duration", + "xpathRelative": "Duration" }, { "id": "BT-12-Text", @@ -558,12 +558,12 @@ "xpathRelative": "Time" }, { - "id": "BT-12-Measure", - "alias": "bt12Measure", - "type": "measure", + "id": "BT-12-Duration", + "alias": "bt12Duration", + "type": "duration", "parentNodeId": "ND-SubSubNode2", - "xpathAbsolute": "/*/SubNode/SubSubNode2/Measure", - "xpathRelative": "Measure" + "xpathAbsolute": "/*/SubNode/SubSubNode2/Duration", + "xpathRelative": "Duration" }, { "id": "BT-13-Text", @@ -614,12 +614,12 @@ "xpathRelative": "Time" }, { - "id": "BT-13-Measure", - "alias": "bt13Measure", - "type": "measure", + "id": "BT-13-Duration", + "alias": "bt13Duration", + "type": "duration", "parentNodeId": "ND-RepeatableInSubNode", - "xpathAbsolute": "/*/SubNode/RepeatableInSubNode/Measure", - "xpathRelative": "Measure" + "xpathAbsolute": "/*/SubNode/RepeatableInSubNode/Duration", + "xpathRelative": "Duration" }, { "id": "BT-14-Text", @@ -670,12 +670,12 @@ "xpathRelative": "Time" }, { - "id": "BT-14-Measure", - "alias": "bt14Measure", - "type": "measure", + "id": "BT-14-Duration", + "alias": "bt14Duration", + "type": "duration", "parentNodeId": "ND-RepeatableInSubNode2", - "xpathAbsolute": "/*/SubNode/RepeatableInSubNode2/Measure", - "xpathRelative": "Measure" + "xpathAbsolute": "/*/SubNode/RepeatableInSubNode2/Duration", + "xpathRelative": "Duration" }, { "id": "BT-21-TextMultilingual", @@ -718,12 +718,12 @@ "xpathRelative": "Time" }, { - "id": "BT-21-Measure", - "alias": "bt21Measure", - "type": "measure", + "id": "BT-21-Duration", + "alias": "bt21Duration", + "type": "duration", "parentNodeId": "ND-NonRepeatableSubNode", - "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode/Measure", - "xpathRelative": "Measure" + "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode/Duration", + "xpathRelative": "Duration" }, { "id": "BT-22-Text", @@ -774,12 +774,12 @@ "xpathRelative": "Time" }, { - "id": "BT-22-Measure", - "alias": "bt22Measure", - "type": "measure", + "id": "BT-22-Duration", + "alias": "bt22Duration", + "type": "duration", "parentNodeId": "ND-NonRepeatableSubNode2", - "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode2/Measure", - "xpathRelative": "Measure" + "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode2/Duration", + "xpathRelative": "Duration" }, { "id": "BT-23-Text", @@ -830,12 +830,12 @@ "xpathRelative": "Time" }, { - "id": "BT-23-Measure", - "alias": "bt23Measure", - "type": "measure", + "id": "BT-23-Duration", + "alias": "bt23Duration", + "type": "duration", "parentNodeId": "ND-RepeatableInRepeatableNode", - "xpathAbsolute": "/*/RepeatableNode/RepeatableInRepeatableNode/Measure", - "xpathRelative": "Measure" + "xpathAbsolute": "/*/RepeatableNode/RepeatableInRepeatableNode/Duration", + "xpathRelative": "Duration" }, { "id": "BT-24-Text", @@ -886,12 +886,12 @@ "xpathRelative": "Time" }, { - "id": "BT-24-Measure", - "alias": "bt24Measure", - "type": "measure", + "id": "BT-24-Duration", + "alias": "bt24Duration", + "type": "duration", "parentNodeId": "ND-RepeatableInRepeatableNode2", - "xpathAbsolute": "/*/RepeatableNode/RepeatableInRepeatableNode2/Measure", - "xpathRelative": "Measure" + "xpathAbsolute": "/*/RepeatableNode/RepeatableInRepeatableNode2/Duration", + "xpathRelative": "Duration" }, { "id": "BT-25-TextMultilingual", @@ -934,12 +934,12 @@ "xpathRelative": "Time" }, { - "id": "BT-25-Measure", - "alias": "bt25Measure", - "type": "measure", + "id": "BT-25-Duration", + "alias": "bt25Duration", + "type": "duration", "parentNodeId": "ND-RepeatableSubSubNode", - "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode/RepeatableSubSubNode/Measure", - "xpathRelative": "Measure" + "xpathAbsolute": "/*/RepeatableNode/NonRepeatableSubNode/RepeatableSubSubNode/Duration", + "xpathRelative": "Duration" }, { "id": "BT-00-Number-In-Repeatable-Node",