diff --git a/build_effective_set_generator_java/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/ExpressionLanguage.java b/build_effective_set_generator_java/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/ExpressionLanguage.java index 1b52d4c7b..47236d0f5 100644 --- a/build_effective_set_generator_java/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/ExpressionLanguage.java +++ b/build_effective_set_generator_java/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/ExpressionLanguage.java @@ -16,13 +16,13 @@ package org.qubership.cloud.parameters.processor.expression; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.LoadingCache; import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.JinjavaConfig; +import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.FatalTemplateErrorsException; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; import groovy.lang.GroovyClassLoader; import groovy.lang.GroovyRuntimeException; import groovy.text.GStringTemplateEngine; @@ -35,7 +35,6 @@ import org.qubership.cloud.devops.gstringtojinjavatranslator.jinjava.*; import org.qubership.cloud.devops.gstringtojinjavatranslator.translator.GStringToJinJavaTranslator; import org.qubership.cloud.parameters.processor.MergeMap; -import org.qubership.cloud.parameters.processor.ParametersProcessor; import org.qubership.cloud.parameters.processor.exceptions.ExpressionLanguageException; import org.qubership.cloud.parameters.processor.expression.binding.Binding; import org.qubership.cloud.parameters.processor.expression.binding.DynamicMap; @@ -56,11 +55,17 @@ public class ExpressionLanguage extends AbstractLanguage { private static final Pattern EXPRESSION_PATTERN = Pattern.compile("(?$") // <% VARIABLE %> + }; private boolean insecure; private final Jinjava jinjava; private final GStringToJinJavaTranslator gStringToJinJavaTranslator; + private final DynamicPropertyResolver dynamicResolver; public ExpressionLanguage(Binding binding) { super(binding); @@ -78,6 +83,8 @@ public ExpressionLanguage(Binding binding) { this.binding.forEach((key1, value) -> this.binding.put(key1, translateParameter(value.getValue()))); + + this.dynamicResolver = new DynamicPropertyResolver(this.binding); } private Parameter translateParameter(Object value) { @@ -209,6 +216,12 @@ private Parameter processValue(Object value, Map binding, boo parameter.setValue(removeEscaping(escapeDollar, parameter.getValue())); return parameter; } + + Parameter preserved = tryPreserveType(value, binding); + if (preserved != null) { + return preserved; + } + Object val = getValue(value); boolean isProcessed = false; boolean isSecured = getIsSecured(value); @@ -216,22 +229,19 @@ private Parameter processValue(Object value, Map binding, boo if (val instanceof String) { String strValue = (String) val; - - String rendered = ""; - this.binding.getTypeCollector().clear(); + String jinJavaRendered = ""; try { - rendered = renderStringByJinJava(strValue, binding, escapeDollar); + jinJavaRendered = renderStringByJinJava(strValue, binding, escapeDollar); + val = jinJavaRendered; } catch (Exception e) { log.debug(String.format("Parameter {} was not processed by JinJava, hence reverting to Groovy.", strValue)); - rendered = renderStringByGroovy(strValue, binding, escapeDollar); + String groovyRendered = renderStringByGroovy(strValue, binding, escapeDollar); + val = groovyRendered; } - Object originalValue = this.binding.getTypeCollector().get(rendered); // Object - Class targetType = (originalValue != null) ? (Class) originalValue : String.class; - val = convertToType(rendered, targetType); isProcessed = true; - Matcher secureMarkerMatcher = SECURED_PATTERN.matcher(String.valueOf(val)); + Matcher secureMarkerMatcher = SECURED_PATTERN.matcher((String) val); if (secureMarkerMatcher.find()) { isSecured = true; val = ((String) Objects.requireNonNull(val)).replaceAll("([\\u0096\\u0097])", ""); @@ -248,6 +258,88 @@ private Parameter processValue(Object value, Map binding, boo return ret; } + // Extracts variable name from simple parameter reference patterns: ${VAR}, $VAR, or <% VAR %>. + private String extractParameterReference(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + for (Pattern pattern : PARAMETER_REFERENCE_PATTERNS) { + Matcher matcher = pattern.matcher(trimmed); + if (matcher.matches()) { + return matcher.group(1); + } + } + return null; + } + + // Preserves data type for simple parameter references (${VAR}, $VAR) using Jinjava's expression resolver. + // Returns null if type preservation is not applicable (complex expressions or String values). + private Parameter tryPreserveType(Object value, Map binding) { + Object val = getValue(value); + if (!(val instanceof String)) { + return null; + } + + String referencedVar = extractParameterReference((String) val); + if (referencedVar == null) { + return null; + } + + // Use Jinjava's expression resolver - returns typed Object (Integer, Boolean, etc.) + Object resolvedValue = resolveWithJinjava(referencedVar, binding); + + // Unwrap if still a Parameter + if (resolvedValue instanceof Parameter) { + resolvedValue = ((Parameter) resolvedValue).getValue(); + } + + if (resolvedValue == null || resolvedValue instanceof String) { + return null; + } + + // Get secured flag from source parameter + Parameter srcParam = binding.get(referencedVar); + if (srcParam == null) { + srcParam = this.binding.get(referencedVar); + } + boolean isSecured = srcParam != null && srcParam.isSecured(); + + Parameter result = new Parameter(resolvedValue); + if (value instanceof Parameter) { + result.setOrigin(((Parameter) value).getOrigin()); + } + result.setParsed(true); + result.setProcessed(true); + result.setSecured(isSecured); + result.setValid(true); + + log.debug("Type preserved for {}: {} ({})", referencedVar, resolvedValue, resolvedValue.getClass().getSimpleName()); + return result; + } + + // Uses Jinjava's expression resolver which returns typed Object instead of String. + private Object resolveWithJinjava(String expression, Map binding) { + try { + Context context = new Context(jinjava.getGlobalContextCopy(), binding, jinjava.getGlobalConfig().getDisabled()); + context.setDynamicVariableResolver(this.dynamicResolver); + + JinjavaInterpreter interpreter = jinjava.getGlobalConfig() + .getInterpreterFactory() + .newInstance(jinjava, context, jinjava.getGlobalConfig()); + + JinjavaInterpreter.pushCurrent(interpreter); + try { + return interpreter.resolveELExpression(expression, -1); + } finally { + JinjavaInterpreter.popCurrent(); + } + } catch (Exception e) { + log.debug("Failed to resolve '{}' with Jinjava: {}", expression, e.getMessage()); + return null; + } + } + private String renderStringByGroovy(String value, Map binding, boolean escapeDollar) { int i = 0; String rendered = value; @@ -278,21 +370,14 @@ private String renderStringByJinJava(String value, Map bindin return rendered; } - private Object removeEscaping(boolean escapeDollar, Object val) throws JsonProcessingException { - - if (escapeDollar && val != null) { - Class originalType = val.getClass(); - String strValue; - if (val instanceof String) { - strValue = val.toString(); - } else { - strValue = mapper.writeValueAsString(val); - } - strValue = strValue.replaceAll("\\\\\\$", "\\$"); // \$ -> $ - strValue = strValue.replaceAll("\\\\\\\\", "\\\\"); // \\ -> \ - return convertToType(strValue, originalType); + private Object removeEscaping(boolean escapeDollar, Object val) { + // Only process escaping for String values - non-String types (Integer, Boolean, etc.) + // don't need escape processing and should preserve their original type + if (escapeDollar && val instanceof String) { + String strValue = (String) val; + strValue = strValue.replaceAll("\\\\\\$", "\\$"); // \$ -> $ + val = strValue.replaceAll("\\\\\\\\", "\\\\"); // \\ -> \ } - return val; } @@ -477,18 +562,4 @@ public Map processParameters(Map parameters) return processedParams; } - private static Object convertToType(String value, Class type) { - if (type == String.class) { - return value; - } else if (type == Integer.class || type == int.class) { - return Integer.parseInt(value); - } else if (type == Long.class || type == long.class) { - return Long.parseLong(value); - } else if (type == Boolean.class || type == boolean.class) { - return Boolean.parseBoolean(value); - } else if (type == Double.class || type == double.class) { - return Double.parseDouble(value); - } - return value; - } } diff --git a/build_effective_set_generator_java/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/Binding.java b/build_effective_set_generator_java/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/Binding.java index c6c3bec29..5996c59d4 100644 --- a/build_effective_set_generator_java/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/Binding.java +++ b/build_effective_set_generator_java/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/Binding.java @@ -47,8 +47,6 @@ public class Binding extends HashMap implements Cloneable { private String tenant; private ParametersParser escapeParser; private ParametersParser oldParser; - @Getter - private Map> typeCollector = new HashMap<>(); public Binding(String defaultEscapeSequence) { this.escapeSequence = defaultEscapeSequence; @@ -212,9 +210,6 @@ public Parameter get(Object key) { if (result == null || result.getValue() == null) { return null; } - if (result != null && result.getValue() != null) { - typeCollector.put(result.getValue().toString(), result.getValue().getClass()); - } return result; } diff --git a/build_effective_set_generator_java/parameters-processor/src/test/java/org/qubership/cloud/expression/ExpressionLanguageTest.java b/build_effective_set_generator_java/parameters-processor/src/test/java/org/qubership/cloud/expression/ExpressionLanguageTest.java index 37ed10c9e..e8a0738e3 100644 --- a/build_effective_set_generator_java/parameters-processor/src/test/java/org/qubership/cloud/expression/ExpressionLanguageTest.java +++ b/build_effective_set_generator_java/parameters-processor/src/test/java/org/qubership/cloud/expression/ExpressionLanguageTest.java @@ -566,4 +566,125 @@ void processedGlobalResourceProfileMustBeSuccessfullyProcessedAgain() throws NoS processMap.setAccessible(true); assertEquals("{GLOBAL_RESOURCE_PROFILE={key1=value1, key2=value2}}", processMap.invoke(el, binding, binding, true).toString()); } + + // Test that data types are preserved when one parameter references another. + // This addresses the issue where EXPVAR: ${ORGVAR} was being converted to a String instead of preserving the Integer type. + + @Test + void testTypePreservationForIntegerReference() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Binding binding = new Binding("true"); + + Parameter intParam = new Parameter(27017); + binding.put("MONGO_DB_PORT", intParam); + + Parameter refParam = new Parameter("${MONGO_DB_PORT}"); + binding.put("DUMPS_MONGO_PORT", refParam); + + ExpressionLanguage el = new ExpressionLanguage(binding); + Method processValue = ExpressionLanguage.class.getDeclaredMethod("processValue", Object.class); + processValue.setAccessible(true); + + Parameter result = (Parameter) processValue.invoke(el, refParam); + + assertThat(result.getValue(), instanceOf(Integer.class)); + assertEquals(27017, result.getValue()); + } + + @Test + void testTypePreservationForBooleanWithBraceSyntax() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Binding binding = new Binding("true"); + + Parameter boolParam = new Parameter(true); + binding.put("ENABLE_SSL", boolParam); + + Parameter refParam = new Parameter("${ENABLE_SSL}"); + + ExpressionLanguage el = new ExpressionLanguage(binding); + Method processValue = ExpressionLanguage.class.getDeclaredMethod("processValue", Object.class); + processValue.setAccessible(true); + + Parameter result = (Parameter) processValue.invoke(el, refParam); + + assertThat(result.getValue(), instanceOf(Boolean.class)); + assertEquals(true, result.getValue()); + } + + @Test + void testTypePreservationForBooleanWithDollarSyntax() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Binding binding = new Binding("true"); + + Parameter boolParam = new Parameter(true); + binding.put("ENABLE_SSL", boolParam); + + Parameter refParam = new Parameter("$ENABLE_SSL"); + + ExpressionLanguage el = new ExpressionLanguage(binding); + Method processValue = ExpressionLanguage.class.getDeclaredMethod("processValue", Object.class); + processValue.setAccessible(true); + + Parameter result = (Parameter) processValue.invoke(el, refParam); + + assertThat(result.getValue(), instanceOf(Boolean.class)); + assertEquals(true, result.getValue()); + } + + @Test + void testTypePreservationForGroovyStyleReference() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Binding binding = new Binding("true"); + + Parameter intParam = new Parameter(8080); + binding.put("BASE_PORT", intParam); + + Parameter refParam = new Parameter("$BASE_PORT"); + binding.put("SERVER_PORT", refParam); + + ExpressionLanguage el = new ExpressionLanguage(binding); + Method processValue = ExpressionLanguage.class.getDeclaredMethod("processValue", Object.class); + processValue.setAccessible(true); + + Parameter result = (Parameter) processValue.invoke(el, refParam); + + assertThat(result.getValue(), instanceOf(Integer.class)); + assertEquals(8080, result.getValue()); + } + + @Test + void testTypePreservationForLongReference() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Binding binding = new Binding("true"); + + Parameter longParam = new Parameter(604800000L); + binding.put("CDC_TOPIC_STREAMING_RETENTION_MS", longParam); + + Parameter refParam = new Parameter("${CDC_TOPIC_STREAMING_RETENTION_MS}"); + binding.put("RETENTION_COPY", refParam); + + ExpressionLanguage el = new ExpressionLanguage(binding); + Method processValue = ExpressionLanguage.class.getDeclaredMethod("processValue", Object.class); + processValue.setAccessible(true); + + Parameter result = (Parameter) processValue.invoke(el, refParam); + + assertThat(result.getValue(), instanceOf(Long.class)); + assertEquals(604800000L, result.getValue()); + } + + @Test + void testStringReferenceShouldNotPreserveType() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Binding binding = new Binding("true"); + + Parameter strParam = new Parameter("myhost.local"); + binding.put("TEST_HOST", strParam); + + Parameter refParam = new Parameter("${TEST_HOST}"); + + ExpressionLanguage el = new ExpressionLanguage(binding); + Method processValue = ExpressionLanguage.class.getDeclaredMethod("processValue", Object.class); + processValue.setAccessible(true); + + Parameter result = (Parameter) processValue.invoke(el, refParam); + + assertThat(result.getValue(), instanceOf(String.class)); + assertEquals("myhost.local", result.getValue()); + } + }