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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 71 additions & 9 deletions src/main/java/eu/europa/ted/eforms/sdk/SdkSymbolResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,26 @@
import java.util.function.Function;
import java.util.stream.Collectors;

import eu.europa.ted.efx.exceptions.ConsistencyCheckException;
import eu.europa.ted.efx.exceptions.SdkInconsistencyException;
import eu.europa.ted.efx.exceptions.SymbolResolutionException;

import eu.europa.ted.eforms.sdk.component.SdkComponent;
import eu.europa.ted.eforms.sdk.component.SdkComponentType;
import eu.europa.ted.eforms.sdk.entity.SdkCodelist;
import eu.europa.ted.eforms.sdk.entity.SdkField;
import eu.europa.ted.eforms.sdk.entity.SdkNode;
import eu.europa.ted.eforms.sdk.entity.SdkDataType;
import eu.europa.ted.eforms.sdk.entity.SdkNoticeSubtype;
import eu.europa.ted.eforms.sdk.repository.SdkCodelistRepository;
import eu.europa.ted.eforms.sdk.repository.SdkDataTypeRepository;
import eu.europa.ted.eforms.sdk.repository.SdkFieldRepository;
import eu.europa.ted.eforms.sdk.repository.SdkNodeRepository;
import eu.europa.ted.eforms.sdk.repository.SdkNoticeTypeRepository;
import eu.europa.ted.eforms.sdk.resource.SdkResourceLoader;
import eu.europa.ted.eforms.xpath.XPathProcessor;
import eu.europa.ted.efx.interfaces.SymbolResolver;
import eu.europa.ted.efx.model.PrivacySetting;
import eu.europa.ted.efx.model.expressions.PathExpression;
import eu.europa.ted.efx.model.expressions.scalar.NodePath;
import eu.europa.ted.efx.model.expressions.scalar.ScalarPath;
Expand All @@ -61,6 +66,8 @@ public class SdkSymbolResolver implements SymbolResolver {

protected Map<String, SdkNoticeSubtype> noticeTypesById;

protected SdkDataTypeRepository dataTypeById;

private SdkNode cachedRootNode;

@Override
Expand Down Expand Up @@ -110,6 +117,7 @@ protected void loadMapData(final String sdkVersion, final Path sdkRootPath)

this.codelistById = new SdkCodelistRepository(sdkVersion, codelistsPath);
this.noticeTypesById = new SdkNoticeTypeRepository(sdkVersion, noticeTypesPath);
this.dataTypeById = SdkDataTypeRepository.createDefault();
}

@Override
Expand Down Expand Up @@ -159,7 +167,7 @@ public PathExpression getAbsolutePathOfNode(final String nodeId) {
}

private PathExpression getAbsolutePathOfNode(final SdkNode sdkNode) {
if (this.isNodeRepeatableFromContext(sdkNode, null)) {
if (this.isNodeRepeatableFromContext(sdkNode, this.getRootNode())) {
return new NodeSequencePath(sdkNode.getXpathAbsolute());
} else {
return new NodePath(sdkNode.getXpathAbsolute());
Expand Down Expand Up @@ -343,13 +351,16 @@ public PathExpression getAbsolutePathOfFieldWithoutTheAttribute(final String fie
throw SymbolResolutionException.unknownSymbol(fieldId);
}

String pathToElement = sdkField.getXpathInfo().getPathToLastElement();
SdkNode parentNode = sdkField.getParentNode();
if (!sdkField.getXpathInfo().isAttribute()) {
return this.getAbsolutePathOfField(sdkField);
}

if (parentNode != null && this.isNodeRepeatableFromContext(parentNode, null)) {
return new NodeSequencePath(pathToElement);
String pathToElement = sdkField.getXpathInfo().getPathToLastElement();
FieldTypes fieldType = FieldTypes.fromString(sdkField.getType());
if (this.isFieldRepeatableFromContext(sdkField, this.getRootNode())) {
return SequencePath.instantiate(pathToElement, fieldType);
} else {
return new NodePath(pathToElement);
return ScalarPath.instantiate(pathToElement, fieldType);
}
}

Expand Down Expand Up @@ -414,9 +425,10 @@ public boolean isFieldRepeatableFromContext(final String fieldId, final String c
}

private boolean isFieldRepeatableFromContext(final SdkField sdkField, final SdkField context) {
// If the field itself is repeatable, it returns multiple values
// If the field itself is repeatable, it returns multiple values UNLESS it IS the context
// (e.g., inside a predicate on this field: BT-Repeatable[BT-Repeatable != ''])
if (sdkField.isRepeatable()) {
return true;
return !sdkField.equals(context);
}

// Use cached ancestry from node
Expand Down Expand Up @@ -499,7 +511,7 @@ public boolean isNodeRepeatableFromContext(final String nodeId, final String con

final SdkNode contextNode = contextNodeId != null
? this.resolveNode(contextNodeId)
: null;
: this.getRootNode();
if (contextNodeId != null && contextNode == null) {
throw SymbolResolutionException.unknownSymbol(contextNodeId);
}
Expand Down Expand Up @@ -618,4 +630,54 @@ private SdkNode resolveNode(String nodeId) {

// #endregion Identifier Resolution ------------------------------------------------

@Override
public String getPrivacyCodeOfField(final String fieldId) {
final SdkField sdkField = this.resolveField(fieldId);
if (sdkField == null) {
throw SymbolResolutionException.unknownSymbol(fieldId);
}

return sdkField.getPrivacyCode();
}

@Override
public String getPrivacySettingOfField(final String fieldId, final PrivacySetting privacyField) {
final SdkField sdkField = this.resolveField(fieldId);
if (sdkField == null) {
throw SymbolResolutionException.unknownSymbol(fieldId);
}

final SdkField.PrivacySettings privacy = sdkField.getPrivacySettings();
if (privacy == null) {
return null;
}

switch (privacyField) {
case PRIVACY_CODE_FIELD:
return privacy.getPrivacyCodeFieldId();
case PUBLICATION_DATE_FIELD:
return privacy.getPublicationDateFieldId();
case JUSTIFICATION_CODE_FIELD:
return privacy.getJustificationCodeFieldId();
case JUSTIFICATION_DESCRIPTION_FIELD:
return privacy.getJustificationDescriptionFieldId();
default:
throw ConsistencyCheckException.unhandledPrivacySetting(privacyField);
}
}

@Override
public String getPrivacyMask(final String fieldId) {
final SdkField sdkField = this.resolveField(fieldId);
if (sdkField == null) {
throw SymbolResolutionException.unknownSymbol(fieldId);
}

final SdkDataType dataType = this.dataTypeById.get(sdkField.getType());
if (dataType == null) {
throw SdkInconsistencyException.unknownDataType(sdkField.getType());
}
return dataType.getPrivacyMask();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ public enum ErrorCode {
MISSING_TYPE_MAPPING,
MISSING_TYPE_ANNOTATION,
UNKNOWN_EXPRESSION_TYPE,
INVALID_VARIABLE_CONTEXT
INVALID_VARIABLE_CONTEXT,
UNHANDLED_PRIVACY_SETTING,
UNHANDLED_LINKED_FIELD_PROPERTY
}

private static final String TYPE_NOT_REGISTERED =
Expand Down Expand Up @@ -61,6 +63,16 @@ public enum ErrorCode {
"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) {
Expand Down Expand Up @@ -100,4 +112,14 @@ public static ConsistencyCheckException unknownExpressionType(Class<?> type) {
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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ public enum ErrorCode {
SHORTHAND_REQUIRES_CODE_OR_INDICATOR,
SHORTHAND_REQUIRES_FIELD_CONTEXT,
INVALID_NOTICE_SUBTYPE_RANGE_ORDER,
INVALID_NOTICE_SUBTYPE_TOKEN
INVALID_NOTICE_SUBTYPE_TOKEN,
FIELD_NOT_WITHHOLDABLE
}

private static final String SHORTHAND_REQUIRES_CODE_OR_INDICATOR = "Indirect label reference shorthand #{%1$s}, requires a field of type 'code' or 'indicator'. Field %1$s is of type %2$s.";
private static final String SHORTHAND_REQUIRES_FIELD_CONTEXT = "The %s shorthand syntax can only be used when a field is declared as context.";
private static final String INVALID_NOTICE_SUBTYPE_RANGE_ORDER = "Notice subtype range '%s-%s' is not in ascending order.";
private static final String INVALID_NOTICE_SUBTYPE_TOKEN = "Invalid notice subtype token '%s'. Expected format: 'X' or 'X-Y'.";
private static final String FIELD_NOT_WITHHOLDABLE = "Field '%s' is always published and cannot be withheld from publication.";

private final ErrorCode errorCode;

Expand Down Expand Up @@ -59,4 +61,8 @@ public static InvalidUsageException invalidNoticeSubtypeRangeOrder(String start,
public static InvalidUsageException invalidNoticeSubtypeToken(String token) {
return new InvalidUsageException(ErrorCode.INVALID_NOTICE_SUBTYPE_TOKEN, String.format(INVALID_NOTICE_SUBTYPE_TOKEN, token));
}
}

public static InvalidUsageException fieldNotWithholdable(String fieldId) {
return new InvalidUsageException(ErrorCode.FIELD_NOT_WITHHOLDABLE, String.format(FIELD_NOT_WITHHOLDABLE, fieldId));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* 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 inconsistent data in the SDK.
* This indicates a problem with the SDK data, not a user error or a toolkit bug.
*/
public class SdkInconsistencyException extends IllegalStateException {

public enum ErrorCode {
MISSING_PRIVACY_CODE_FIELD,
MISSING_PUBLICATION_DATE_FIELD,
UNKNOWN_DATA_TYPE
}

private static final String MISSING_PRIVACY_CODE_FIELD =
"Field '%s' has a privacy code but no privacy code field ID. "
+ "This indicates inconsistent privacy settings in the SDK data.";

private static final String MISSING_PUBLICATION_DATE_FIELD =
"Field '%s' has a privacy code but no publication date field ID. "
+ "This indicates inconsistent privacy settings in the SDK data.";

private static final String UNKNOWN_DATA_TYPE =
"Unknown data type '%s'. "
+ "This indicates a field type that is not defined in the SDK.";

private final ErrorCode errorCode;

private SdkInconsistencyException(ErrorCode errorCode, String message) {
super(message);
this.errorCode = errorCode;
}

public ErrorCode getErrorCode() {
return errorCode;
}

public static SdkInconsistencyException missingPrivacyCodeField(String fieldId) {
return new SdkInconsistencyException(ErrorCode.MISSING_PRIVACY_CODE_FIELD,
String.format(MISSING_PRIVACY_CODE_FIELD, fieldId));
}

public static SdkInconsistencyException missingPublicationDateField(String fieldId) {
return new SdkInconsistencyException(ErrorCode.MISSING_PUBLICATION_DATE_FIELD,
String.format(MISSING_PUBLICATION_DATE_FIELD, fieldId));
}

public static SdkInconsistencyException unknownDataType(String fieldType) {
return new SdkInconsistencyException(ErrorCode.UNKNOWN_DATA_TYPE,
String.format(UNKNOWN_DATA_TYPE, fieldType));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ public class TypeMismatchException extends ParseCancellationException {
public enum ErrorCode {
CANNOT_CONVERT,
CANNOT_COMPARE,
EXPECTED_SEQUENCE,
EXPECTED_SCALAR,
EXPECTED_FIELD_CONTEXT
}

private static final String CANNOT_CONVERT = "Type mismatch. Expected %s instead of %s.";
private static final String CANNOT_COMPARE = "Type mismatch. Cannot compare values of different types: %s and %s";
private static final String EXPECTED_SEQUENCE = "Type mismatch. Field '%s' may return multiple values from context '%s', but is used as a scalar. Use a sequence expression or change the context.";
private static final String EXPECTED_SCALAR = "Type mismatch. Field '%s' may return multiple values from context '%s', but is used as a scalar. Use a sequence expression or change the context.";
private static final String EXPECTED_FIELD_CONTEXT = "Type mismatch. Context variable '$%s' refers to node '%s', but is used as a value. Only field context variables can be used in value expressions.";

private final ErrorCode errorCode;
Expand Down Expand Up @@ -71,7 +71,7 @@ public static TypeMismatchException cannotCompare(Expression left, Expression ri
}

public static TypeMismatchException fieldMayRepeat(String fieldId, String contextSymbol) {
return new TypeMismatchException(ErrorCode.EXPECTED_SEQUENCE, String.format(EXPECTED_SEQUENCE, fieldId,
return new TypeMismatchException(ErrorCode.EXPECTED_SCALAR, String.format(EXPECTED_SCALAR, fieldId,
contextSymbol != null ? contextSymbol : "root"));
}

Expand Down
44 changes: 44 additions & 0 deletions src/main/java/eu/europa/ted/efx/interfaces/ScriptGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,13 @@
import eu.europa.ted.efx.model.expressions.scalar.ScalarExpression;
import eu.europa.ted.efx.model.expressions.scalar.StringExpression;
import eu.europa.ted.efx.model.expressions.scalar.TimeExpression;
import eu.europa.ted.efx.model.expressions.sequence.BooleanSequenceExpression;
import eu.europa.ted.efx.model.expressions.sequence.DateSequenceExpression;
import eu.europa.ted.efx.model.expressions.sequence.DurationSequenceExpression;
import eu.europa.ted.efx.model.expressions.sequence.NumericSequenceExpression;
import eu.europa.ted.efx.model.expressions.sequence.SequenceExpression;
import eu.europa.ted.efx.model.expressions.sequence.StringSequenceExpression;
import eu.europa.ted.efx.model.expressions.sequence.TimeSequenceExpression;

/**
* A ScriptGenerator is used by the EFX expression translator to translate specific computations to
Expand Down Expand Up @@ -412,9 +416,42 @@ public StringExpression composeSubstringExtraction(StringExpression text, Numeri

public BooleanExpression composeExistsCondition(PathExpression reference);

/**
* Uniqueness check for EFX 1 syntax.
* <p>
* This method supports the limited uniqueness syntax available in EFX 1.
* It is used exclusively by the EFX 1 translator and is kept for backward
* compatibility with EFX 1.
* <p>
* <b>EFX 2 does not use this method.</b> EFX 2's stricter type checking enables
* more powerful uniqueness syntax, supported by the typed overloads below.
*
* @param needle The value to check for uniqueness
* @param haystack The collection to search within
* @return A boolean expression evaluating to true if needle appears exactly once in haystack
*/
public BooleanExpression composeUniqueValueCondition(PathExpression needle,
PathExpression haystack);

// Typed uniqueness conditions (EFX 2)
public BooleanExpression composeUniqueValueCondition(StringExpression needle,
StringSequenceExpression haystack);

public BooleanExpression composeUniqueValueCondition(NumericExpression needle,
NumericSequenceExpression haystack);

public BooleanExpression composeUniqueValueCondition(BooleanExpression needle,
BooleanSequenceExpression haystack);

public BooleanExpression composeUniqueValueCondition(DateExpression needle,
DateSequenceExpression haystack);

public BooleanExpression composeUniqueValueCondition(TimeExpression needle,
TimeSequenceExpression haystack);

public BooleanExpression composeUniqueValueCondition(DurationExpression needle,
DurationSequenceExpression haystack);

public BooleanExpression composeSequenceEqualFunction(SequenceExpression one,
SequenceExpression two);

Expand All @@ -430,6 +467,13 @@ public DateExpression composeAddition(final DateExpression date,
public DateExpression composeSubtraction(final DateExpression date,
final DurationExpression duration);

/**
* Returns the current date as a date expression in the target language.
*
* @return A date expression representing today's date.
*/
public DateExpression getCurrentDate();

//#endregion Date Functions -------------------------------------------------

// #region Time Functions ---------------------------------------------------
Expand Down
Loading
Loading