diff --git a/pom.xml b/pom.xml index b0b5560b..97df6097 100644 --- a/pom.xml +++ b/pom.xml @@ -60,7 +60,7 @@ 1.13.0 - 2.0.0-alpha.1 + 2.0.0-SNAPSHOT 1.4.0 diff --git a/src/main/java/eu/europa/ted/efx/EfxTranslator.java b/src/main/java/eu/europa/ted/efx/EfxTranslator.java index 22b9d504..498b5487 100644 --- a/src/main/java/eu/europa/ted/efx/EfxTranslator.java +++ b/src/main/java/eu/europa/ted/efx/EfxTranslator.java @@ -16,6 +16,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.file.Path; + import eu.europa.ted.efx.component.EfxTranslatorFactory; import eu.europa.ted.efx.interfaces.TranslatorDependencyFactory; import eu.europa.ted.efx.interfaces.TranslatorOptions; @@ -25,9 +26,15 @@ * an EFX translator to translate EFX expressions and templates. */ public class EfxTranslator { + + private EfxTranslator() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + } private static TranslatorOptions defaultOptions = EfxTranslatorOptions.DEFAULT; + //#region Translate EFX expressions ----------------------------------------- + /** * Instantiates an EFX expression translator and translates a given expression. * @@ -54,6 +61,10 @@ public static String translateExpression(final TranslatorDependencyFactory depen return translateExpression(dependencyFactory, sdkVersion, expression, defaultOptions, expressionParameters); } + //#endregion Translate EFX expressions -------------------------------------- + + //#region Translate EFX templates ------------------------------------------- + /** * Instantiates an EFX template translator and translates the EFX template contained in the given * file. @@ -82,6 +93,14 @@ public static String translateTemplate(final TranslatorDependencyFactory depende return translateTemplate(dependencyFactory, sdkVersion, pathname, defaultOptions); } + public static String translateTemplate(final TranslatorDependencyFactory dependencyFactory, final String sdkVersion, + final String qualifier, + final Path pathname, TranslatorOptions options) + throws IOException, InstantiationException { + return EfxTranslatorFactory.getEfxTemplateTranslator(sdkVersion, qualifier, dependencyFactory, options) + .renderTemplate(pathname); + } + /** * Instantiates an EFX template translator and translates the given EFX template. * @@ -98,8 +117,7 @@ public static String translateTemplate(final TranslatorDependencyFactory depende public static String translateTemplate(final TranslatorDependencyFactory dependencyFactory, final String sdkVersion, final String template, TranslatorOptions options) throws InstantiationException { - return EfxTranslatorFactory.getEfxTemplateTranslator(sdkVersion, dependencyFactory, options) - .renderTemplate(template); + return translateTemplate(dependencyFactory, sdkVersion, "", template, options); } public static String translateTemplate(final TranslatorDependencyFactory dependencyFactory, final String sdkVersion, @@ -108,6 +126,13 @@ public static String translateTemplate(final TranslatorDependencyFactory depende return translateTemplate(dependencyFactory, sdkVersion, template, defaultOptions); } + public static String translateTemplate(final TranslatorDependencyFactory dependencyFactory, final String sdkVersion, + final String qualifier, final String template, TranslatorOptions options) + throws InstantiationException { + return EfxTranslatorFactory.getEfxTemplateTranslator(sdkVersion, qualifier, dependencyFactory, options) + .renderTemplate(template); + } + /** * Instantiates an EFX template translator and translates the EFX template contained in the given * InputStream. @@ -126,8 +151,7 @@ public static String translateTemplate(final TranslatorDependencyFactory depende public static String translateTemplate(final TranslatorDependencyFactory dependencyFactory, final String sdkVersion, final InputStream stream, TranslatorOptions options) throws IOException, InstantiationException { - return EfxTranslatorFactory.getEfxTemplateTranslator(sdkVersion, dependencyFactory, options) - .renderTemplate(stream); + return translateTemplate(dependencyFactory, sdkVersion, "", stream, options); } public static String translateTemplate(final TranslatorDependencyFactory dependencyFactory, final String sdkVersion, @@ -135,4 +159,13 @@ public static String translateTemplate(final TranslatorDependencyFactory depende throws IOException, InstantiationException { return translateTemplate(dependencyFactory, sdkVersion, stream, defaultOptions); } + + public static String translateTemplate(final TranslatorDependencyFactory dependencyFactory, final String sdkVersion, + final String qualifier, final InputStream stream, TranslatorOptions options) + throws IOException, InstantiationException { + return EfxTranslatorFactory.getEfxTemplateTranslator(sdkVersion, qualifier, dependencyFactory, options) + .renderTemplate(stream); + } + + //#endregion Translate EFX templates ---------------------------------------- } diff --git a/src/main/java/eu/europa/ted/efx/EfxTranslatorOptions.java b/src/main/java/eu/europa/ted/efx/EfxTranslatorOptions.java index 26e1f376..225eebaa 100644 --- a/src/main/java/eu/europa/ted/efx/EfxTranslatorOptions.java +++ b/src/main/java/eu/europa/ted/efx/EfxTranslatorOptions.java @@ -10,28 +10,50 @@ public class EfxTranslatorOptions implements TranslatorOptions { + /** + * This is the default namespace for user-defined functions (UDFs) in EFX. + * There is no need to change this but we made it customizable anyway. + * + * This setting is relevant mostly for the EFX to XSLT translation process but it can also + * be used for other target languages that need a namespace declaration for UDFs. + */ + public static final String DEFAULT_UDF_NAMESPACE = "efx-udf"; + // Change to EfxDecimalFormatSymbols.EFX_DEFAULT to use the decimal format // preferred by OP (space as thousands separator and comma as decimal separator). - public static final EfxTranslatorOptions DEFAULT = new EfxTranslatorOptions(DecimalFormat.XSL_DEFAULT, Locale.ENGLISH); + public static final EfxTranslatorOptions DEFAULT = new EfxTranslatorOptions(DEFAULT_UDF_NAMESPACE, DecimalFormat.XSL_DEFAULT, Locale.ENGLISH); private final DecimalFormat symbols; - private Locale primaryLocale; - private ArrayList otherLocales; + private final Locale primaryLocale; + private final ArrayList otherLocales; + private final String userDefinedFunctionNamespace; public EfxTranslatorOptions(DecimalFormat symbols) { - this(symbols, Locale.ENGLISH); + this(DEFAULT_UDF_NAMESPACE, symbols); + } + + public EfxTranslatorOptions(String udfNamespace, DecimalFormat symbols) { + this(udfNamespace, symbols, Locale.ENGLISH); } public EfxTranslatorOptions(DecimalFormat symbols, String primaryLanguage, String... otherLanguages) { this(symbols, Locale.forLanguageTag(primaryLanguage), Arrays.stream(otherLanguages).map(Locale::forLanguageTag).toArray(Locale[]::new)); } + public EfxTranslatorOptions(String udfNamespace, DecimalFormat symbols, String primaryLanguage, String... otherLanguages) { + this(udfNamespace, symbols, Locale.forLanguageTag(primaryLanguage), Arrays.stream(otherLanguages).map(Locale::forLanguageTag).toArray(Locale[]::new)); + } + public EfxTranslatorOptions(DecimalFormat symbols, Locale primaryLocale, Locale... otherLocales) { + this(DEFAULT_UDF_NAMESPACE, symbols, primaryLocale, otherLocales); + } + + public EfxTranslatorOptions(String udfNamespace, DecimalFormat symbols, Locale primaryLocale, Locale... otherLocales) { + this.userDefinedFunctionNamespace = udfNamespace; this.symbols = symbols; this.primaryLocale = primaryLocale; this.otherLocales = new ArrayList<>(Arrays.asList(otherLocales)); } - @Override public DecimalFormat getDecimalFormat() { @@ -68,8 +90,13 @@ public String[] getAllLanguage3LetterCodes() { return languages.toArray(new String[0]); } - public EfxTranslatorOptions withLanguage(String language) { - this.primaryLocale = Locale.forLanguageTag(language); - return this; + /** + * This will be used by the Script Generator when generating script to invoke a user-defined function. + * It will also be used by the Markup Generator to generate the namespace declaration as well as the + * user-defined function definitions. + */ + @Override + public String getUserDefinedFunctionNamespace() { + return this.userDefinedFunctionNamespace; } } \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/interfaces/MarkupGenerator.java b/src/main/java/eu/europa/ted/efx/interfaces/MarkupGenerator.java index ce6f8153..288ccd41 100644 --- a/src/main/java/eu/europa/ted/efx/interfaces/MarkupGenerator.java +++ b/src/main/java/eu/europa/ted/efx/interfaces/MarkupGenerator.java @@ -14,6 +14,7 @@ package eu.europa.ted.efx.interfaces; import java.util.List; +import java.util.Map; import java.util.Set; import org.apache.commons.lang3.tuple.Pair; @@ -23,6 +24,7 @@ import eu.europa.ted.efx.model.expressions.scalar.NumericExpression; import eu.europa.ted.efx.model.expressions.scalar.StringExpression; import eu.europa.ted.efx.model.templates.Markup; +import eu.europa.ted.efx.model.types.EfxDataType; /** * The role of this interface is to allow the reuse of the Sdk6EfxTemplateTranslator to generate @@ -45,6 +47,39 @@ public interface MarkupGenerator { */ Markup composeOutputFile(final List content, final List fragments); + /** + * Given a body (main content) and a set of fragments, this method returns the full content of the + * target template file. + * + * @param variables the variables to be included in the template file. + * @param functions the functions to be included in the template file. + * @param content the body (main content) of the template. + * @param fragments the fragments to be included in the template file. + * @return the full content of the target template file. + */ + Markup composeOutputFile(List globals, final List content, final List fragments); + + /** + * Renders the markup necessary to declare and initialize the variable making it available at runtime. + * + * @param type A subclass of {@link EfxDataType} specifying the type of the variable. + * @param name The name of the variable to be declared. + * @param initialiser The expression used to initialize the variable. + * @return A {@link Markup} object that contains the variable declaration. + */ + Markup renderVariableDeclaration(final Class type, final String name, final Expression initialiser); + + /** + * Renders the Markup necessary to make the function available at runtime. + * + * @param type A sub-class of EfxDataType that represents the return type of the function. + * @param name The name of the function. + * @param parameters The function parameters as a map of parameter names to their corresponding EfxDataType. + * @param expression The function body (the expression that must be evaluated when the function is invoked). + * @return A Markup object that declares the function. + */ + Markup renderFunctionDeclaration(final Class type, final String name, final Map> parameters, final Expression expression); + /** * Given an expression (which will eventually, at runtime, evaluate to the value of a field), this * method returns the template code that dereferences it (retrieves the value) in the target 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 bc7b1bc1..406284e0 100644 --- a/src/main/java/eu/europa/ted/efx/interfaces/ScriptGenerator.java +++ b/src/main/java/eu/europa/ted/efx/interfaces/ScriptGenerator.java @@ -110,6 +110,8 @@ public T composeFieldAttributeReference( */ public T composeVariableReference(String variableName, Class type); + public T composeParameterReference(String parameterName, Class type); + public T composeVariableDeclaration(String variableName, Class type); public T composeParameterDeclaration(String parameterName, Class type); @@ -302,9 +304,7 @@ public NumericExpression composeNumericOperation(NumericExpression leftOperand, public DurationExpression getDurationLiteralEquivalent(final String efxLiteral); - /* - * Numeric Functions - */ + // #region Numeric Functions ------------------------------------------------ public NumericExpression composeCountOperation(final SequenceExpression list); @@ -314,9 +314,9 @@ public NumericExpression composeNumericOperation(NumericExpression leftOperand, public NumericExpression composeStringLengthCalculation(StringExpression text); - /* - * String Functions - */ + // #endregion Numeric Functions ------------------------------------------- + + // #region String Functions ----------------------------------------------- public StringExpression composeStringConcatenation(List list); @@ -392,9 +392,9 @@ public StringExpression composeSubstringExtraction(StringExpression text, Numeri */ public StringExpression getTextInPreferredLanguage(final PathExpression fieldReference); - /* - * Boolean Functions - */ + // #endregion String Functions ---------------------------------------------- + + // #region Boolean Functions ------------------------------------------------ public BooleanExpression composeExistsCondition(PathExpression reference); @@ -404,9 +404,9 @@ public BooleanExpression composeUniqueValueCondition(PathExpression needle, public BooleanExpression composeSequenceEqualFunction(SequenceExpression one, SequenceExpression two); - /* - * Date Functions - */ + // #endregion Boolean Functions -------------------------------------------- + + // #region Date Functions --------------------------------------------------- public DateExpression composeToDateConversion(StringExpression pop); @@ -416,17 +416,15 @@ public DateExpression composeAddition(final DateExpression date, public DateExpression composeSubtraction(final DateExpression date, final DurationExpression duration); - /* - * Time Functions - */ + //#endregion Date Functions ------------------------------------------------- - public TimeExpression composeToTimeConversion(StringExpression pop); + // #region Time Functions --------------------------------------------------- + public TimeExpression composeToTimeConversion(StringExpression pop); + // #endregion Time Functions ------------------------------------------------ - /* - * Duration Functions - */ + // #region Duration Functions ----------------------------------------------- public DurationExpression composeToDayTimeDurationConversion(StringExpression text); @@ -446,9 +444,9 @@ public DurationExpression composeAddition(final DurationExpression left, public DurationExpression composeSubtraction(final DurationExpression left, final DurationExpression right); - /* - * Sequence Functions - */ + // #endregion Duration Functions -------------------------------------------- + + // #region Sequence Functions -------------------------------------------- public T composeDistinctValuesFunction( T list, Class listType); @@ -464,4 +462,24 @@ public T composeExceptFunction(T listOne, public T composeIndexer(SequenceExpression list, NumericExpression index, Class type); + + // #endregion Sequence Functions ----------------------------------------- + + // #region Function Invocation ------------------------------------------ + + /** + * Composes a function invocation expression with the specified function name, parameters, + * and return type. + * + * @param The type of the resulting expression, which must extend {@link TypedExpression}. + * @param functionName The name of the function to be invoked. + * @param parameters A list of parameters to be passed to the function, each of which must + * extend {@link TypedExpression}. + * @param type The class object representing the expected return type of the function invocation. + * @return An expression that will invoke the function at runtime. + */ + public T composeFunctionInvocation(String functionName, + List parameters, Class type); + + // #endregion Function Invocation ----------------------------------------- } diff --git a/src/main/java/eu/europa/ted/efx/interfaces/TranslatorOptions.java b/src/main/java/eu/europa/ted/efx/interfaces/TranslatorOptions.java index 76540b3c..1cf63d2c 100644 --- a/src/main/java/eu/europa/ted/efx/interfaces/TranslatorOptions.java +++ b/src/main/java/eu/europa/ted/efx/interfaces/TranslatorOptions.java @@ -12,4 +12,6 @@ public interface TranslatorOptions { public String[] getAllLanguage2LetterCodes(); public String[] getAllLanguage3LetterCodes(); + + public String getUserDefinedFunctionNamespace(); } diff --git a/src/main/java/eu/europa/ted/efx/model/CallStack.java b/src/main/java/eu/europa/ted/efx/model/CallStack.java index ccbe28bc..24216708 100644 --- a/src/main/java/eu/europa/ted/efx/model/CallStack.java +++ b/src/main/java/eu/europa/ted/efx/model/CallStack.java @@ -1,17 +1,22 @@ package eu.europa.ted.efx.model; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; import java.util.Stack; +import java.util.stream.Stream; import org.antlr.v4.runtime.misc.ParseCancellationException; -import eu.europa.ted.efx.model.expressions.Expression; import eu.europa.ted.efx.model.expressions.TypedExpression; import eu.europa.ted.efx.model.types.EfxDataType; -import eu.europa.ted.efx.model.variables.Identifier; import eu.europa.ted.efx.model.variables.Parameter; +import eu.europa.ted.efx.model.variables.Function; +import eu.europa.ted.efx.model.variables.Identifier; +import eu.europa.ted.efx.model.variables.IdentifierList; +import eu.europa.ted.efx.model.variables.ParameterList; +import eu.europa.ted.efx.model.variables.Variable; /** * The call stack is a stack of stack frames. Each stack frame represents a @@ -38,7 +43,7 @@ class StackFrame extends Stack { * Keeps a list of all identifiers declared in the current scope as well as * their type. */ - Map identifierRegistry = new HashMap(); + transient Map identifierRegistry = new HashMap<>(); /** * Registers an identifier in the current scope. This registration is later used @@ -46,7 +51,6 @@ class StackFrame extends Stack { * identifier is declared in the current scope. * * @param identifier The identifier to register. - * @param dataType The type of the identifier. */ void declareIdentifier(Identifier identifier) { this.identifierRegistry.put(identifier.name, identifier); @@ -93,6 +97,12 @@ public void clear() { */ Stack frames; + /** + * Keeps a list of all identifiers declared in the global scope as well as + * their type. + */ + Map globalIdentifierRegistry = new LinkedHashMap<>(); + /** * Default and only constructor. Adds a global scope to the stack. */ @@ -126,7 +136,7 @@ public void popStackFrame() { // If the dropped frame is not empty, then it contains return values that should // be passed to the next frame on the stack. - if (droppedFrame.size() > 0) { + if (!droppedFrame.isEmpty()) { if (this.frames.empty()) { throw new ParseCancellationException(STACK_UNDERFLOW); } @@ -146,6 +156,25 @@ public void declareIdentifier(Identifier identifier) { this.frames.peek().declareIdentifier(identifier); } + /** + * Declares a global identifier. + * + * @param identifier The identifier to declare. + */ + public void declareGlobalIdentifier(Identifier identifier) { + if (this.inScope(identifier.name)) { + throw new ParseCancellationException(IDENTIFIER_ALREADY_DECLARED + identifier.name); + } + this.globalIdentifierRegistry.put(identifier.name, identifier); + } + + public void declareFunction(Function function) { + if (this.inScope(function.name)) { + throw new ParseCancellationException(IDENTIFIER_ALREADY_DECLARED + function.name); + } + this.globalIdentifierRegistry.put(function.name, function); + } + /** * Checks if an identifier is declared in the current scope. * @@ -153,8 +182,8 @@ public void declareIdentifier(Identifier identifier) { * @return True if the identifier is declared in the current scope. */ boolean inScope(String identifier) { - return this.frames.stream().anyMatch( - f -> f.identifierRegistry.containsKey(identifier)); + return this.globalIdentifierRegistry.containsKey(identifier) + || this.frames.stream().anyMatch(f -> f.identifierRegistry.containsKey(identifier)); } /** @@ -171,39 +200,101 @@ StackFrame findFrameContaining(String identifier) { .findFirst().orElse(null); } + public IdentifierList getGlobals() { + IdentifierList globals = new IdentifierList(); + for (Identifier identifier : globalIdentifierRegistry.values()) { + globals.add(identifier); + } + return globals; + } + + /** * Gets the value of a parameter. * * @param parameterName The identifier of the parameter. * @return The value of the parameter. */ - Optional getParameter(String parameterName) { + Optional getParameter(String parameterName) { return this.frames.stream() .filter(f -> f.identifierRegistry.containsKey(parameterName) && Parameter.class.isAssignableFrom(f.identifierRegistry.get(parameterName).getClass())) .findFirst() - .map(x -> ((Parameter) x.identifierRegistry.get(parameterName)).parameterValue); + .map(x -> ((Parameter) x.identifierRegistry.get(parameterName)).referenceExpression); } /** - * Gets the type of a variable. - * - * @param identifier The identifier of the variable. - * @return The type of the variable. + * Retrieves an {@link Identifier} associated with the given identifier string. + * The method searches through the identifier registries of all frames and the global + * identifier registry. It returns the first matching {@link Identifier} found. + * + * @param identifier the string identifier to search for in the registries. + * @return an {@link Optional} containing the {@link Identifier} if found, or an empty {@link Optional} if not found. */ Optional getIdentifier(String identifier) { - return this.frames.stream().filter(f -> f.identifierRegistry.containsKey(identifier)).findFirst() - .map(x -> x.identifierRegistry.get(identifier)); + return Stream.concat( + this.frames.stream().map(f -> f.identifierRegistry), + Stream.of(this.globalIdentifierRegistry)) + .filter(registry -> registry.containsKey(identifier)) + .findFirst() + .map(registry -> registry.get(identifier)); + } + + Optional getVariable(String identifier) { + return Stream.concat( + this.frames.stream().map(f -> f.identifierRegistry), + Stream.of(this.globalIdentifierRegistry)) + .filter(registry -> registry.containsKey(identifier)) + .findFirst() + .map(registry -> registry.get(identifier)) + .filter(Variable.class::isInstance) + .map(Variable.class::cast); } /** - * Gets the type of a variable. - * - * @param identifierName The identifier of the variable. - * @return The type of the variable. + * Retrieves a function from the function registry by its name. + * + * @param functionName the name of the function to retrieve + * @return the {@link Function} associated with the given name + * @throws ParseCancellationException if the function name is not found in the registry, + * with a message indicating the undeclared identifier + */ + public Function getFunction(String functionName) { + return Optional.ofNullable(this.globalIdentifierRegistry.get(functionName)) + .filter(Function.class::isInstance) + .map(Function.class::cast) + .orElseThrow(() -> new ParseCancellationException(UNDECLARED_IDENTIFIER + functionName)); + } + + /** + * Retrieves the list of parameters for a specified function name. + * + * @param functionName the name of the function whose parameters are to be retrieved + * @return the list of parameters associated with the specified function + * @throws ParseCancellationException if the function name is not declared in the registry + */ + public ParameterList getFunctionParameters(String functionName) { + return Optional.ofNullable(this.globalIdentifierRegistry.get(functionName)) + .filter(Function.class::isInstance) + .map(identifier -> ((Function) identifier).parameters) + .orElseThrow(() -> new ParseCancellationException(UNDECLARED_IDENTIFIER + functionName)); + } + + /** + * Retrieves the data type of a given identifier by its name. + *

+ * This method first attempts to find the identifier using the {@code getIdentifier} method. + * If not found, it tries to resolve it as a function using the {@code getFunction} method. + * If the identifier is still not found, a {@link ParseCancellationException} is thrown. + *

+ * + * @param identifierName the name of the identifier to look up + * @return the class type of the identifier's data type + * @throws ParseCancellationException if the identifier is not declared */ public Class getTypeOfIdentifier(String identifierName) { - Optional identifier = this.getIdentifier(identifierName); + Optional identifier = this.getIdentifier(identifierName) + .or(() -> Optional.ofNullable((Identifier) this.getFunction(identifierName))); if (!identifier.isPresent()) { throw new ParseCancellationException(UNDECLARED_IDENTIFIER + identifierName); } @@ -220,8 +311,8 @@ public Class getTypeOfIdentifier(String identifierName) { * current scope. */ public void pushIdentifierReference(String identifierName) { - getParameter(identifierName).ifPresentOrElse(parameterValue -> this.push(parameterValue), - () -> getIdentifier(identifierName).ifPresentOrElse( + getParameter(identifierName).ifPresentOrElse(this::push, + () -> getVariable(identifierName).ifPresentOrElse( variable -> this.push(variable.referenceExpression), () -> { throw new ParseCancellationException(UNDECLARED_IDENTIFIER + identifierName); @@ -237,40 +328,54 @@ public void push(ParsedEntity item) { this.frames.peek().push(item); } + /** + * Removes and returns the top element of the call stack, ensuring it matches + * the expected type. + * This method is thread-safe. + * + * @param The type of the element expected to be returned, which + * must extend {@code ParsedEntity}. + * @param expectedType The {@code Class} object representing the expected type + * of the element. + * @return The top element of the call stack, cast to the specified type. + */ public synchronized T pop(Class expectedType) { return this.frames.peek().pop(expectedType); } /** - * Gets the object at the top of the current stack frame without removing it - * from the stack. - * - * @return The object at the top of the current stack frame. + * Retrieves, but does not remove, the top ParsedEntity from the call stack. + * This method is thread-safe as it is synchronized. + * + * @return the top ParsedEntity from the call stack, or {@code null} if the + * stack is empty. */ public synchronized ParsedEntity peek() { return this.frames.peek().peek(); } /** - * Gets the number of elements in the current stack frame. - * - * @return The number of elements in the current stack frame. + * Returns the size of the current call stack frame. + * + * @return the number of elements in the top frame of the call stack. */ public int size() { return this.frames.peek().size(); } /** - * Checks if the current stack frame is empty. - * - * @return True if the current stack frame is empty. + * Checks if the call stack is empty. + * + * @return {@code true} if the top frame of the call stack is empty, + * {@code false} otherwise. */ public boolean empty() { return this.frames.peek().empty(); } /** - * Clears the current stack frame. + * Clears the current call stack frame by removing all elements from it. + * This operation affects only the top frame of the stack. */ public void clear() { this.frames.peek().clear(); diff --git a/src/main/java/eu/europa/ted/efx/model/Context.java b/src/main/java/eu/europa/ted/efx/model/Context.java index e752d6fa..b24d017d 100644 --- a/src/main/java/eu/europa/ted/efx/model/Context.java +++ b/src/main/java/eu/europa/ted/efx/model/Context.java @@ -82,11 +82,11 @@ protected Context(final String symbol, final PathExpression absolutePath) { this(symbol, absolutePath, absolutePath); } - public Boolean isFieldContext() { + public boolean isFieldContext() { return this.getClass().equals(FieldContext.class); } - public Boolean isNodeContext() { + public boolean isNodeContext() { return this.getClass().equals(NodeContext.class); } diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/Expression.java b/src/main/java/eu/europa/ted/efx/model/expressions/Expression.java index c0404122..75d6dd54 100644 --- a/src/main/java/eu/europa/ted/efx/model/expressions/Expression.java +++ b/src/main/java/eu/europa/ted/efx/model/expressions/Expression.java @@ -1,6 +1,7 @@ package eu.europa.ted.efx.model.expressions; import java.lang.reflect.Constructor; +import java.util.Objects; import org.antlr.v4.runtime.misc.ParseCancellationException; @@ -10,7 +11,7 @@ public interface Expression extends ParsedEntity { public String getScript(); - public Boolean isLiteral(); + public boolean isLiteral(); static T instantiate(String script, Class type) { return Expression.instantiate(script, false, type); @@ -22,7 +23,7 @@ static T from(Expression source, Class returnType) { static T instantiate(String script, Boolean isLiteral, Class type) { try { - if (isLiteral) { + if (Boolean.TRUE.equals(isLiteral)) { Constructor constructor = type.getConstructor(String.class, Boolean.class); return constructor.newInstance(script, isLiteral); } else { @@ -44,7 +45,7 @@ static T empty(Class type) { public abstract class Impl implements Expression { private final String script; - private final Boolean isLiteral; + private final boolean isLiteral; @Override public String getScript() { @@ -52,7 +53,7 @@ public String getScript() { } @Override - public Boolean isLiteral() { + public boolean isLiteral() { return this.isLiteral; } @@ -71,15 +72,21 @@ public final Boolean isEmpty() { @Override public boolean equals(Object obj) { - if (obj == null) { - return false; + if (this == obj) { + return true; } - if (Expression.class.isAssignableFrom(obj.getClass())) { - return this.script.equals(((Expression) obj).getScript()); + if (!(obj instanceof Expression)) { + return false; } - return false; + Expression other = (Expression) obj; + return Objects.equals(script, other.getScript()) && isLiteral == other.isLiteral(); + } + + @Override + public int hashCode() { + return Objects.hash(script, isLiteral); } } } \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/TypedExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/TypedExpression.java index dbaf3fe7..1b21095b 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 @@ -14,16 +14,15 @@ public interface TypedExpression extends Expression { public Class getDataType(); - public Boolean is(Class dataType); + public boolean is(Class dataType); - public Boolean is(Class referenceType, Class dataType); + public boolean is(Class referenceType, Class dataType); static Class getEfxDataType( Class clazz, Class dataType1) { EfxDataTypeAssociation annotation = clazz.getAnnotation(EfxDataTypeAssociation.class); if (annotation == null) { - return EfxDataType.ANY.asSubclass(dataType1); // throw new IllegalArgumentException("Missing - // @EfxDataTypeAssociation annotation"); + return EfxDataType.ANY.asSubclass(dataType1); // throw new IllegalArgumentException("Missing @EfxDataTypeAssociation annotation"); } return annotation.dataType().asSubclass(dataType1); } @@ -61,7 +60,7 @@ public static TypedExpressi } } - public static Boolean canConvert(Class from, Class to) { + public static boolean canConvert(Class from, Class to) { var fromExpressionType = from.getAnnotation(EfxExpressionTypeAssociation.class).expressionType(); var fromDataType = from.getAnnotation(EfxDataTypeAssociation.class).dataType(); var toExpressionType = to.getAnnotation(EfxExpressionTypeAssociation.class).expressionType(); @@ -75,12 +74,12 @@ public abstract class Impl extends Expression.Impl implem private Class expressionType; private Class dataType; - public Impl(final String script, Class expressionType, + protected Impl(final String script, Class expressionType, Class dataType) { this(script, false, expressionType, dataType); } - public Impl(final String script, final Boolean isLiteral, + protected Impl(final String script, final Boolean isLiteral, Class expressionType, Class dataType) { super(script, isLiteral); this.expressionType = expressionType; @@ -98,12 +97,12 @@ public Class getDataType() { } @Override - public Boolean is(Class dataType) { + public boolean is(Class dataType) { return dataType.isAssignableFrom(this.dataType); } @Override - public Boolean is(Class referenceType, Class dataType) { + public boolean is(Class referenceType, Class dataType) { return referenceType.isAssignableFrom(this.expressionType) && dataType.isAssignableFrom(this.dataType); } } diff --git a/src/main/java/eu/europa/ted/efx/model/variables/Argument.java b/src/main/java/eu/europa/ted/efx/model/variables/Argument.java new file mode 100644 index 00000000..0873ed04 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/variables/Argument.java @@ -0,0 +1,37 @@ +package eu.europa.ted.efx.model.variables; + +import java.util.Objects; + +import eu.europa.ted.efx.model.expressions.TypedExpression; + +public class Argument extends Parameter { + + public final TypedExpression value; + + public Argument(Parameter parameter, TypedExpression value) { + super(parameter.name, parameter.referenceExpression); + this.value = value; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + Objects.hash(value); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + Argument other = (Argument) obj; + return Objects.equals(value, other.value); + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/variables/ArgumentList.java b/src/main/java/eu/europa/ted/efx/model/variables/ArgumentList.java new file mode 100644 index 00000000..e6a451d1 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/variables/ArgumentList.java @@ -0,0 +1,68 @@ +package eu.europa.ted.efx.model.variables; + +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.antlr.v4.runtime.misc.ParseCancellationException; + +import eu.europa.ted.efx.model.ParsedEntity; +import eu.europa.ted.efx.model.expressions.TypedExpression; + +public class ArgumentList extends LinkedList implements ParsedEntity { + + private static final String TYPE_MISMATCH = "Type mismatch. Expected %s instead of %s."; + + public final String identifier; + public final ParameterList parameters; + + public ArgumentList(Function function) { + this.identifier = function.name; + this.parameters = function.parameters; + } + + public boolean addArgument(TypedExpression argument) { + int position = this.size(); // Get the current position of the argument + if (position >= parameters.size()) { + throw new IllegalArgumentException("Too many arguments for the function."); + } + + var actualType = argument.getClass(); + var expectedType = parameters.get(position).getParameterType(); + if (!TypedExpression.canConvert(actualType, expectedType)) { + throw new ParseCancellationException( + String.format(TYPE_MISMATCH, TypedExpression.getEfxDataType(expectedType), + TypedExpression.getEfxDataType(actualType))); + } + return this.add(new Argument(parameters.get(position), argument)); + } + + public List getArguments() { + return this.stream().map(argument -> argument.value).collect(Collectors.toList()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (!(obj instanceof ArgumentList)) { + return false; + } + ArgumentList other = (ArgumentList) obj; + return Objects.equals(identifier, other.identifier) && + Objects.equals(parameters, other.parameters); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + Objects.hash(identifier, parameters); + return result; + } +} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/variables/Function.java b/src/main/java/eu/europa/ted/efx/model/variables/Function.java new file mode 100644 index 00000000..dc06e74e --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/variables/Function.java @@ -0,0 +1,17 @@ +package eu.europa.ted.efx.model.variables; + +import eu.europa.ted.efx.model.expressions.TypedExpression; +import eu.europa.ted.efx.model.types.EfxDataType; + +public class Function extends Identifier { + + public ParameterList parameters; + public TypedExpression expression; + + public Function(String name, Class returnType, ParameterList parameters, + TypedExpression expression) { + super(name, returnType); + this.parameters = parameters; + this.expression = expression; + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/variables/FunctionList.java b/src/main/java/eu/europa/ted/efx/model/variables/FunctionList.java new file mode 100644 index 00000000..7323022e --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/variables/FunctionList.java @@ -0,0 +1,11 @@ +package eu.europa.ted.efx.model.variables; + +import java.util.LinkedList; + +import eu.europa.ted.efx.model.ParsedEntity; + +public class FunctionList extends LinkedList implements ParsedEntity { + + public FunctionList() { + } +} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/variables/Identifier.java b/src/main/java/eu/europa/ted/efx/model/variables/Identifier.java index 22acf0ad..db473e7d 100644 --- a/src/main/java/eu/europa/ted/efx/model/variables/Identifier.java +++ b/src/main/java/eu/europa/ted/efx/model/variables/Identifier.java @@ -1,20 +1,55 @@ package eu.europa.ted.efx.model.variables; +import java.util.Objects; + import eu.europa.ted.efx.model.ParsedEntity; import eu.europa.ted.efx.model.expressions.Expression; import eu.europa.ted.efx.model.expressions.TypedExpression; import eu.europa.ted.efx.model.types.EfxDataType; -public class Identifier implements ParsedEntity { - final public String name; - final public Class dataType; - final public Expression declarationExpression; - final public TypedExpression referenceExpression; +/** + * Represents an identifier that is declared and used in an EFX expression. + * Identifiers typically point to functions, variables and parameters. + * + * The Identifier class keeps track not only of the name and data type of the identifier, + * but also of the expressions used to declare and reference it. + * + * @see Expression + * @see TypedExpression + * @see ParsedEntity + * @see EfxDataType + */ +public abstract class Identifier implements ParsedEntity { + public final String name; + public final Class dataType; - public Identifier(String name, Expression declarationExpression, TypedExpression referenceExpression) { + /** + * Creates an Identifier with the given name and expressions. + * The Identifier's data type is inferred from the reference expression. + * + * @param name The name of the identifier. + * @param declarationExpression The expression that should be used to declare the Identifier at runtime. + * @param referenceExpression The expression that should be used to reference the identifier. + */ + protected Identifier(String name, Class dataType) { this.name = name; - this.dataType = referenceExpression.getDataType(); - this.declarationExpression = declarationExpression; - this.referenceExpression = referenceExpression; + this.dataType = dataType; + } + + @Override + public int hashCode() { + return Objects.hash(name, dataType); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Identifier other = (Identifier) obj; + return Objects.equals(name, other.name) && Objects.equals(dataType, other.dataType); } } \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/variables/IdentifierList.java b/src/main/java/eu/europa/ted/efx/model/variables/IdentifierList.java new file mode 100644 index 00000000..87587ff0 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/variables/IdentifierList.java @@ -0,0 +1,11 @@ +package eu.europa.ted.efx.model.variables; + +import java.util.LinkedList; + +import eu.europa.ted.efx.model.ParsedEntity; + +public class IdentifierList extends LinkedList implements ParsedEntity { + + public IdentifierList() { + } +} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/variables/Parameter.java b/src/main/java/eu/europa/ted/efx/model/variables/Parameter.java index 299d480e..67924fe0 100644 --- a/src/main/java/eu/europa/ted/efx/model/variables/Parameter.java +++ b/src/main/java/eu/europa/ted/efx/model/variables/Parameter.java @@ -1,14 +1,41 @@ package eu.europa.ted.efx.model.variables; -import eu.europa.ted.efx.model.expressions.Expression; +import java.util.Objects; + import eu.europa.ted.efx.model.expressions.TypedExpression; public class Parameter extends Identifier { - public final TypedExpression parameterValue; + public final TypedExpression referenceExpression; + + public Parameter(String parameterName, TypedExpression referenceExpression) { + super(parameterName, referenceExpression.getDataType()); + this.referenceExpression = referenceExpression; + } + + public Class getParameterType() { + return referenceExpression.getClass(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + Objects.hash(referenceExpression); + return result; + } - public Parameter(String parameterName, Expression declarationExpression, TypedExpression referenceExpression, TypedExpression parameterValue) { - super(parameterName, declarationExpression, referenceExpression); - this.parameterValue = parameterValue; + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + Parameter other = (Parameter) obj; + return Objects.equals(referenceExpression, other.referenceExpression); } -} \ No newline at end of file +} diff --git a/src/main/java/eu/europa/ted/efx/model/variables/ParameterList.java b/src/main/java/eu/europa/ted/efx/model/variables/ParameterList.java new file mode 100644 index 00000000..c7c56897 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/variables/ParameterList.java @@ -0,0 +1,20 @@ +package eu.europa.ted.efx.model.variables; + +import java.util.LinkedList; + +import eu.europa.ted.efx.model.ParsedEntity; +import eu.europa.ted.efx.model.types.EfxDataType; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class ParameterList extends LinkedList implements ParsedEntity { + + public Map> toMap() { + var map = new LinkedHashMap>(); + for (Parameter parameter : this) { + map.put(parameter.name, parameter.dataType); + } + return map; + } +} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/variables/Variable.java b/src/main/java/eu/europa/ted/efx/model/variables/Variable.java index 746d18f6..30a2ceaf 100644 --- a/src/main/java/eu/europa/ted/efx/model/variables/Variable.java +++ b/src/main/java/eu/europa/ted/efx/model/variables/Variable.java @@ -4,17 +4,15 @@ import eu.europa.ted.efx.model.expressions.TypedExpression; public class Variable extends Identifier { - final public TypedExpression initializationExpression; + public final Expression declarationExpression; + public final TypedExpression initializationExpression; + public final TypedExpression referenceExpression; public Variable(String variableName, Expression declarationExpression, TypedExpression initializationExpression, TypedExpression referenceExpression) { - super(variableName, declarationExpression, referenceExpression); + super(variableName, initializationExpression.getDataType()); + this.declarationExpression = declarationExpression; this.initializationExpression = initializationExpression; + this.referenceExpression = referenceExpression; assert referenceExpression.getDataType() == initializationExpression.getDataType(); } - - public Variable(Identifier identifier, TypedExpression initializationExpression) { - super(identifier.name, identifier.declarationExpression, identifier.referenceExpression); - this.initializationExpression = initializationExpression; - assert identifier.dataType == initializationExpression.getDataType(); - } } \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1.java b/src/main/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1.java index 21e5e147..6b1e45b3 100644 --- a/src/main/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1.java +++ b/src/main/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1.java @@ -1209,8 +1209,6 @@ private void exitParameterDeclaration(String parameterName, Class void exitSequenceAtIndex( // #endregion New in EFX-2: Indexers --------------------------------------- + // //#region New in EFX-2: Function invocation ------------------------------ + + + @Override + public void enterFunctionInvocation(FunctionInvocationContext ctx) { + this.stack.push(this.stack.getFunction(getFunctionName(ctx))); + } + + @Override + public void enterArgumentList(ArgumentListContext ctx) { + this.stack.push(new ArgumentList(this.stack.pop(Function.class))); + } + + @Override + public void exitArgument(ArgumentContext ctx) { + var argument = this.stack.pop(TypedExpression.class); // pop the argument + var functionArguments = this.stack.pop(ArgumentList.class); // pop the function arguments + functionArguments.addArgument(argument); // add the argument to the list of arguments + this.stack.push(functionArguments); // push the updated list of arguments + } + + @Override + public void exitArgumentList(ArgumentListContext ctx) { + var arguments = this.stack.pop(ArgumentList.class); // pop the function arguments + var countExpectedArguments = arguments.parameters.size(); + var countPassedArguments = arguments.size(); + if (countExpectedArguments != countPassedArguments) { // check if the number of passed arguments is correct + throw new ParseCancellationException("The function " + arguments.identifier + " expects " + + countExpectedArguments + " arguments, but " + countPassedArguments + " were passed."); + } + this.stack.push(arguments); // push the list of arguments back to the stack + } + + @Override + public void exitStringFunctionInvocation(StringFunctionInvocationContext ctx) { + var parameters = this.stack.pop(ArgumentList.class).getArguments(); + this.stack.push(this.script.composeFunctionInvocation(getFunctionName(ctx), parameters, StringExpression.class)); + } + + @Override + public void exitNumericFunctionInvocation(NumericFunctionInvocationContext ctx) { + var parameters = this.stack.pop(ArgumentList.class).getArguments(); + this.stack.push(this.script.composeFunctionInvocation(getFunctionName(ctx), parameters, NumericExpression.class)); + } + + @Override + public void exitBooleanFunctionInvocation(BooleanFunctionInvocationContext ctx) { + var parameters = this.stack.pop(ArgumentList.class).getArguments(); + this.stack.push(this.script.composeFunctionInvocation(getFunctionName(ctx), parameters, BooleanExpression.class)); + } + + @Override + public void exitDateFunctionInvocation(DateFunctionInvocationContext ctx) { + var parameters = this.stack.pop(ArgumentList.class).getArguments(); + this.stack.push(this.script.composeFunctionInvocation(getFunctionName(ctx), parameters, DateExpression.class)); + } + + @Override + public void exitTimeFunctionInvocation(TimeFunctionInvocationContext ctx) { + var parameters = this.stack.pop(ArgumentList.class).getArguments(); + this.stack.push(this.script.composeFunctionInvocation(getFunctionName(ctx), parameters, TimeExpression.class)); + } + + @Override + public void exitDurationFunctionInvocation(DurationFunctionInvocationContext ctx) { + var parameters = this.stack.pop(ArgumentList.class).getArguments(); + this.stack.push(this.script.composeFunctionInvocation(getFunctionName(ctx), parameters, DurationExpression.class)); + } + + // #endregion New in EFX-2: Function invocation ----------------------------- + // #region Parameter Declarations ------------------------------------------- @@ -1273,8 +1350,6 @@ private void exitParameterDeclaration(String parameterName, Class returnType) { + var expression = this.stack.pop(TypedExpression.class); + var parameters = this.stack.pop(ParameterList.class); + + this.stack.declareFunction(new Function(functionName, returnType, parameters, expression)); + } + + // #endregion Global declarationExpressions ------------------------------------ + // #region Template File ---------------------------------------------------- @Override @@ -228,13 +321,24 @@ public void enterTemplateFile(TemplateFileContext ctx) { public void exitTemplateFile(TemplateFileContext ctx) { this.blockStack.pop(); + List globals = new ArrayList<>(); + for (Identifier identifier : this.stack.getGlobals()) { + if (identifier instanceof Variable) { + Variable variable = (Variable) identifier; + globals.add(this.markup.renderVariableDeclaration(variable.dataType, variable.name, variable.initializationExpression)); + } else if (identifier instanceof Function) { + Function function = (Function) identifier; + globals.add(this.markup.renderFunctionDeclaration(function.dataType, function.name, function.parameters.toMap(), function.expression)); + } + } + List templateCalls = new ArrayList<>(); List templates = new ArrayList<>(); - for (ContentBlock rootBlock : this.rootBlock.getChildren()) { - templateCalls.add(rootBlock.renderCallTemplate(markup)); - rootBlock.renderTemplate(markup, templates); + for (ContentBlock block : this.rootBlock.getChildren()) { + templateCalls.add(block.renderCallTemplate(markup)); + block.renderTemplate(markup, templates); } - Markup file = this.markup.composeOutputFile(templateCalls, templates); + Markup file = this.markup.composeOutputFile(globals, templateCalls, templates); this.stack.push(file); } @@ -734,20 +838,26 @@ public void exitDurationVariableInitializer(DurationVariableInitializerContext c private void exitVariableInitializer( String variableName, Class expressionType) { var expression = this.stack.pop(expressionType); - VariableList variables = this.stack.pop(VariableList.class); try { var variable = new Variable(variableName, this.script.composeVariableDeclaration(variableName, expression.getClass()), expression, this.script.composeVariableReference(variableName, expression.getClass())); - variables.add(variable); - this.stack.push(variables); - this.stack.declareIdentifier(variable); + this.stack.push(variable); } catch (Exception e) { throw new ParseCancellationException(e); } } + @Override + public void exitTemplateVariableDeclaration(TemplateVariableDeclarationContext arg0) { + var variable = this.stack.pop(Variable.class); + this.stack.declareIdentifier(variable); + VariableList variables = this.stack.pop(VariableList.class); + variables.add(variable); + this.stack.push(variables); + } + // #endregion Variable Initializers ----------------------------------------- // #endregion New in EFX-2 -------------------------------------------------- @@ -762,6 +872,49 @@ public void enterContextDeclarationBlock(ContextDeclarationBlockContext arg0) { // #endregion Context Declaration Blocks {...} ------------------------------ + // #region Parameter Declarations ------------------------------------------- + + @Override + public void exitStringParameterDeclaration(StringParameterDeclarationContext ctx) { + this.exitParameterDeclaration(getVariableName(ctx), StringExpression.class); + } + + @Override + public void exitNumericParameterDeclaration(NumericParameterDeclarationContext ctx) { + this.exitParameterDeclaration(getVariableName(ctx), NumericExpression.class); + } + + @Override + public void exitBooleanParameterDeclaration(BooleanParameterDeclarationContext ctx) { + this.exitParameterDeclaration(getVariableName(ctx), BooleanExpression.class); + } + + @Override + public void exitDateParameterDeclaration(DateParameterDeclarationContext ctx) { + this.exitParameterDeclaration(getVariableName(ctx), DateExpression.class); + } + + @Override + public void exitTimeParameterDeclaration(TimeParameterDeclarationContext ctx) { + this.exitParameterDeclaration(getVariableName(ctx), TimeExpression.class); + } + + @Override + public void exitDurationParameterDeclaration(DurationParameterDeclarationContext ctx) { + this.exitParameterDeclaration(getVariableName(ctx), DurationExpression.class); + } + + private void exitParameterDeclaration(String parameterName, Class parameterType) { + Parameter parameter = new Parameter(parameterName, + this.script.composeParameterReference(parameterName, parameterType)); + this.stack.declareIdentifier(parameter); + var parameterList = this.stack.pop(ParameterList.class); + parameterList.add(parameter); + this.stack.push(parameterList); + } + + // #endregion Parameter Declarations ---------------------------------------- + // #region Template lines -------------------------------------------------- @Override @@ -878,34 +1031,66 @@ private int getIndentLevel(TemplateLineContext ctx) { return 0; } - static private String getVariableName(StringVariableInitializerContext ctx) { + // #region Variable Names -------------------------------------------------- + + private static String getVariableName(StringVariableInitializerContext ctx) { return getVariableName(ctx.Variable().getText()); } - static private String getVariableName(NumericVariableInitializerContext ctx) { + private static String getVariableName(NumericVariableInitializerContext ctx) { return getVariableName(ctx.Variable().getText()); } - static private String getVariableName(BooleanVariableInitializerContext ctx) { + private static String getVariableName(BooleanVariableInitializerContext ctx) { return getVariableName(ctx.Variable().getText()); } - static private String getVariableName(DateVariableInitializerContext ctx) { + private static String getVariableName(DateVariableInitializerContext ctx) { return getVariableName(ctx.Variable().getText()); } - static private String getVariableName(TimeVariableInitializerContext ctx) { + private static String getVariableName(TimeVariableInitializerContext ctx) { return getVariableName(ctx.Variable().getText()); } - static private String getVariableName(DurationVariableInitializerContext ctx) { + private static String getVariableName(DurationVariableInitializerContext ctx) { return getVariableName(ctx.Variable().getText()); } - static private String getVariableName(ContextVariableInitializerContext ctx) { + private static String getVariableName(ContextVariableInitializerContext ctx) { return getVariableName(ctx.Variable().getText()); } + // #endregion Variable Names ----------------------------------------------- + + // #region Function Names -------------------------------------------------- + + private static String getFunctionName(StringFunctionDeclarationContext ctx) { + return getFunctionName(ctx.Function().getText()); + } + + private static String getFunctionName(BooleanFunctionDeclarationContext ctx) { + return getFunctionName(ctx.Function().getText()); + } + + private static String getFunctionName(NumericFunctionDeclarationContext ctx) { + return getFunctionName(ctx.Function().getText()); + } + + private static String getFunctionName(DateFunctionDeclarationContext ctx) { + return getFunctionName(ctx.Function().getText()); + } + + private static String getFunctionName(TimeFunctionDeclarationContext ctx) { + return getFunctionName(ctx.Function().getText()); + } + + private static String getFunctionName(DurationFunctionDeclarationContext ctx) { + return getFunctionName(ctx.Function().getText()); + } + + // #endregion Function Names ----------------------------------------------- + // #endregion Helpers ------------------------------------------------------- // #region Pre-processing ------------------------------------------------- @@ -929,13 +1114,25 @@ String processTemplate() { // #region Template Variables --------------------------------------------- + @Override + public void exitGlobalVariableDeclaration(GlobalVariableDeclarationContext arg0) { + var variable = this.stack.pop(Variable.class); + this.stack.declareGlobalIdentifier(variable); + } + + @Override + public void exitTemplateVariableDeclaration(TemplateVariableDeclarationContext arg0) { + var variable = this.stack.pop(Variable.class); + this.stack.declareIdentifier(variable); + } + @Override public void exitContextDeclaration(ContextDeclarationContext ctx) { - final String filedId = getFieldIdFromChildSimpleFieldReferenceContext(ctx); - if (filedId != null) { + final String fieldId = getFieldIdFromChildSimpleFieldReferenceContext(ctx); + if (fieldId != null) { final ContextVariableInitializerContext initializer = ctx.contextVariableInitializer(); if (initializer != null) { - var t = FieldTypes.fromString(this.symbols.getTypeOfField(filedId)); + var t = FieldTypes.fromString(this.symbols.getTypeOfField(fieldId)); this.stack.declareIdentifier(new Variable(getVariableName(initializer), PathExpression.instantiate("", t), PathExpression.instantiate("", t), PathExpression.instantiate("", t))); } } @@ -943,39 +1140,39 @@ public void exitContextDeclaration(ContextDeclarationContext ctx) { @Override public void exitStringVariableInitializer(StringVariableInitializerContext ctx) { - this.stack.declareIdentifier(new Variable(getVariableName(ctx), StringExpression.empty(), StringExpression.empty(), StringExpression.empty())); + this.stack.push(new Variable(getVariableName(ctx), StringExpression.empty(), StringExpression.empty(), StringExpression.empty())); } @Override public void exitBooleanVariableInitializer(BooleanVariableInitializerContext ctx) { - this.stack.declareIdentifier(new Variable(getVariableName(ctx), BooleanExpression.empty(), BooleanExpression.empty(), BooleanExpression.empty())); + this.stack.push(new Variable(getVariableName(ctx), BooleanExpression.empty(), BooleanExpression.empty(), BooleanExpression.empty())); } @Override public void exitNumericVariableInitializer(NumericVariableInitializerContext ctx) { - this.stack.declareIdentifier(new Variable(getVariableName(ctx), NumericExpression.empty(), NumericExpression.empty(), NumericExpression.empty())); + this.stack.push(new Variable(getVariableName(ctx), NumericExpression.empty(), NumericExpression.empty(), NumericExpression.empty())); } @Override public void exitDateVariableInitializer(DateVariableInitializerContext ctx) { - this.stack.declareIdentifier(new Variable(getVariableName(ctx), DateExpression.empty(), DateExpression.empty(), DateExpression.empty())); + this.stack.push(new Variable(getVariableName(ctx), DateExpression.empty(), DateExpression.empty(), DateExpression.empty())); } @Override public void exitTimeVariableInitializer(TimeVariableInitializerContext ctx) { - this.stack.declareIdentifier(new Variable(getVariableName(ctx), TimeExpression.empty(), TimeExpression.empty(), TimeExpression.empty())); + this.stack.push(new Variable(getVariableName(ctx), TimeExpression.empty(), TimeExpression.empty(), TimeExpression.empty())); } @Override public void exitDurationVariableInitializer(DurationVariableInitializerContext ctx) { - this.stack.declareIdentifier(new Variable(getVariableName(ctx), DurationExpression.empty(), DurationExpression.empty(), DurationExpression.empty())); + this.stack.push(new Variable(getVariableName(ctx), DurationExpression.empty(), DurationExpression.empty(), DurationExpression.empty())); } // #endregion Template Variables ------------------------------------------ // #region Scope management -------------------------------------------- - Stack levels = new Stack(); + Stack levels = new Stack<>(); @Override public void enterTemplateLine(TemplateLineContext ctx) { @@ -1017,13 +1214,10 @@ public void exitTemplateLine(TemplateLineContext ctx) { throw new ParseCancellationException(START_INDENT_AT_ZERO); } this.levels.push(this.levels.peek() + 1); - } else if (indentChange == 0) { - - if (this.levels.isEmpty()) { + } else if (indentChange == 0 && this.levels.isEmpty()) { assert indentLevel == 0 : UNEXPECTED_INDENTATION; this.levels.push(0); } - } } // #endregion Scope management -------------------------------------------- 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 3c33cf26..1fb6248b 100644 --- a/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java +++ b/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java @@ -123,6 +123,11 @@ public T composeVariableReference(String variableNam return Expression.instantiate("$" + variableName, type); } + @Override + public T composeParameterReference(String parameterName, Class type) { + return Expression.instantiate("$" + parameterName, type); + } + @Override public T composeVariableDeclaration(String variableName, Class type) { return Expression.instantiate("$" + variableName, type); @@ -548,6 +553,16 @@ public T composeExceptFunction(T listOne, T listT //#endregion Duration functions --------------------------------------------- + @Override + public T composeFunctionInvocation(String functionName, List parameters, + Class type) { + String namespace = translatorOptions.getUserDefinedFunctionNamespace(); + String qualifiedFunctionName = (namespace != null && !namespace.isEmpty()) + ? namespace + ":" + functionName + : functionName; + return Expression.instantiate(qualifiedFunctionName + "(" + parameters.stream().map(p -> p.getScript()).collect(Collectors.joining(", ")) + ")", type); + } + //#region Helpers ----------------------------------------------------------- @@ -556,7 +571,7 @@ private String quoted(final String text) { } private int getWeeksFromDurationLiteral(final String literal) { - Matcher weeksMatcher = Pattern.compile("(?<=[^0-9])[0-9]+(?=W)").matcher(literal); + Matcher weeksMatcher = Pattern.compile("(?<=\\D)\\d+(?=W)").matcher(literal); return weeksMatcher.find() ? Integer.parseInt(weeksMatcher.group()) : 0; } diff --git a/src/test/java/eu/europa/ted/efx/EfxTestsBase.java b/src/test/java/eu/europa/ted/efx/EfxTestsBase.java index f5f10827..f8bf9af4 100644 --- a/src/test/java/eu/europa/ted/efx/EfxTestsBase.java +++ b/src/test/java/eu/europa/ted/efx/EfxTestsBase.java @@ -1,10 +1,16 @@ package eu.europa.ted.efx; import static org.junit.jupiter.api.Assertions.assertEquals; + +import eu.europa.ted.efx.interfaces.TranslatorOptions; import eu.europa.ted.efx.mock.DependencyFactoryMock; import eu.europa.ted.efx.model.DecimalFormat; public abstract class EfxTestsBase { + + protected static final TranslatorOptions DEFAULT_OPTIONS = new EfxTranslatorOptions("udf", + DecimalFormat.EFX_DEFAULT); + protected abstract String getSdkVersion(); protected void testExpressionTranslationWithContext(final String expectedTranslation, @@ -24,7 +30,7 @@ protected String translateExpressionWithContext(final String context, final Stri protected String translateExpression(final String expression, final String... params) { try { return EfxTranslator.translateExpression(DependencyFactoryMock.INSTANCE, getSdkVersion(), - expression, new EfxTranslatorOptions(DecimalFormat.EFX_DEFAULT), params); + expression, DEFAULT_OPTIONS, params); } catch (InstantiationException e) { throw new RuntimeException(e); } @@ -33,7 +39,7 @@ protected String translateExpression(final String expression, final String... pa protected String translateTemplate(final String template) { try { return EfxTranslator.translateTemplate(DependencyFactoryMock.INSTANCE, getSdkVersion(), - template + "\n"); + template + "\n", DEFAULT_OPTIONS); } catch (InstantiationException e) { throw new RuntimeException(e); } diff --git a/src/test/java/eu/europa/ted/efx/mock/MarkupGeneratorMock.java b/src/test/java/eu/europa/ted/efx/mock/MarkupGeneratorMock.java index 826d6bf3..b6251e8d 100644 --- a/src/test/java/eu/europa/ted/efx/mock/MarkupGeneratorMock.java +++ b/src/test/java/eu/europa/ted/efx/mock/MarkupGeneratorMock.java @@ -1,6 +1,8 @@ package eu.europa.ted.efx.mock; +import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -13,9 +15,27 @@ import eu.europa.ted.efx.model.expressions.scalar.NumericExpression; import eu.europa.ted.efx.model.expressions.scalar.StringExpression; import eu.europa.ted.efx.model.templates.Markup; +import eu.europa.ted.efx.model.types.EfxDataType; public class MarkupGeneratorMock implements MarkupGenerator { + @Override + public Markup renderVariableDeclaration(Class dataType, String variableName, + Expression initialiser) { + return new Markup(String.format("%s:$%s=%s", dataType.getSimpleName(), variableName, initialiser.getScript())); + } + + @Override + public Markup renderFunctionDeclaration(Class type, String name, Map> parameters, + Expression expression) { + return new Markup( + String.format("%s:%s(%s) -> { %s }", type.getSimpleName(), name, + parameters.entrySet().stream() + .map(entry -> entry.getValue().getSimpleName() + ":$" + entry.getKey()) + .collect(Collectors.joining(", ")), + expression.getScript())); + } + @Override public Markup renderVariableExpression(Expression valueReference) { return new Markup(String.format("eval(%s)", valueReference.getScript())); @@ -73,14 +93,20 @@ public Markup renderFragmentInvocation(String name, PathExpression context, Set> variables) { return new Markup(String.format("for-each(%s).call(%s(%s))", context.getScript(), name, variables.stream() - .map(v -> String.format("%s:%s", v.getLeft(), v.getRight())) + .map(v -> String.format("%s:=%s", v.getLeft(), v.getRight())) .collect(Collectors.joining(", ")))); } @Override public Markup composeOutputFile(List body, List templates) { - return new Markup(String.format("%s\n%s", + return this.composeOutputFile(new ArrayList(), body, templates); + } + + @Override + public Markup composeOutputFile(List globals, List body, List templates) { + return new Markup(String.format("%s\n%s\n%s", + globals.stream().map(t -> t.script).collect(Collectors.joining("\n")), templates.stream().map(t -> t.script).collect(Collectors.joining("\n")), - body.stream().map(t -> t.script).collect(Collectors.joining("\n")))); + body.stream().map(t -> t.script).collect(Collectors.joining("\n"))).trim()); } } 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 7ed9036b..5f0a806a 100644 --- a/src/test/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2Test.java +++ b/src/test/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2Test.java @@ -12,7 +12,34 @@ protected String getSdkVersion() { return "eforms-sdk-2.0"; } - /*** Template line ***/ + // #region Globals ---------------------------------------------------------- + + @Test + void testGlobals_VariableDeclaration() { + assertEquals( + lines( + "String:$t3='a'", + "Number:$n1=12", + "String:$t1=$t3", + "String:test(String:$p1, Number:$p2) -> { concat($p1, $t1) }", + "Number:$n2=$n1 + 1", + "String:$t4=udf:test($t3, 22)", + "let block01(t2) -> { eval(PathNode/TextField/normalize-space(text())) }", + "for-each(/*).call(block01(t2:=udf:test($t3, 99)))"), // + translateTemplate(lines( + "// comment", // + "{text:$t3='a'}// comment", + "{number:$n1=12}", + "{text:$t1=$t3}", + "{text:?test(text:$p1, number:$p2) = concat($p1, $t1)}", + "{number:$n2 = $n1 + 1}", + "{text:$t4= ?test($t3, 22)}", + "{ND-Root, text:$t2=?test($t3, 99)} ${BT-00-Text}"))); + } + + // #endregion Globals ------------------------------------------------------- + + // #region Template line ---------------------------------------------------- @Test void testTemplateLineNoIdent() { @@ -176,18 +203,18 @@ void testTemplateLine_VariableScope() { @Test void testTemplateLine_ContextVariable() { assertEquals( - lines("let block01(ctx, t) -> { #1: eval(for $x in ./normalize-space(text()) return concat($x, $t))", // - "for-each(.).call(block0101(ctx:$ctx, t:$t, t2:'test'))", // - "for-each(.).call(block0102(ctx:$ctx, t:$t, t2:'test3')) }", // - "let block0101(ctx, t, t2) -> { #1.1: eval(for $y in ./normalize-space(text()) return concat($y, $t, $t2))", // - "for-each(.).call(block010101(ctx:$ctx, t:$t, t2:$t2))", // - "for-each(.).call(block010102(ctx:$ctx, t:$t, t2:$t2)) }", // - "let block010101(ctx, t, t2) -> { eval(for $z in ./normalize-space(text()) return concat($z, $t, $ctx)) }", // - "let block010102(ctx, t, t2) -> { eval(for $z in ./normalize-space(text()) return concat($z, $t, $ctx)) }", // - "let block0102(ctx, t, t2) -> { eval(for $z in ./normalize-space(text()) return concat($z, $t2, $ctx)) }", // - "for-each(/*/PathNode/TextField).call(block01(ctx:., t:./normalize-space(text())))"), // + lines("let block01(xyz, ctx, t) -> { #1: eval(for $x in ./normalize-space(text()) return concat($x, $t))", // + "for-each(.).call(block0101(xyz:=$xyz, ctx:=$ctx, t:=$t, t2:='test'))", // + "for-each(.).call(block0102(xyz:=$xyz, ctx:=$ctx, t:=$t, t2:='test3')) }", // + "let block0101(xyz, ctx, t, t2) -> { #1.1: eval(for $y in ./normalize-space(text()) return concat($y, $t, $t2))", // + "for-each(.).call(block010101(xyz:=$xyz, ctx:=$ctx, t:=$t, t2:=$t2))", // + "for-each(.).call(block010102(xyz:=$xyz, ctx:=$ctx, t:=$t, t2:=$t2)) }", // + "let block010101(xyz, ctx, t, t2) -> { eval(for $z in ./normalize-space(text()) return concat($z, $t, $ctx)) }", // + "let block010102(xyz, ctx, t, t2) -> { eval(for $z in ./normalize-space(text()) return concat($z, $t, $ctx)) }", // + "let block0102(xyz, ctx, t, t2) -> { eval(for $z in ./normalize-space(text()) return concat($z, $t2, $ctx)) }", // + "for-each(/*/PathNode/TextField).call(block01(xyz:='a', ctx:=., t:=./normalize-space(text())))"), // translateTemplate(lines( - "{context:$ctx = BT-00-Text, text:$t = BT-00-Text} ${for text:$x in BT-00-Text return concat($x, $t)}", + "{text:$xyz='a', context:$ctx = BT-00-Text, text:$t = BT-00-Text} ${for text:$x in BT-00-Text return concat($x, $t)}", " {BT-00-Text, text:$t2 = 'test'} ${for text:$y in BT-00-Text return concat($y, $t, $t2)}", " {BT-00-Text} ${for text:$z in BT-00-Text return concat($z, $t, $ctx)}", " {BT-00-Text} ${for text:$z in BT-00-Text return concat($z, $t, $ctx)}", @@ -238,8 +265,9 @@ void testTemplateLine_WithLineBreak() { translateTemplate("{BT-00-Text} #{field|name|BT-00-Text} \\n")); } + // #endregion Template line ------------------------------------------------- - /*** Labels ***/ + // #region Labels ----------------------------------------------------------- @Test void testStandardLabelReference() { @@ -395,8 +423,9 @@ void testShorthandIndirectLabelReferenceFromContextField_WithNodeContext() { assertThrows(ParseCancellationException.class, () -> translateTemplate("{ND-Root} #value")); } + // #endregion Labels -------------------------------------------------------- - /*** Expression block ***/ + // #region Expression block ------------------------------------------------- @Test void testShorthandFieldValueReferenceFromContextField() { @@ -416,8 +445,9 @@ void testShorthandFieldValueReferenceFromContextField_WithNodeContext() { assertThrows(ParseCancellationException.class, () -> translateTemplate("{ND-Root} $value")); } + // #endregion Expression block ---------------------------------------------- - /*** Other ***/ + // #region Other ----------------------------------------------------------- @Test void testNestedExpression() { @@ -432,4 +462,6 @@ void testEndOfLineComments() { "let block01() -> { label(concat('field', '|', 'name', '|', 'BT-00-Text'))text(' blah blah') }\nfor-each(/*).call(block01())", translateTemplate("{ND-Root} #{name|BT-00-Text} blah blah // comment blah blah")); } + + // #endregion Other -------------------------------------------------------- }