From 6ad799b6d0856be122dfc9b85b00d52aced7a6b1 Mon Sep 17 00:00:00 2001 From: Hans Van Akelyen Date: Fri, 17 Apr 2026 12:01:44 +0200 Subject: [PATCH] Add option to select java version and tests, fixes #5172 --- .../pipeline/transforms/janino/Janino.java | 9 + .../transforms/janino/JaninoDialog.java | 40 +- .../transforms/janino/JaninoMeta.java | 28 + .../janino/function/FunctionLib.java | 12 +- .../UserDefinedJavaClassDialog.java | 36 ++ .../UserDefinedJavaClassMeta.java | 43 +- .../janino/messages/messages_en_US.properties | 1 + .../messages/messages_en_US.properties | 1 + .../janino/JaninoMetaFunctionTest.java | 115 ++++ .../transforms/janino/JaninoMetaTest.java | 243 ++++++++- .../transforms/janino/JaninoTest.java | 383 ++++++++++++++ .../javafilter/JavaFilterMetaTest.java | 494 ++++++++++++++++++ .../transforms/javafilter/JavaFilterTest.java | 257 +++++++++ .../userdefinedjavaclass/FieldHelperTest.java | 160 ++++++ .../TransformClassBaseStaticMethodsTest.java | 173 ++++++ .../TransformDefinitionsTest.java | 212 ++++++++ .../UserDefinedJavaClassCodeSnippetsTest.java | 86 +++ .../UserDefinedJavaClassDefTest.java | 153 ++++++ .../UserDefinedJavaClassMetaTest.java | 373 +++++++++++++ .../UserDefinedJavaClassTest.java | 188 +++++++ .../util/JaninoCheckerUtilTest.java | 124 +++++ .../janino/src/test/resources/java-filter.xml | 22 + .../resources/user-defined-java-class.xml | 1 + 23 files changed, 3145 insertions(+), 9 deletions(-) create mode 100644 plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/janino/JaninoMetaFunctionTest.java create mode 100644 plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/janino/JaninoTest.java create mode 100644 plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/javafilter/JavaFilterMetaTest.java create mode 100644 plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/javafilter/JavaFilterTest.java create mode 100644 plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/TransformClassBaseStaticMethodsTest.java create mode 100644 plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/TransformDefinitionsTest.java create mode 100644 plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassCodeSnippetsTest.java create mode 100644 plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassDefTest.java create mode 100644 plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassTest.java create mode 100644 plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/util/JaninoCheckerUtilTest.java create mode 100644 plugins/transforms/janino/src/test/resources/java-filter.xml diff --git a/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/janino/Janino.java b/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/janino/Janino.java index 00bff1e9710..3136dceadc6 100644 --- a/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/janino/Janino.java +++ b/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/janino/Janino.java @@ -173,6 +173,15 @@ private Object[] calcFields(IRowMeta rowMeta, Object[] r) throws HopValueExcepti data.expressionEvaluators[m].setThrownExceptions(new Class[] {Exception.class}); data.expressionEvaluators[m].setParentClassLoader(loader); data.expressionEvaluators[m].setDefaultImports(functionLib.getImportPackages()); + int javaVersion = meta.getEffectiveJavaTargetVersion(); + data.expressionEvaluators[m].setTargetVersion(javaVersion); + // Janino default: parse up to Java 11 when source is unset, emit Java 6 bytecode when + // target is unset. + // Keep that permissive parse level for the default target (6) so existing expressions + // keep working. + if (javaVersion > JaninoMeta.JAVA_TARGET_VERSION_MIN) { + data.expressionEvaluators[m].setSourceVersion(javaVersion); + } // Validate Formula JaninoCheckerUtil janinoCheckerUtil = new JaninoCheckerUtil(); diff --git a/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/janino/JaninoDialog.java b/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/janino/JaninoDialog.java index a340064cd47..7cae5572510 100644 --- a/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/janino/JaninoDialog.java +++ b/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/janino/JaninoDialog.java @@ -39,6 +39,7 @@ import org.apache.hop.ui.core.widget.TableView; import org.apache.hop.ui.pipeline.transform.BaseTransformDialog; import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.CCombo; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.layout.FormAttachment; @@ -50,8 +51,15 @@ public class JaninoDialog extends BaseTransformDialog { private static final Class PKG = JaninoMeta.class; + private static final String[] JAVA_TARGET_VERSION_ITEMS = + new String[] { + "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21" + }; + private TableView wFields; + private CCombo wJavaTargetVersion; + private final JaninoMeta currentMeta; private final JaninoMeta originalMeta; @@ -75,12 +83,31 @@ public String open() { changed = currentMeta.hasChanged(); + Label wlJavaTargetVersion = new Label(shell, SWT.RIGHT); + wlJavaTargetVersion.setText( + BaseMessages.getString(PKG, "JaninoDialog.JavaTargetVersion.Label")); + PropsUi.setLook(wlJavaTargetVersion); + FormData fdlJavaTargetVersion = new FormData(); + fdlJavaTargetVersion.left = new FormAttachment(0, 0); + fdlJavaTargetVersion.right = new FormAttachment(middle, -margin); + fdlJavaTargetVersion.top = new FormAttachment(wSpacer, margin); + wlJavaTargetVersion.setLayoutData(fdlJavaTargetVersion); + + wJavaTargetVersion = new CCombo(shell, SWT.BORDER | SWT.READ_ONLY); + PropsUi.setLook(wJavaTargetVersion); + wJavaTargetVersion.setItems(JAVA_TARGET_VERSION_ITEMS); + FormData fdJavaTargetVersion = new FormData(); + fdJavaTargetVersion.left = new FormAttachment(middle, 0); + fdJavaTargetVersion.right = new FormAttachment(100, 0); + fdJavaTargetVersion.top = new FormAttachment(wSpacer, margin); + wJavaTargetVersion.setLayoutData(fdJavaTargetVersion); + Label wlFields = new Label(shell, SWT.NONE); wlFields.setText(BaseMessages.getString(PKG, "JaninoDialog.Fields.Label")); PropsUi.setLook(wlFields); FormData fdlFields = new FormData(); fdlFields.left = new FormAttachment(0, 0); - fdlFields.top = new FormAttachment(wSpacer, margin); + fdlFields.top = new FormAttachment(wJavaTargetVersion, margin); wlFields.setLayoutData(fdlFields); final int nrFields = currentMeta.getFunctions().size(); @@ -214,6 +241,14 @@ protected void setComboBoxes() { /** Copy information from the meta-data currentMeta to the dialog fields. */ public void getData() { + int effectiveVersion = currentMeta.getEffectiveJavaTargetVersion(); + int versionIndex = effectiveVersion - JaninoMeta.JAVA_TARGET_VERSION_MIN; + if (versionIndex >= 0 && versionIndex < JAVA_TARGET_VERSION_ITEMS.length) { + wJavaTargetVersion.select(versionIndex); + } else { + wJavaTargetVersion.select(0); + } + if (currentMeta.getFunctions() != null) { for (int i = 0; i < currentMeta.getFunctions().size(); i++) { JaninoMetaFunction function = currentMeta.getFunctions().get(i); @@ -265,6 +300,9 @@ private void ok() { transformName = wTransformName.getText(); // return value + currentMeta.setJavaTargetVersion( + Const.toInt(wJavaTargetVersion.getText(), JaninoMeta.JAVA_TARGET_VERSION_DEFAULT)); + currentMeta.getFunctions().clear(); for (TableItem item : wFields.getNonEmptyItems()) { JaninoMetaFunction function = new JaninoMetaFunction(); diff --git a/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/janino/JaninoMeta.java b/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/janino/JaninoMeta.java index 1cd6d464434..5127f2ac0f0 100644 --- a/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/janino/JaninoMeta.java +++ b/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/janino/JaninoMeta.java @@ -49,6 +49,12 @@ public class JaninoMeta extends BaseTransformMeta { private static final Class PKG = JaninoMeta.class; + /** Default matches Janino bytecode default ({@code UnitCompiler#getDefaultTargetVersion()}). */ + public static final int JAVA_TARGET_VERSION_DEFAULT = 6; + + public static final int JAVA_TARGET_VERSION_MIN = 6; + public static final int JAVA_TARGET_VERSION_MAX = 21; + /** The formula calculations to be performed */ @HopMetadataProperty( key = "formula", @@ -56,6 +62,15 @@ public class JaninoMeta extends BaseTransformMeta { injectionGroupDescription = "Janino.Injection.FORMULA") private List functions; + /** + * Java language / class file level passed to Janino ({@link + * org.codehaus.janino.ExpressionEvaluator #setSourceVersion} and {@link + * org.codehaus.janino.ExpressionEvaluator#setTargetVersion}). When unset or invalid, {@link + * #JAVA_TARGET_VERSION_DEFAULT} is used. + */ + @HopMetadataProperty(key = "java_target_version") + private int javaTargetVersion = JAVA_TARGET_VERSION_DEFAULT; + public JaninoMeta() { super(); this.functions = new ArrayList<>(); @@ -64,6 +79,19 @@ public JaninoMeta() { public JaninoMeta(JaninoMeta m) { this(); m.functions.forEach(f -> this.functions.add(new JaninoMetaFunction(f))); + this.javaTargetVersion = m.javaTargetVersion; + } + + /** + * Resolved Janino compiler source/target version (major Java version number), for backwards + * compatibility when pipelines omit {@link #javaTargetVersion} or contain invalid values. + */ + public int getEffectiveJavaTargetVersion() { + if (javaTargetVersion < JAVA_TARGET_VERSION_MIN + || javaTargetVersion > JAVA_TARGET_VERSION_MAX) { + return JAVA_TARGET_VERSION_DEFAULT; + } + return javaTargetVersion; } @Override diff --git a/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/janino/function/FunctionLib.java b/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/janino/function/FunctionLib.java index 1eb0f1761e6..37e4072725b 100644 --- a/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/janino/function/FunctionLib.java +++ b/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/janino/function/FunctionLib.java @@ -29,6 +29,7 @@ import java.util.List; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.hop.core.exception.HopException; import org.apache.hop.core.plugins.IPlugin; import org.apache.hop.core.plugins.PluginRegistry; @@ -175,7 +176,16 @@ public Set> findAllClassesUsingGoogleGuice(ClassLoader classLoader, Str throws IOException { return ClassPath.from(classLoader).getAllClasses().stream() .filter(clazz -> clazz.getPackageName().contains(packageName)) - .map(ClassPath.ClassInfo::load) + .flatMap( + clazz -> { + try { + return Stream.of(clazz.load()); + } catch (Exception | Error e) { + // Skip classes that cannot be loaded (e.g. bad path-based class names from + // test-classpath entries, missing dependencies, incompatible bytecode). + return Stream.empty(); + } + }) .collect(Collectors.toSet()); } } diff --git a/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassDialog.java b/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassDialog.java index 73243815665..2377d09de10 100644 --- a/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassDialog.java +++ b/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassDialog.java @@ -40,6 +40,7 @@ import org.apache.hop.pipeline.PipelineHopMeta; import org.apache.hop.pipeline.PipelineMeta; import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.pipeline.transforms.janino.JaninoMeta; import org.apache.hop.pipeline.transforms.rowgenerator.GeneratorField; import org.apache.hop.pipeline.transforms.rowgenerator.RowGeneratorMeta; import org.apache.hop.pipeline.transforms.userdefinedjavaclass.UserDefinedJavaClassCodeSnippets.Category; @@ -66,6 +67,7 @@ import org.apache.hop.ui.util.EnvironmentUtils; import org.apache.hop.ui.util.SwtSvgImageUtil; import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.CCombo; import org.eclipse.swt.custom.CTabFolder; import org.eclipse.swt.custom.CTabFolder2Adapter; import org.eclipse.swt.custom.CTabFolderEvent; @@ -108,10 +110,17 @@ public class UserDefinedJavaClassDialog extends BaseTransformDialog { public static final String CONST_SET_VALUE = "setValue()"; public static final String CONST_SNIPPITS_CATEGORY = "Snippits Category"; + private static final String[] JAVA_TARGET_VERSION_ITEMS = + new String[] { + "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21" + }; + private ModifyListener lsMod; private TableView wFields; + private CCombo wJavaTargetVersion; + private Label wlPosition; private Button wClearResultFields; @@ -224,6 +233,28 @@ public String open() { Control lastControl = wSpacer; + Label wlJavaTargetVersion = new Label(shell, SWT.RIGHT); + wlJavaTargetVersion.setText( + BaseMessages.getString(PKG, "UserDefinedJavaClassDialog.JavaTargetVersion.Label")); + PropsUi.setLook(wlJavaTargetVersion); + FormData fdlJavaTargetVersion = new FormData(); + fdlJavaTargetVersion.left = new FormAttachment(0, 0); + fdlJavaTargetVersion.right = new FormAttachment(middle, -margin); + fdlJavaTargetVersion.top = new FormAttachment(lastControl, margin); + wlJavaTargetVersion.setLayoutData(fdlJavaTargetVersion); + + wJavaTargetVersion = new CCombo(shell, SWT.BORDER | SWT.READ_ONLY); + PropsUi.setLook(wJavaTargetVersion); + wJavaTargetVersion.setItems(JAVA_TARGET_VERSION_ITEMS); + wJavaTargetVersion.addModifyListener(lsMod); + FormData fdJavaTargetVersion = new FormData(); + fdJavaTargetVersion.left = new FormAttachment(middle, 0); + fdJavaTargetVersion.right = new FormAttachment(100, 0); + fdJavaTargetVersion.top = new FormAttachment(lastControl, margin); + wJavaTargetVersion.setLayoutData(fdJavaTargetVersion); + + lastControl = wJavaTargetVersion; + SashForm wSash = new SashForm(shell, SWT.VERTICAL); // Top sash form @@ -1027,6 +1058,8 @@ public void getData() { wClearResultFields.setSelection(input.isClearingResultFields()); + wJavaTargetVersion.setText(Integer.toString(input.getEffectiveJavaTargetVersion())); + wFields.setRowNums(); wFields.optWidth(true); @@ -1118,6 +1151,9 @@ private boolean cancel() { } private void getInfo(UserDefinedJavaClassMeta meta) { + meta.setJavaTargetVersion( + Const.toInt(wJavaTargetVersion.getText(), JaninoMeta.JAVA_TARGET_VERSION_DEFAULT)); + int nrFields = wFields.nrNonEmpty(); List newFields = new ArrayList<>(nrFields); for (int i = 0; i < nrFields; i++) { diff --git a/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassMeta.java b/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassMeta.java index 9552e1b3f74..4e10d11a8be 100644 --- a/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassMeta.java +++ b/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassMeta.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; import org.apache.hop.core.CheckResult; @@ -46,6 +47,7 @@ import org.apache.hop.pipeline.transform.BaseTransformMeta; import org.apache.hop.pipeline.transform.ITransformIOMeta; import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.pipeline.transforms.janino.JaninoMeta; import org.apache.hop.pipeline.transforms.util.JaninoCheckerUtil; import org.codehaus.commons.compiler.CompileException; import org.codehaus.janino.ClassBodyEvaluator; @@ -181,6 +183,34 @@ public Object clone() throws CloneNotSupportedException { injectionGroupDescription = "UserDefinedJavaClass.Injection.PARAMETERS") private List usageParameters; + /** + * Janino bytecode / language level for compiling embedded user classes (same semantics as {@link + * JaninoMeta#getEffectiveJavaTargetVersion()}). + */ + @Getter + @Setter(AccessLevel.NONE) + @HopMetadataProperty(key = "java_target_version") + private int javaTargetVersion = JaninoMeta.JAVA_TARGET_VERSION_DEFAULT; + + public void setJavaTargetVersion(int javaTargetVersion) { + if (this.javaTargetVersion != javaTargetVersion) { + this.javaTargetVersion = javaTargetVersion; + this.hasChanged = true; + } + } + + /** + * Resolved Janino compiler source/target version for {@link ClassBodyEvaluator}, for backwards + * compatibility when pipelines omit {@link #javaTargetVersion} or contain invalid values. + */ + public int getEffectiveJavaTargetVersion() { + if (javaTargetVersion < JaninoMeta.JAVA_TARGET_VERSION_MIN + || javaTargetVersion > JaninoMeta.JAVA_TARGET_VERSION_MAX) { + return JaninoMeta.JAVA_TARGET_VERSION_DEFAULT; + } + return javaTargetVersion; + } + public UserDefinedJavaClassMeta() { super(); hasChanged = true; @@ -201,14 +231,15 @@ public UserDefinedJavaClassMeta(UserDefinedJavaClassMeta m) { m.targetTransformDefinitions.forEach( d -> this.targetTransformDefinitions.add(new TargetTransformDefinition(d))); m.usageParameters.forEach(u -> this.usageParameters.add(new UsageParameter(u))); + this.javaTargetVersion = m.javaTargetVersion; } @VisibleForTesting Class cookClass(UserDefinedJavaClassDef def, ClassLoader clsLoader) throws CompileException, IOException, HopTransformException { - String checksum = def.getChecksum(); - Class rtn = UserDefinedJavaClassMeta.CLASS_CACHE.getIfPresent(checksum); + String cacheKey = def.getChecksum() + ":" + getEffectiveJavaTargetVersion(); + Class rtn = UserDefinedJavaClassMeta.CLASS_CACHE.getIfPresent(cacheKey); if (rtn != null) { return rtn; } @@ -247,9 +278,15 @@ Class cookClass(UserDefinedJavaClassDef def, ClassLoader clsLoader) "org.apache.hop.core.variables.*", "java.util.*"); + int javaVersion = getEffectiveJavaTargetVersion(); + cbe.setTargetVersion(javaVersion); + if (javaVersion > JaninoMeta.JAVA_TARGET_VERSION_MIN) { + cbe.setSourceVersion(javaVersion); + } + cbe.cook(new Scanner(null, sr)); rtn = cbe.getClazz(); - UserDefinedJavaClassMeta.CLASS_CACHE.put(checksum, rtn); + UserDefinedJavaClassMeta.CLASS_CACHE.put(cacheKey, rtn); return rtn; } diff --git a/plugins/transforms/janino/src/main/resources/org/apache/hop/pipeline/transforms/janino/messages/messages_en_US.properties b/plugins/transforms/janino/src/main/resources/org/apache/hop/pipeline/transforms/janino/messages/messages_en_US.properties index 571ff8de87f..480f1c13ce7 100644 --- a/plugins/transforms/janino/src/main/resources/org/apache/hop/pipeline/transforms/janino/messages/messages_en_US.properties +++ b/plugins/transforms/janino/src/main/resources/org/apache/hop/pipeline/transforms/janino/messages/messages_en_US.properties @@ -28,6 +28,7 @@ Janino.Injection.VALUE_PRECISION=Precision Janino.Injection.VALUE_TYPE=Type Janino.Name=User defined Java expression JaninoDialog.DialogTitle=User defined Java expression +JaninoDialog.JavaTargetVersion.Label=Java target version: JaninoDialog.Fields.Label=Fields: JaninoDialog.Janino.Column=Java expression JaninoDialog.Length.Column=Length diff --git a/plugins/transforms/janino/src/main/resources/org/apache/hop/pipeline/transforms/userdefinedjavaclass/messages/messages_en_US.properties b/plugins/transforms/janino/src/main/resources/org/apache/hop/pipeline/transforms/userdefinedjavaclass/messages/messages_en_US.properties index 293503295a8..e857301ccd9 100644 --- a/plugins/transforms/janino/src/main/resources/org/apache/hop/pipeline/transforms/userdefinedjavaclass/messages/messages_en_US.properties +++ b/plugins/transforms/janino/src/main/resources/org/apache/hop/pipeline/transforms/userdefinedjavaclass/messages/messages_en_US.properties @@ -80,6 +80,7 @@ UserDefinedJavaClassDialog.GettingFields.Label=Getting fields...please wait UserDefinedJavaClassDialog.InfoFields.Label=Info fields UserDefinedJavaClassDialog.InfoTransforms.Label=Info transforms\: UserDefinedJavaClassDialog.InputFields.Label=Input fields +UserDefinedJavaClassDialog.JavaTargetVersion.Label=Java target version\: UserDefinedJavaClassDialog.NoTransformClassSet=No class tab has been set as the transform class\! Should the first tab set as active Script? UserDefinedJavaClassDialog.OutputFields.Label=Output fields UserDefinedJavaClassDialog.Parameters.Label=Parameters\: diff --git a/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/janino/JaninoMetaFunctionTest.java b/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/janino/JaninoMetaFunctionTest.java new file mode 100644 index 00000000000..9df82d231ef --- /dev/null +++ b/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/janino/JaninoMetaFunctionTest.java @@ -0,0 +1,115 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hop.pipeline.transforms.janino; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.hop.core.row.IValueMeta; +import org.junit.jupiter.api.Test; + +class JaninoMetaFunctionTest { + + private static JaninoMetaFunction build(String fieldName) { + JaninoMetaFunction f = new JaninoMetaFunction(); + f.setFieldName(fieldName); + f.setFormula("1+1"); + f.setValueType(IValueMeta.TYPE_INTEGER); + f.setValueLength(9); + f.setValuePrecision(0); + f.setReplaceField("rep"); + return f; + } + + // ------------------------------------------------------------------ equals + + @Test + void equals_sameFieldName_returnsTrue() { + JaninoMetaFunction a = build("x"); + JaninoMetaFunction b = build("x"); + assertTrue(a.equals(b)); + } + + @Test + void equals_differentFieldName_returnsFalse() { + assertFalse(build("a").equals(build("b"))); + } + + @Test + void equals_null_returnsFalse() { + assertFalse(build("x").equals(null)); + } + + @Test + void equals_differentClass_returnsFalse() { + assertFalse(build("x").equals("x")); + } + + @Test + void equals_reflexive() { + JaninoMetaFunction f = build("y"); + assertTrue(f.equals(f)); + } + + // ------------------------------------------------------------------ hashCode + + @Test + void hashCode_equalObjects_sameHashCode() { + assertEquals(build("x").hashCode(), build("x").hashCode()); + } + + // ------------------------------------------------------------------ clone / copy constructor + + @Test + void copyConstructor_copiesAllFields() { + JaninoMetaFunction original = build("myField"); + JaninoMetaFunction copy = new JaninoMetaFunction(original); + + assertNotSame(original, copy); + assertEquals("myField", copy.getFieldName()); + assertEquals("1+1", copy.getFormula()); + assertEquals(IValueMeta.TYPE_INTEGER, copy.getValueType()); + assertEquals(9, copy.getValueLength()); + assertEquals(0, copy.getValuePrecision()); + assertEquals("rep", copy.getReplaceField()); + } + + @Test + void clone_returnsEqualButDistinctInstance() { + JaninoMetaFunction original = build("z"); + JaninoMetaFunction cloned = (JaninoMetaFunction) original.clone(); + + assertNotNull(cloned); + assertNotSame(original, cloned); + assertEquals(original.getFieldName(), cloned.getFieldName()); + assertEquals(original.getFormula(), cloned.getFormula()); + } + + // ------------------------------------------------------------------ default constructor + + @Test + void defaultConstructor_fieldsAreNullOrZero() { + JaninoMetaFunction f = new JaninoMetaFunction(); + assertFalse(f.equals(null)); // doesn't throw + assertEquals(0, f.getValueType()); + assertEquals(0, f.getValueLength()); + assertEquals(0, f.getValuePrecision()); + } +} diff --git a/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/janino/JaninoMetaTest.java b/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/janino/JaninoMetaTest.java index 9eee4ed8eec..2f08e89149d 100644 --- a/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/janino/JaninoMetaTest.java +++ b/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/janino/JaninoMetaTest.java @@ -18,13 +18,23 @@ package org.apache.hop.pipeline.transforms.janino; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; +import org.apache.hop.core.ICheckResult; +import org.apache.hop.core.exception.HopTransformException; import org.apache.hop.core.plugins.PluginRegistry; +import org.apache.hop.core.row.IRowMeta; import org.apache.hop.core.row.IValueMeta; +import org.apache.hop.core.row.RowMeta; import org.apache.hop.core.row.value.ValueMetaDate; import org.apache.hop.core.row.value.ValueMetaInteger; import org.apache.hop.core.row.value.ValueMetaNumber; @@ -32,8 +42,10 @@ import org.apache.hop.core.row.value.ValueMetaPluginType; import org.apache.hop.core.row.value.ValueMetaString; import org.apache.hop.core.xml.XmlHandler; +import org.apache.hop.metadata.api.IHopMetadataProvider; import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider; import org.apache.hop.metadata.serializer.xml.XmlMetadataUtil; +import org.apache.hop.pipeline.PipelineMeta; import org.apache.hop.pipeline.transform.TransformMeta; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -51,6 +63,8 @@ void beforeEach() throws Exception { } } + // ------------------------------------------------------------------ load/save + @Test void testLoadSave() throws Exception { Path path = Paths.get(Objects.requireNonNull(getClass().getResource("/janino.xml")).toURI()); @@ -62,10 +76,9 @@ void testLoadSave() throws Exception { meta, new MemoryMetadataProvider()); - validate(meta); + validateFixture(meta); // Do a round trip: - // String xmlCopy = XmlHandler.openTag(TransformMeta.XML_TAG) + XmlMetadataUtil.serializeObjectToXml(meta) @@ -76,10 +89,11 @@ void testLoadSave() throws Exception { JaninoMeta.class, metaCopy, new MemoryMetadataProvider()); - validate(metaCopy); + validateFixture(metaCopy); } - private static void validate(JaninoMeta meta) { + private static void validateFixture(JaninoMeta meta) { + assertEquals(JaninoMeta.JAVA_TARGET_VERSION_DEFAULT, meta.getEffectiveJavaTargetVersion()); assertEquals(3, meta.getFunctions().size()); JaninoMetaFunction f = meta.getFunctions().getFirst(); assertEquals("f1", f.getFieldName()); @@ -105,4 +119,225 @@ private static void validate(JaninoMeta meta) { assertEquals(2, f.getValuePrecision()); assertEquals("replace3", f.getReplaceField()); } + + // ------------------------------------------------------------------ + // getEffectiveJavaTargetVersion + + @Test + void effectiveVersion_default_returnsDefault() { + JaninoMeta meta = new JaninoMeta(); + assertEquals(JaninoMeta.JAVA_TARGET_VERSION_DEFAULT, meta.getEffectiveJavaTargetVersion()); + } + + @Test + void effectiveVersion_valid_returnsAsIs() { + JaninoMeta meta = new JaninoMeta(); + meta.setJavaTargetVersion(11); + assertEquals(11, meta.getEffectiveJavaTargetVersion()); + meta.setJavaTargetVersion(JaninoMeta.JAVA_TARGET_VERSION_MIN); + assertEquals(JaninoMeta.JAVA_TARGET_VERSION_MIN, meta.getEffectiveJavaTargetVersion()); + meta.setJavaTargetVersion(JaninoMeta.JAVA_TARGET_VERSION_MAX); + assertEquals(JaninoMeta.JAVA_TARGET_VERSION_MAX, meta.getEffectiveJavaTargetVersion()); + } + + @Test + void effectiveVersion_belowMin_returnsDefault() { + JaninoMeta meta = new JaninoMeta(); + meta.setJavaTargetVersion(0); + assertEquals(JaninoMeta.JAVA_TARGET_VERSION_DEFAULT, meta.getEffectiveJavaTargetVersion()); + meta.setJavaTargetVersion(-1); + assertEquals(JaninoMeta.JAVA_TARGET_VERSION_DEFAULT, meta.getEffectiveJavaTargetVersion()); + } + + @Test + void effectiveVersion_aboveMax_returnsDefault() { + JaninoMeta meta = new JaninoMeta(); + meta.setJavaTargetVersion(99); + assertEquals(JaninoMeta.JAVA_TARGET_VERSION_DEFAULT, meta.getEffectiveJavaTargetVersion()); + } + + // ------------------------------------------------------------------ clone + + @Test + void clone_copiesVersion() { + JaninoMeta meta = new JaninoMeta(); + meta.setJavaTargetVersion(17); + JaninoMetaFunction fn = new JaninoMetaFunction(); + fn.setFieldName("x"); + fn.setFormula("1"); + fn.setValueType(IValueMeta.TYPE_INTEGER); + meta.getFunctions().add(fn); + + JaninoMeta copy = (JaninoMeta) meta.clone(); + + assertEquals(17, copy.getJavaTargetVersion()); + assertEquals(1, copy.getFunctions().size()); + assertNotSame(meta.getFunctions().get(0), copy.getFunctions().get(0)); + } + + // ------------------------------------------------------------------ getFields + + @Test + void getFields_newField_addsToRow() throws HopTransformException { + JaninoMeta meta = new JaninoMeta(); + JaninoMetaFunction fn = new JaninoMetaFunction(); + fn.setFieldName("added"); + fn.setFormula("1+1"); + fn.setValueType(IValueMeta.TYPE_INTEGER); + fn.setValueLength(9); + fn.setValuePrecision(0); + meta.getFunctions().add(fn); + + RowMeta row = new RowMeta(); + row.addValueMeta(new ValueMetaString("existing")); + + meta.getFields(row, "step", null, null, null, mock(IHopMetadataProvider.class)); + + assertEquals(2, row.size()); + assertEquals("added", row.getValueMeta(1).getName()); + assertEquals(IValueMeta.TYPE_INTEGER, row.getValueMeta(1).getType()); + } + + @Test + void getFields_replaceField_changesExistingEntry() throws HopTransformException { + JaninoMeta meta = new JaninoMeta(); + JaninoMetaFunction fn = new JaninoMetaFunction(); + fn.setFieldName("n"); + fn.setFormula("n*2"); + fn.setValueType(IValueMeta.TYPE_INTEGER); + fn.setValueLength(9); + fn.setValuePrecision(0); + fn.setReplaceField("n"); + meta.getFunctions().add(fn); + + RowMeta row = new RowMeta(); + row.addValueMeta(new ValueMetaInteger("n")); + + meta.getFields(row, "step", null, null, null, mock(IHopMetadataProvider.class)); + + assertEquals(1, row.size()); // replaced, not appended + } + + @Test + void getFields_replaceFieldMissing_throwsHopTransformException() { + JaninoMeta meta = new JaninoMeta(); + JaninoMetaFunction fn = new JaninoMetaFunction(); + fn.setFieldName("x"); + fn.setFormula("1"); + fn.setValueType(IValueMeta.TYPE_INTEGER); + fn.setReplaceField("nonexistent"); + meta.getFunctions().add(fn); + + RowMeta row = new RowMeta(); + row.addValueMeta(new ValueMetaString("other")); + + assertThrows( + HopTransformException.class, + () -> meta.getFields(row, "step", null, null, null, mock(IHopMetadataProvider.class))); + } + + @Test + void getFields_emptyFieldName_skipped() throws HopTransformException { + JaninoMeta meta = new JaninoMeta(); + JaninoMetaFunction fn = new JaninoMetaFunction(); + fn.setFieldName(""); + fn.setFormula("1"); + fn.setValueType(IValueMeta.TYPE_INTEGER); + meta.getFunctions().add(fn); + + RowMeta row = new RowMeta(); + row.addValueMeta(new ValueMetaString("existing")); + + meta.getFields(row, "step", null, null, null, mock(IHopMetadataProvider.class)); + assertEquals(1, row.size()); // nothing added + } + + // ------------------------------------------------------------------ check + + @Test + void check_noPrevFields_addsWarning() { + JaninoMeta meta = new JaninoMeta(); + List remarks = new ArrayList<>(); + meta.check( + remarks, + mock(PipelineMeta.class), + mock(TransformMeta.class), + null, + new String[] {"in"}, + new String[0], + mock(IRowMeta.class), + null, + mock(IHopMetadataProvider.class)); + + assertTrue(remarks.stream().anyMatch(r -> r.getType() == ICheckResult.TYPE_RESULT_WARNING)); + } + + @Test + void check_withPrevFields_addsOk() { + JaninoMeta meta = new JaninoMeta(); + RowMeta prev = new RowMeta(); + prev.addValueMeta(new ValueMetaString("field1")); + + List remarks = new ArrayList<>(); + meta.check( + remarks, + mock(PipelineMeta.class), + mock(TransformMeta.class), + prev, + new String[] {"in"}, + new String[0], + mock(IRowMeta.class), + null, + mock(IHopMetadataProvider.class)); + + assertTrue(remarks.stream().anyMatch(r -> r.getType() == ICheckResult.TYPE_RESULT_OK)); + } + + @Test + void check_noInputTransforms_addsError() { + JaninoMeta meta = new JaninoMeta(); + List remarks = new ArrayList<>(); + meta.check( + remarks, + mock(PipelineMeta.class), + mock(TransformMeta.class), + new RowMeta(), + new String[0], + new String[0], + mock(IRowMeta.class), + null, + mock(IHopMetadataProvider.class)); + + assertTrue(remarks.stream().anyMatch(r -> r.getType() == ICheckResult.TYPE_RESULT_ERROR)); + } + + @Test + void check_withInputTransforms_addsOk() { + JaninoMeta meta = new JaninoMeta(); + RowMeta prev = new RowMeta(); + prev.addValueMeta(new ValueMetaString("f")); + + List remarks = new ArrayList<>(); + meta.check( + remarks, + mock(PipelineMeta.class), + mock(TransformMeta.class), + prev, + new String[] {"in"}, + new String[0], + mock(IRowMeta.class), + null, + mock(IHopMetadataProvider.class)); + + assertEquals(2, remarks.size()); + assertEquals(ICheckResult.TYPE_RESULT_OK, remarks.get(0).getType()); + assertEquals(ICheckResult.TYPE_RESULT_OK, remarks.get(1).getType()); + } + + // ------------------------------------------------------------------ supportsErrorHandling + + @Test + void supportsErrorHandling_returnsTrue() { + assertTrue(new JaninoMeta().supportsErrorHandling()); + } } diff --git a/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/janino/JaninoTest.java b/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/janino/JaninoTest.java new file mode 100644 index 00000000000..cf4f00eef70 --- /dev/null +++ b/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/janino/JaninoTest.java @@ -0,0 +1,383 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hop.pipeline.transforms.janino; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.apache.hop.core.IRowSet; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.logging.HopLogStore; +import org.apache.hop.core.logging.ILoggingObject; +import org.apache.hop.core.plugins.Plugin; +import org.apache.hop.core.plugins.PluginRegistry; +import org.apache.hop.core.plugins.TransformPluginType; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.row.IValueMeta; +import org.apache.hop.core.row.RowMeta; +import org.apache.hop.core.row.value.ValueMetaDate; +import org.apache.hop.core.row.value.ValueMetaInteger; +import org.apache.hop.core.row.value.ValueMetaNumber; +import org.apache.hop.core.row.value.ValueMetaPlugin; +import org.apache.hop.core.row.value.ValueMetaPluginType; +import org.apache.hop.core.row.value.ValueMetaString; +import org.apache.hop.pipeline.transform.TransformErrorMeta; +import org.apache.hop.pipeline.transforms.mock.TransformMockHelper; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +/** + * Unit tests for {@link Janino#processRow()} covering the data-transform path, the {@link + * JaninoMeta#javaTargetVersion} compilation gate, and error-handling routing. + */ +class JaninoTest { + + private TransformMockHelper helper; + + @BeforeAll + static void initPlugins() throws Exception { + HopLogStore.init(); + // Register value meta types needed by the row meta machinery. + PluginRegistry registry = PluginRegistry.getInstance(); + String[] valueMetaClasses = { + ValueMetaString.class.getName(), + ValueMetaInteger.class.getName(), + ValueMetaDate.class.getName(), + ValueMetaNumber.class.getName() + }; + for (String cls : valueMetaClasses) { + registry.registerPluginClass(cls, ValueMetaPluginType.class, ValueMetaPlugin.class); + } + // Register the Janino transform as a native plugin so getClassLoader() returns a usable + // loader without triggering the full classpath scan that HopEnvironment.init() does. + Plugin janinoPlugin = + new Plugin( + new String[] {"Janino"}, + TransformPluginType.class, + Janino.class, + "Scripting", + "User Defined Java Expression", + "", + "janino.svg", + false, + true, // native plugin → getClassLoader() returns registry's own loader + Map.of(Janino.class, Janino.class.getName()), + Collections.emptyList(), + null, + new String[0], + null, + false); + registry.registerPlugin(TransformPluginType.class, janinoPlugin); + } + + @BeforeEach + void setUp() { + helper = new TransformMockHelper<>("Janino TEST", JaninoMeta.class, JaninoData.class); + when(helper.logChannelFactory.create(any(), any(ILoggingObject.class))) + .thenReturn(helper.iLogChannel); + when(helper.pipeline.isRunning()).thenReturn(true); + } + + @AfterEach + void tearDown() { + helper.cleanUp(); + } + + // ------------------------------------------------------------------ helpers + + private Janino buildSpy(JaninoMeta meta) { + return Mockito.spy( + new Janino( + helper.transformMeta, meta, new JaninoData(), 0, helper.pipelineMeta, helper.pipeline)); + } + + private static JaninoMetaFunction intFn(String name, String formula) { + JaninoMetaFunction fn = new JaninoMetaFunction(); + fn.setFieldName(name); + fn.setFormula(formula); + fn.setValueType(IValueMeta.TYPE_INTEGER); + fn.setValueLength(-1); + fn.setValuePrecision(-1); + return fn; + } + + private static JaninoMetaFunction strFn(String name, String formula) { + JaninoMetaFunction fn = new JaninoMetaFunction(); + fn.setFieldName(name); + fn.setFormula(formula); + fn.setValueType(IValueMeta.TYPE_STRING); + fn.setValueLength(-1); + fn.setValuePrecision(-1); + return fn; + } + + /** Wire a single output IRowSet and return it (for result capture). */ + private static IRowSet attachOutputRowSet(Janino janino) { + IRowSet output = mock(IRowSet.class, Mockito.RETURNS_MOCKS); + when(output.putRow(any(IRowMeta.class), any(Object[].class))).thenReturn(true); + when(output.isDone()).thenReturn(false); + when(output.getRowWait(any(Long.class), any(TimeUnit.class))).thenReturn(null); + janino.addRowSetToOutputRowSets(output); + return output; + } + + // ------------------------------------------------------------------ processRow: no input + + @Test + void processRow_noInput_returnsFalse() throws HopException { + JaninoMeta meta = new JaninoMeta(); + Janino janino = buildSpy(meta); + doReturn(null).when(janino).getRow(); + + assertFalse(janino.processRow()); + } + + // ------------------------------------------------------------------ processRow: arithmetic + + @Test + void processRow_integerArithmetic_appendsLongResult() throws HopException { + JaninoMeta meta = new JaninoMeta(); + meta.getFunctions().add(intFn("result", "1 + 2")); + + RowMeta inputMeta = new RowMeta(); + inputMeta.addValueMeta(new ValueMetaString("dummy")); + + Janino janino = buildSpy(meta); + doReturn(new Object[] {"x"}).doReturn(null).when(janino).getRow(); + doReturn(inputMeta).when(janino).getInputRowMeta(); + + IRowSet output = attachOutputRowSet(janino); + janino.init(); + assertTrue(janino.processRow()); + janino.processRow(); // drain (null row) + + ArgumentCaptor rowCaptor = ArgumentCaptor.forClass(Object[].class); + verify(output).putRow(any(IRowMeta.class), rowCaptor.capture()); + Object[] row = rowCaptor.getValue(); + assertNotNull(row); + assertEquals("x", row[0]); + assertEquals(3L, row[1]); // Integer 3 promoted to Long + } + + // ------------------------------------------------------------------ processRow: String result + + @Test + void processRow_stringResult_appendsString() throws HopException { + JaninoMeta meta = new JaninoMeta(); + meta.getFunctions().add(strFn("greeting", "\"hello\"")); + + RowMeta inputMeta = new RowMeta(); + inputMeta.addValueMeta(new ValueMetaInteger("id")); + + Janino janino = buildSpy(meta); + doReturn(new Object[] {1L}).doReturn(null).when(janino).getRow(); + doReturn(inputMeta).when(janino).getInputRowMeta(); + + IRowSet output = attachOutputRowSet(janino); + janino.init(); + assertTrue(janino.processRow()); + + ArgumentCaptor rowCaptor = ArgumentCaptor.forClass(Object[].class); + verify(output).putRow(any(IRowMeta.class), rowCaptor.capture()); + assertEquals("hello", rowCaptor.getValue()[1]); + } + + // ------------------------------------------------------------------ processRow: input field + // reference + + @Test + void processRow_referencesInputField_computesCorrectly() throws HopException { + JaninoMeta meta = new JaninoMeta(); + meta.getFunctions().add(intFn("doubled", "n * 2")); + + RowMeta inputMeta = new RowMeta(); + inputMeta.addValueMeta(new ValueMetaInteger("n")); + + Janino janino = buildSpy(meta); + doReturn(new Object[] {5L}).doReturn(null).when(janino).getRow(); + doReturn(inputMeta).when(janino).getInputRowMeta(); + + IRowSet output = attachOutputRowSet(janino); + janino.init(); + assertTrue(janino.processRow()); + + ArgumentCaptor rowCaptor = ArgumentCaptor.forClass(Object[].class); + verify(output).putRow(any(IRowMeta.class), rowCaptor.capture()); + assertEquals(10L, rowCaptor.getValue()[1]); // 5 * 2 = 10 + } + + // ------------------------------------------------------------------ processRow: null formula + // result + + @Test + void processRow_nullResult_appendsNull() throws HopException { + JaninoMeta meta = new JaninoMeta(); + JaninoMetaFunction fn = intFn("x", "null"); + fn.setValueType(IValueMeta.TYPE_STRING); + meta.getFunctions().add(fn); + + RowMeta inputMeta = new RowMeta(); + inputMeta.addValueMeta(new ValueMetaInteger("id")); + + Janino janino = buildSpy(meta); + doReturn(new Object[] {1L}).doReturn(null).when(janino).getRow(); + doReturn(inputMeta).when(janino).getInputRowMeta(); + + IRowSet output = attachOutputRowSet(janino); + janino.init(); + assertTrue(janino.processRow()); + + ArgumentCaptor rowCaptor = ArgumentCaptor.forClass(Object[].class); + verify(output).putRow(any(IRowMeta.class), rowCaptor.capture()); + // null result at index 1 + assertFalse(rowCaptor.getValue().length == 0); + } + + // ------------------------------------------------------------------ target version ≥ 8: static + // interface method + + @Test + void processRow_target8_staticInterfaceMethod_succeeds() throws HopException { + JaninoMeta meta = new JaninoMeta(); + meta.setJavaTargetVersion(8); + // Comparator.naturalOrder() calls a static interface method (Java 8+) + meta.getFunctions() + .add( + intFn( + "cmp", + "java.util.Comparator.naturalOrder().compare(Integer.valueOf(5), Integer.valueOf(3))")); + + RowMeta inputMeta = new RowMeta(); + inputMeta.addValueMeta(new ValueMetaInteger("id")); + + Janino janino = buildSpy(meta); + doReturn(new Object[] {1L}).doReturn(null).when(janino).getRow(); + doReturn(inputMeta).when(janino).getInputRowMeta(); + + IRowSet output = attachOutputRowSet(janino); + janino.init(); + assertTrue(janino.processRow()); + + ArgumentCaptor rowCaptor = ArgumentCaptor.forClass(Object[].class); + verify(output).putRow(any(IRowMeta.class), rowCaptor.capture()); + // naturalOrder().compare(5, 3) > 0 + assertTrue(((Long) rowCaptor.getValue()[1]) > 0); + } + + // ------------------------------------------------------------------ target version 6: static + // interface method fails + + @Test + void processRow_target6_staticInterfaceMethod_throwsHopException() throws HopException { + JaninoMeta meta = new JaninoMeta(); + meta.setJavaTargetVersion(6); // below 8 — static interface calls blocked by Janino + meta.getFunctions() + .add( + intFn( + "cmp", + "java.util.Comparator.naturalOrder().compare(Integer.valueOf(5), Integer.valueOf(3))")); + + RowMeta inputMeta = new RowMeta(); + inputMeta.addValueMeta(new ValueMetaInteger("id")); + + Janino janino = buildSpy(meta); + doReturn(new Object[] {1L}).when(janino).getRow(); + doReturn(inputMeta).when(janino).getInputRowMeta(); + attachOutputRowSet(janino); + janino.init(); + + assertThrows(HopException.class, janino::processRow); + } + + // ------------------------------------------------------------------ error-handling route + + @Test + void processRow_badFormula_withErrorHandling_sendsToErrorStream() throws HopException { + JaninoMeta meta = new JaninoMeta(); + // Empty field name triggers the "Unable to find field name" exception during cook + JaninoMetaFunction fn = new JaninoMetaFunction(); + fn.setFieldName(""); // blank — triggers HopException in calcFields + fn.setFormula("1"); + fn.setValueType(IValueMeta.TYPE_INTEGER); + fn.setValueLength(-1); + fn.setValuePrecision(-1); + meta.getFunctions().add(fn); + + RowMeta inputMeta = new RowMeta(); + inputMeta.addValueMeta(new ValueMetaInteger("id")); + + Janino janino = buildSpy(meta); + doReturn(new Object[] {1L}).when(janino).getRow(); + doReturn(inputMeta).when(janino).getInputRowMeta(); + + // Enable error handling so the transform routes errors instead of re-throwing. + // Set up the minimal TransformErrorMeta the BaseTransform.handlePutError path needs. + when(helper.transformMeta.isDoingErrorHandling()).thenReturn(true); + TransformErrorMeta errorMeta = mock(TransformErrorMeta.class); + when(helper.transformMeta.getTransformErrorMeta()).thenReturn(errorMeta); + when(errorMeta.getErrorRowMeta(any())).thenReturn(new RowMeta()); + + attachOutputRowSet(janino); + janino.init(); + + // Should return true (row consumed by error routing) rather than throwing HopException + assertTrue(janino.processRow()); + } + + // ------------------------------------------------------------------ multiple rows + + @Test + void processRow_multipleRows_computesEachCorrectly() throws HopException { + JaninoMeta meta = new JaninoMeta(); + meta.getFunctions().add(intFn("sq", "n * n")); + + RowMeta inputMeta = new RowMeta(); + inputMeta.addValueMeta(new ValueMetaInteger("n")); + + Janino janino = buildSpy(meta); + doReturn(new Object[] {2L}).doReturn(new Object[] {3L}).doReturn(null).when(janino).getRow(); + doReturn(inputMeta).when(janino).getInputRowMeta(); + + IRowSet output = attachOutputRowSet(janino); + janino.init(); + + assertTrue(janino.processRow()); // row 1 + assertTrue(janino.processRow()); // row 2 + assertFalse(janino.processRow()); // end + + ArgumentCaptor rowCaptor = ArgumentCaptor.forClass(Object[].class); + verify(output, Mockito.times(2)).putRow(any(IRowMeta.class), rowCaptor.capture()); + + assertEquals(4L, rowCaptor.getAllValues().get(0)[1]); // 2*2 + assertEquals(9L, rowCaptor.getAllValues().get(1)[1]); // 3*3 + } +} diff --git a/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/javafilter/JavaFilterMetaTest.java b/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/javafilter/JavaFilterMetaTest.java new file mode 100644 index 00000000000..2262c835bef --- /dev/null +++ b/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/javafilter/JavaFilterMetaTest.java @@ -0,0 +1,494 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hop.pipeline.transforms.javafilter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; +import org.apache.hop.core.ICheckResult; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.row.RowMeta; +import org.apache.hop.core.row.value.ValueMetaInteger; +import org.apache.hop.core.row.value.ValueMetaString; +import org.apache.hop.core.xml.XmlHandler; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider; +import org.apache.hop.metadata.serializer.xml.XmlMetadataUtil; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.pipeline.transform.stream.IStream; +import org.junit.jupiter.api.Test; + +class JavaFilterMetaTest { + + // ------------------------------------------------------------------ serialization + + @Test + void testDeserialize() throws Exception { + // Deserialize directly (without round-trip getXml(), which would call + // convertIOMetaToTransformNames() and reset the stream names to ""). + var document = + XmlHandler.loadXmlFile(JavaFilterMetaTest.class.getResourceAsStream("/java-filter.xml")); + var node = XmlHandler.getSubNode(document, TransformMeta.XML_TAG); + JavaFilterMeta meta = + XmlMetadataUtil.deSerializeFromXml( + node, JavaFilterMeta.class, new MemoryMetadataProvider()); + + assertEquals("amount > 0", meta.getCondition()); + assertEquals("positiveRows", meta.getTrueTransform()); + assertEquals("negativeRows", meta.getFalseTransform()); + } + + @Test + void testGetXml_conditionRoundTrips() throws Exception { + // getXml() calls convertIOMetaToTransformNames() which resets stream names in tests + // (streams have no linked TransformMeta) — just verify condition survives the round-trip. + JavaFilterMeta meta = new JavaFilterMeta(); + meta.setCondition("x > 0"); + String xml = + XmlHandler.openTag(TransformMeta.XML_TAG) + + meta.getXml() + + XmlHandler.closeTag(TransformMeta.XML_TAG); + + var document = XmlHandler.loadXmlString(xml); + var node = XmlHandler.getSubNode(document, TransformMeta.XML_TAG); + JavaFilterMeta copy = + XmlMetadataUtil.deSerializeFromXml( + node, JavaFilterMeta.class, new MemoryMetadataProvider()); + assertEquals("x > 0", copy.getCondition()); + } + + // ------------------------------------------------------------------ getters / setters + + @Test + void setGetCondition() { + JavaFilterMeta meta = new JavaFilterMeta(); + assertNull(meta.getCondition()); + meta.setCondition("x > 0"); + assertEquals("x > 0", meta.getCondition()); + } + + @Test + void setGetTrueTransform() { + JavaFilterMeta meta = new JavaFilterMeta(); + assertNull(meta.getTrueTransform()); + meta.setTrueTransform("yes"); + assertEquals("yes", meta.getTrueTransform()); + } + + @Test + void setGetFalseTransform() { + JavaFilterMeta meta = new JavaFilterMeta(); + assertNull(meta.getFalseTransform()); + meta.setFalseTransform("no"); + assertEquals("no", meta.getFalseTransform()); + } + + // ------------------------------------------------------------------ setDefault + + @Test + void setDefault_setsConditionToTrue() { + JavaFilterMeta meta = new JavaFilterMeta(); + meta.setDefault(); + assertEquals("true", meta.getCondition()); + } + + // ------------------------------------------------------------------ clone + + @Test + void clone_returnsDifferentInstance() { + JavaFilterMeta meta = new JavaFilterMeta(); + meta.setCondition("n > 0"); + meta.setTrueTransform("yes"); + meta.setFalseTransform("no"); + + JavaFilterMeta cloned = (JavaFilterMeta) meta.clone(); + assertNotSame(meta, cloned); + assertEquals("n > 0", cloned.getCondition()); + } + + // ------------------------------------------------------------------ hashCode + + @Test + void hashCode_sameCondition_sameHashCode() { + JavaFilterMeta a = new JavaFilterMeta(); + a.setCondition("x > 0"); + JavaFilterMeta b = new JavaFilterMeta(); + b.setCondition("x > 0"); + assertEquals(a.hashCode(), b.hashCode()); + } + + // ------------------------------------------------------------------ + // convertIOMetaToTransformNames + + @Test + void convertIOMetaToTransformNames_nullStreams_setsEmptyStrings() { + JavaFilterMeta meta = new JavaFilterMeta(); + // streams start with null TransformMeta → getTransformName() returns null → NVL → "" + meta.convertIOMetaToTransformNames(); + assertEquals("", meta.getTrueTransform()); + assertEquals("", meta.getFalseTransform()); + } + + @Test + void convertIOMetaToTransformNames_withNames_copiesNames() { + JavaFilterMeta meta = new JavaFilterMeta(); + TransformMeta trueMeta = mock(TransformMeta.class); + when(trueMeta.getName()).thenReturn("trueTarget"); + TransformMeta falseMeta = mock(TransformMeta.class); + when(falseMeta.getName()).thenReturn("falseTarget"); + + List streams = meta.getTransformIOMeta().getTargetStreams(); + streams.get(0).setTransformMeta(trueMeta); + streams.get(1).setTransformMeta(falseMeta); + + meta.convertIOMetaToTransformNames(); + assertEquals("trueTarget", meta.getTrueTransform()); + assertEquals("falseTarget", meta.getFalseTransform()); + } + + // ------------------------------------------------------------------ + // searchInfoAndTargetTransforms + + @Test + void searchInfoAndTargetTransforms_findsMatchingTransforms() { + JavaFilterMeta meta = new JavaFilterMeta(); + meta.setTrueTransform("yes"); + meta.setFalseTransform("no"); + + TransformMeta yesMeta = mock(TransformMeta.class); + when(yesMeta.getName()).thenReturn("yes"); + TransformMeta noMeta = mock(TransformMeta.class); + when(noMeta.getName()).thenReturn("no"); + + meta.searchInfoAndTargetTransforms(List.of(yesMeta, noMeta)); + + List streams = meta.getTransformIOMeta().getTargetStreams(); + assertEquals(yesMeta, streams.get(0).getTransformMeta()); + assertEquals(noMeta, streams.get(1).getTransformMeta()); + } + + @Test + void searchInfoAndTargetTransforms_noMatch_setsNull() { + JavaFilterMeta meta = new JavaFilterMeta(); + meta.setTrueTransform("nonexistent"); + meta.setFalseTransform("alsoMissing"); + + TransformMeta other = mock(TransformMeta.class); + when(other.getName()).thenReturn("other"); + + meta.searchInfoAndTargetTransforms(List.of(other)); + + List streams = meta.getTransformIOMeta().getTargetStreams(); + assertNull(streams.get(0).getTransformMeta()); + assertNull(streams.get(1).getTransformMeta()); + } + + // ------------------------------------------------------------------ getTransformIOMeta + + @Test + void getTransformIOMeta_lazyInit_returnsSameInstance() { + JavaFilterMeta meta = new JavaFilterMeta(); + var io1 = meta.getTransformIOMeta(); + var io2 = meta.getTransformIOMeta(); + assertNotNull(io1); + assertEquals(2, io1.getTargetStreams().size()); + } + + // ------------------------------------------------------------------ resetTransformIoMeta (no-op) + + @Test + void resetTransformIoMeta_doesNotClearExistingIo() { + JavaFilterMeta meta = new JavaFilterMeta(); + var io = meta.getTransformIOMeta(); + meta.resetTransformIoMeta(); + assertEquals(io, meta.getTransformIOMeta()); + } + + // ------------------------------------------------------------------ + // excludeFromCopyDistributeVerification + + @Test + void excludeFromCopyDistributeVerification_returnsTrue() { + assertTrue(new JavaFilterMeta().excludeFromCopyDistributeVerification()); + } + + // ------------------------------------------------------------------ check: target stream + // combinations + + @Test + void check_bothTargetsSpecified_addsOk() { + JavaFilterMeta meta = new JavaFilterMeta(); + meta.setCondition("x > 0"); + + TransformMeta trueMeta = mock(TransformMeta.class); + when(trueMeta.getName()).thenReturn("yes"); + TransformMeta falseMeta = mock(TransformMeta.class); + when(falseMeta.getName()).thenReturn("no"); + meta.getTransformIOMeta().getTargetStreams().get(0).setTransformMeta(trueMeta); + meta.getTransformIOMeta().getTargetStreams().get(1).setTransformMeta(falseMeta); + + RowMeta prev = new RowMeta(); + prev.addValueMeta(new ValueMetaInteger("x")); + + List remarks = new ArrayList<>(); + meta.check( + remarks, + mock(PipelineMeta.class), + mock(TransformMeta.class), + prev, + new String[] {"in"}, + new String[] {"yes", "no"}, + mock(IRowMeta.class), + null, + mock(IHopMetadataProvider.class)); + + long okCount = remarks.stream().filter(r -> r.getType() == ICheckResult.TYPE_RESULT_OK).count(); + assertTrue(okCount >= 1); + } + + @Test + void check_neitherTargetSpecified_addsOk() { + JavaFilterMeta meta = new JavaFilterMeta(); + meta.setCondition("true"); + // both streams have null TransformMeta → getTransformName() returns null + + RowMeta prev = new RowMeta(); + prev.addValueMeta(new ValueMetaString("x")); + + List remarks = new ArrayList<>(); + meta.check( + remarks, + mock(PipelineMeta.class), + mock(TransformMeta.class), + prev, + new String[] {"in"}, + new String[0], + mock(IRowMeta.class), + null, + mock(IHopMetadataProvider.class)); + + assertTrue(remarks.stream().anyMatch(r -> r.getType() == ICheckResult.TYPE_RESULT_OK)); + } + + @Test + void check_onlyOneTrueTargetSpecified_addsOk() { + JavaFilterMeta meta = new JavaFilterMeta(); + meta.setCondition("true"); + + TransformMeta trueMeta = mock(TransformMeta.class); + when(trueMeta.getName()).thenReturn("yes"); + meta.getTransformIOMeta().getTargetStreams().get(0).setTransformMeta(trueMeta); + // false stream stays null + + List remarks = new ArrayList<>(); + meta.check( + remarks, + mock(PipelineMeta.class), + mock(TransformMeta.class), + new RowMeta(), + new String[] {"in"}, + new String[] {"yes"}, + mock(IRowMeta.class), + null, + mock(IHopMetadataProvider.class)); + + // The "else" branch adds an OK result even when only one target is set + assertTrue(remarks.stream().anyMatch(r -> r.getType() == ICheckResult.TYPE_RESULT_OK)); + } + + @Test + void check_trueTargetNotInOutput_addsError() { + JavaFilterMeta meta = new JavaFilterMeta(); + meta.setCondition("true"); + + TransformMeta trueMeta = mock(TransformMeta.class); + when(trueMeta.getName()).thenReturn("missingTarget"); + meta.getTransformIOMeta().getTargetStreams().get(0).setTransformMeta(trueMeta); + + List remarks = new ArrayList<>(); + meta.check( + remarks, + mock(PipelineMeta.class), + mock(TransformMeta.class), + new RowMeta(), + new String[] {"in"}, + new String[] {"otherTarget"}, + mock(IRowMeta.class), + null, + mock(IHopMetadataProvider.class)); + + assertTrue(remarks.stream().anyMatch(r -> r.getType() == ICheckResult.TYPE_RESULT_ERROR)); + } + + @Test + void check_falseTargetNotInOutput_addsError() { + JavaFilterMeta meta = new JavaFilterMeta(); + meta.setCondition("true"); + + TransformMeta falseMeta = mock(TransformMeta.class); + when(falseMeta.getName()).thenReturn("missingFalse"); + meta.getTransformIOMeta().getTargetStreams().get(1).setTransformMeta(falseMeta); + + List remarks = new ArrayList<>(); + meta.check( + remarks, + mock(PipelineMeta.class), + mock(TransformMeta.class), + new RowMeta(), + new String[] {"in"}, + new String[] {"otherTarget"}, + mock(IRowMeta.class), + null, + mock(IHopMetadataProvider.class)); + + assertTrue(remarks.stream().anyMatch(r -> r.getType() == ICheckResult.TYPE_RESULT_ERROR)); + } + + @Test + void check_emptyCondition_addsError() { + JavaFilterMeta meta = new JavaFilterMeta(); + meta.setCondition(""); + + List remarks = new ArrayList<>(); + meta.check( + remarks, + mock(PipelineMeta.class), + mock(TransformMeta.class), + new RowMeta(), + new String[] {"in"}, + new String[0], + mock(IRowMeta.class), + null, + mock(IHopMetadataProvider.class)); + + assertTrue(remarks.stream().anyMatch(r -> r.getType() == ICheckResult.TYPE_RESULT_ERROR)); + } + + @Test + void check_nullCondition_addsError() { + JavaFilterMeta meta = new JavaFilterMeta(); + // condition stays null + + List remarks = new ArrayList<>(); + meta.check( + remarks, + mock(PipelineMeta.class), + mock(TransformMeta.class), + new RowMeta(), + new String[] {"in"}, + new String[0], + mock(IRowMeta.class), + null, + mock(IHopMetadataProvider.class)); + + assertTrue(remarks.stream().anyMatch(r -> r.getType() == ICheckResult.TYPE_RESULT_ERROR)); + } + + @Test + void check_prevFieldsEmpty_addsError() { + JavaFilterMeta meta = new JavaFilterMeta(); + meta.setCondition("true"); + + List remarks = new ArrayList<>(); + meta.check( + remarks, + mock(PipelineMeta.class), + mock(TransformMeta.class), + new RowMeta(), + new String[] {"in"}, + new String[0], + mock(IRowMeta.class), + null, + mock(IHopMetadataProvider.class)); + + assertTrue(remarks.stream().anyMatch(r -> r.getType() == ICheckResult.TYPE_RESULT_ERROR)); + } + + @Test + void check_prevFieldsNull_addsError() { + JavaFilterMeta meta = new JavaFilterMeta(); + meta.setCondition("true"); + + List remarks = new ArrayList<>(); + meta.check( + remarks, + mock(PipelineMeta.class), + mock(TransformMeta.class), + null, + new String[] {"in"}, + new String[0], + mock(IRowMeta.class), + null, + mock(IHopMetadataProvider.class)); + + assertTrue(remarks.stream().anyMatch(r -> r.getType() == ICheckResult.TYPE_RESULT_ERROR)); + } + + @Test + void check_noInputTransforms_addsError() { + JavaFilterMeta meta = new JavaFilterMeta(); + meta.setCondition("true"); + + RowMeta prev = new RowMeta(); + prev.addValueMeta(new ValueMetaString("x")); + + List remarks = new ArrayList<>(); + meta.check( + remarks, + mock(PipelineMeta.class), + mock(TransformMeta.class), + prev, + new String[0], + new String[0], + mock(IRowMeta.class), + null, + mock(IHopMetadataProvider.class)); + + assertTrue(remarks.stream().anyMatch(r -> r.getType() == ICheckResult.TYPE_RESULT_ERROR)); + } + + @Test + void check_withInputTransforms_addsOk() { + JavaFilterMeta meta = new JavaFilterMeta(); + meta.setCondition("true"); + + RowMeta prev = new RowMeta(); + prev.addValueMeta(new ValueMetaString("x")); + + List remarks = new ArrayList<>(); + meta.check( + remarks, + mock(PipelineMeta.class), + mock(TransformMeta.class), + prev, + new String[] {"upstream"}, + new String[0], + mock(IRowMeta.class), + null, + mock(IHopMetadataProvider.class)); + + assertTrue(remarks.stream().anyMatch(r -> r.getType() == ICheckResult.TYPE_RESULT_OK)); + } +} diff --git a/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/javafilter/JavaFilterTest.java b/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/javafilter/JavaFilterTest.java new file mode 100644 index 00000000000..aa84e8aadb3 --- /dev/null +++ b/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/javafilter/JavaFilterTest.java @@ -0,0 +1,257 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hop.pipeline.transforms.javafilter; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.concurrent.TimeUnit; +import org.apache.hop.core.IRowSet; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.logging.HopLogStore; +import org.apache.hop.core.logging.ILoggingObject; +import org.apache.hop.core.plugins.PluginRegistry; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.row.RowMeta; +import org.apache.hop.core.row.value.ValueMetaInteger; +import org.apache.hop.core.row.value.ValueMetaPlugin; +import org.apache.hop.core.row.value.ValueMetaPluginType; +import org.apache.hop.core.row.value.ValueMetaString; +import org.apache.hop.pipeline.transforms.mock.TransformMockHelper; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +/** + * Unit tests for {@link JavaFilter#processRow()} covering filter-true, filter-false, field + * references, non-boolean results and compile failures. + */ +class JavaFilterTest { + + private TransformMockHelper helper; + + @BeforeAll + static void initPlugins() throws Exception { + HopLogStore.init(); + PluginRegistry registry = PluginRegistry.getInstance(); + String[] valueMetaClasses = { + org.apache.hop.core.row.value.ValueMetaString.class.getName(), + org.apache.hop.core.row.value.ValueMetaInteger.class.getName(), + org.apache.hop.core.row.value.ValueMetaDate.class.getName(), + org.apache.hop.core.row.value.ValueMetaNumber.class.getName() + }; + for (String cls : valueMetaClasses) { + registry.registerPluginClass(cls, ValueMetaPluginType.class, ValueMetaPlugin.class); + } + } + + @BeforeEach + void setUp() { + helper = + new TransformMockHelper<>("JavaFilter TEST", JavaFilterMeta.class, JavaFilterData.class); + when(helper.logChannelFactory.create(any(), any(ILoggingObject.class))) + .thenReturn(helper.iLogChannel); + when(helper.pipeline.isRunning()).thenReturn(true); + } + + @AfterEach + void tearDown() { + helper.cleanUp(); + } + + // ------------------------------------------------------------------ helpers + + private JavaFilter buildSpy(JavaFilterMeta meta) { + return Mockito.spy( + new JavaFilter( + helper.transformMeta, + meta, + new JavaFilterData(), + 0, + helper.pipelineMeta, + helper.pipeline)); + } + + private static IRowSet attachOutputRowSet(JavaFilter jf) { + IRowSet output = mock(IRowSet.class, Mockito.RETURNS_MOCKS); + when(output.putRow(any(IRowMeta.class), any(Object[].class))).thenReturn(true); + when(output.isDone()).thenReturn(false); + when(output.getRowWait(any(Long.class), any(TimeUnit.class))).thenReturn(null); + jf.addRowSetToOutputRowSets(output); + return output; + } + + // ------------------------------------------------------------------ no input + + @Test + void processRow_noInput_returnsFalse() throws HopException { + JavaFilterMeta meta = new JavaFilterMeta(); + JavaFilter jf = buildSpy(meta); + doReturn(null).when(jf).getRow(); + + assertFalse(jf.processRow()); + } + + // ------------------------------------------------------------------ constant true / false + + @Test + void processRow_conditionTrue_rowPassesThrough() throws HopException { + JavaFilterMeta meta = new JavaFilterMeta(); + meta.setCondition("true"); + + RowMeta inputMeta = new RowMeta(); + inputMeta.addValueMeta(new ValueMetaString("name")); + + JavaFilter jf = buildSpy(meta); + doReturn(new Object[] {"alice"}).doReturn(null).when(jf).getRow(); + doReturn(inputMeta).when(jf).getInputRowMeta(); + + IRowSet output = attachOutputRowSet(jf); + jf.init(); + assertTrue(jf.processRow()); + jf.processRow(); // drain null + + ArgumentCaptor captor = ArgumentCaptor.forClass(Object[].class); + verify(output).putRow(any(IRowMeta.class), captor.capture()); + org.junit.jupiter.api.Assertions.assertArrayEquals(new Object[] {"alice"}, captor.getValue()); + } + + @Test + void processRow_conditionFalse_rowDropped() throws HopException { + JavaFilterMeta meta = new JavaFilterMeta(); + meta.setCondition("false"); + + RowMeta inputMeta = new RowMeta(); + inputMeta.addValueMeta(new ValueMetaString("name")); + + JavaFilter jf = buildSpy(meta); + doReturn(new Object[] {"alice"}).doReturn(null).when(jf).getRow(); + doReturn(inputMeta).when(jf).getInputRowMeta(); + + IRowSet output = attachOutputRowSet(jf); + jf.init(); + assertTrue(jf.processRow()); + jf.processRow(); + + // No row should reach the output + verify(output, Mockito.never()).putRow(any(IRowMeta.class), any(Object[].class)); + } + + // ------------------------------------------------------------------ input-field reference + + @Test + void processRow_conditionUsesInputField_filtersCorrectly() throws HopException { + JavaFilterMeta meta = new JavaFilterMeta(); + meta.setCondition("amount > 0"); + + RowMeta inputMeta = new RowMeta(); + inputMeta.addValueMeta(new ValueMetaInteger("amount")); + + JavaFilter jf = buildSpy(meta); + // Row with amount = 5 → passes; amount = -3 → dropped + doReturn(new Object[] {5L}).doReturn(new Object[] {-3L}).doReturn(null).when(jf).getRow(); + doReturn(inputMeta).when(jf).getInputRowMeta(); + + IRowSet output = attachOutputRowSet(jf); + jf.init(); + jf.processRow(); // amount=5 → true, sent to output + jf.processRow(); // amount=-3 → false, dropped + jf.processRow(); // null → done + + verify(output, Mockito.times(1)).putRow(any(IRowMeta.class), any(Object[].class)); + } + + // ------------------------------------------------------------------ non-boolean result throws + + @Test + void processRow_nonBooleanResult_throwsHopException() throws HopException { + JavaFilterMeta meta = new JavaFilterMeta(); + meta.setCondition("\"not a boolean\""); + + RowMeta inputMeta = new RowMeta(); + inputMeta.addValueMeta(new ValueMetaInteger("id")); + + JavaFilter jf = buildSpy(meta); + doReturn(new Object[] {1L}).when(jf).getRow(); + doReturn(inputMeta).when(jf).getInputRowMeta(); + + attachOutputRowSet(jf); + jf.init(); + + assertThrows(HopException.class, jf::processRow); + } + + // ------------------------------------------------------------------ compile error throws + + @Test + void processRow_invalidConditionSyntax_throwsHopException() throws HopException { + JavaFilterMeta meta = new JavaFilterMeta(); + meta.setCondition("this is not valid java at all !!!"); + + RowMeta inputMeta = new RowMeta(); + inputMeta.addValueMeta(new ValueMetaInteger("id")); + + JavaFilter jf = buildSpy(meta); + doReturn(new Object[] {1L}).when(jf).getRow(); + doReturn(inputMeta).when(jf).getInputRowMeta(); + + attachOutputRowSet(jf); + jf.init(); + + assertThrows(HopException.class, jf::processRow); + } + + // ------------------------------------------------------------------ multiple rows + + @Test + void processRow_multipleRows_returnsCorrectly() throws HopException { + JavaFilterMeta meta = new JavaFilterMeta(); + meta.setCondition("n >= 0"); + + RowMeta inputMeta = new RowMeta(); + inputMeta.addValueMeta(new ValueMetaInteger("n")); + + JavaFilter jf = buildSpy(meta); + doReturn(new Object[] {1L}) + .doReturn(new Object[] {-1L}) + .doReturn(new Object[] {0L}) + .doReturn(null) + .when(jf) + .getRow(); + doReturn(inputMeta).when(jf).getInputRowMeta(); + + IRowSet output = attachOutputRowSet(jf); + jf.init(); + + assertTrue(jf.processRow()); // n=1 → true + assertTrue(jf.processRow()); // n=-1 → false (dropped) + assertTrue(jf.processRow()); // n=0 → true + assertFalse(jf.processRow()); // null → done + + // Only 2 rows should pass (n=1 and n=0) + verify(output, Mockito.times(2)).putRow(any(IRowMeta.class), any(Object[].class)); + } +} diff --git a/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/FieldHelperTest.java b/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/FieldHelperTest.java index ef58fca2d0e..9271c001283 100644 --- a/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/FieldHelperTest.java +++ b/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/FieldHelperTest.java @@ -19,17 +19,22 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import java.math.BigDecimal; import java.net.InetAddress; import java.sql.Timestamp; +import java.util.Date; import org.apache.hop.core.exception.HopValueException; import org.apache.hop.core.logging.LogChannel; import org.apache.hop.core.row.IRowMeta; import org.apache.hop.core.row.IValueMeta; +import org.apache.hop.core.row.RowMeta; import org.apache.hop.core.row.value.ValueMetaBigNumber; import org.apache.hop.core.row.value.ValueMetaBinary; import org.apache.hop.core.row.value.ValueMetaBoolean; @@ -286,4 +291,159 @@ void setValue_ValueMetaBinary() throws Exception { assertArrayEquals(new byte[] {0, 1, 2}, (byte[]) data[0]); } + + // ------------------------------------------------------------------ constructor failure + + @Test + void constructor_fieldNotFound_throwsIllegalArgumentException() { + RowMeta rowMeta = new RowMeta(); + rowMeta.addValueMeta(new ValueMetaString("existing")); + assertThrows(IllegalArgumentException.class, () -> new FieldHelper(rowMeta, "missing")); + } + + // ------------------------------------------------------------------ typed getters + + @Test + void getObject_returnsRawValue() { + ValueMetaString v = new ValueMetaString("name"); + IRowMeta row = mock(IRowMeta.class); + doReturn(v).when(row).searchValueMeta(anyString()); + doReturn(0).when(row).indexOfValue(anyString()); + + Object[] data = {"hello"}; + assertEquals("hello", new FieldHelper(row, "name").getObject(data)); + } + + @Test + void getBoolean_returnsValue() throws HopValueException { + ValueMetaBoolean v = new ValueMetaBoolean("flag"); + IRowMeta row = mock(IRowMeta.class); + doReturn(v).when(row).searchValueMeta(anyString()); + doReturn(0).when(row).indexOfValue(anyString()); + + assertEquals( + Boolean.TRUE, new FieldHelper(row, "flag").getBoolean(new Object[] {Boolean.TRUE})); + } + + @Test + void getLong_returnsValue() throws HopValueException { + ValueMetaInteger v = new ValueMetaInteger("num"); + IRowMeta row = mock(IRowMeta.class); + doReturn(v).when(row).searchValueMeta(anyString()); + doReturn(0).when(row).indexOfValue(anyString()); + + assertEquals(42L, new FieldHelper(row, "num").getLong(new Object[] {42L})); + } + + @Test + void getDouble_returnsValue() throws HopValueException { + ValueMetaNumber v = new ValueMetaNumber("dbl"); + IRowMeta row = mock(IRowMeta.class); + doReturn(v).when(row).searchValueMeta(anyString()); + doReturn(0).when(row).indexOfValue(anyString()); + + assertEquals(3.14, new FieldHelper(row, "dbl").getDouble(new Object[] {3.14})); + } + + @Test + void getString_returnsValue() throws HopValueException { + ValueMetaString v = new ValueMetaString("s"); + IRowMeta row = mock(IRowMeta.class); + doReturn(v).when(row).searchValueMeta(anyString()); + doReturn(0).when(row).indexOfValue(anyString()); + + assertEquals("world", new FieldHelper(row, "s").getString(new Object[] {"world"})); + } + + @Test + void getBigDecimal_returnsValue() throws HopValueException { + ValueMetaBigNumber v = new ValueMetaBigNumber("bd"); + IRowMeta row = mock(IRowMeta.class); + doReturn(v).when(row).searchValueMeta(anyString()); + doReturn(0).when(row).indexOfValue(anyString()); + + BigDecimal bd = new BigDecimal("123.456"); + assertEquals(bd, new FieldHelper(row, "bd").getBigDecimal(new Object[] {bd})); + } + + @Test + void getDate_returnsValue() throws HopValueException { + ValueMetaDate v = new ValueMetaDate("dt"); + IRowMeta row = mock(IRowMeta.class); + doReturn(v).when(row).searchValueMeta(anyString()); + doReturn(0).when(row).indexOfValue(anyString()); + + Date d = new Date(0L); + assertEquals(d, new FieldHelper(row, "dt").getDate(new Object[] {d})); + } + + // ------------------------------------------------------------------ getValueMeta / indexOfValue + + @Test + void getValueMeta_returnsStoredMeta() { + ValueMetaString v = new ValueMetaString("col"); + IRowMeta row = mock(IRowMeta.class); + doReturn(v).when(row).searchValueMeta(anyString()); + doReturn(0).when(row).indexOfValue(anyString()); + + assertNotNull(new FieldHelper(row, "col").getValueMeta()); + assertEquals(v, new FieldHelper(row, "col").getValueMeta()); + } + + @Test + void indexOfValue_returnsStoredIndex() { + ValueMetaString v = new ValueMetaString("col"); + IRowMeta row = mock(IRowMeta.class); + doReturn(v).when(row).searchValueMeta(anyString()); + doReturn(2).when(row).indexOfValue(anyString()); + + assertEquals(2, new FieldHelper(row, "col").indexOfValue()); + } + + // ------------------------------------------------------------------ getAccessor Out variant + + @Test + void getAccessor_outVariant_containsFieldsOut() { + String accessor = FieldHelper.getAccessor(false, "myField"); + assertEquals("get(Fields.Out, \"myField\")", accessor); + } + + // ------------------------------------------------------------------ getGetSignature: invalid + // Java identifier → "value" + + @Test + void getGetSignature_invalidJavaIdentifier_usesValueAsLocalName() { + ValueMetaString v = new ValueMetaString("123invalid"); + String accessor = FieldHelper.getAccessor(true, "123invalid"); + String sig = FieldHelper.getGetSignature(accessor, v); + // Local variable name should be "value" when name is not a valid Java identifier + assertEquals("String value = get(Fields.In, \"123invalid\").getString(r);", sig); + } + + // ------------------------------------------------------------------ getNativeDataTypeSimpleName: + // remaining types + + @Test + void getNativeDataTypeSimpleName_Boolean() { + ValueMetaBoolean v = new ValueMetaBoolean(); + assertEquals("Boolean", FieldHelper.getNativeDataTypeSimpleName(v)); + } + + @Test + void getNativeDataTypeSimpleName_Integer() { + ValueMetaInteger v = new ValueMetaInteger(); + assertEquals("Long", FieldHelper.getNativeDataTypeSimpleName(v)); + } + + @Test + void getNativeDataTypeSimpleName_Number() { + ValueMetaNumber v = new ValueMetaNumber(); + assertEquals("Double", FieldHelper.getNativeDataTypeSimpleName(v)); + } + + @Test + void getNativeDataTypeSimpleName_BigNumber() { + ValueMetaBigNumber v = new ValueMetaBigNumber(); + assertEquals("BigDecimal", FieldHelper.getNativeDataTypeSimpleName(v)); + } } diff --git a/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/TransformClassBaseStaticMethodsTest.java b/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/TransformClassBaseStaticMethodsTest.java new file mode 100644 index 00000000000..445aa5fcb48 --- /dev/null +++ b/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/TransformClassBaseStaticMethodsTest.java @@ -0,0 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hop.pipeline.transforms.userdefinedjavaclass; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.Collections; +import java.util.List; +import org.apache.hop.core.plugins.PluginRegistry; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.row.IValueMeta; +import org.apache.hop.core.row.RowMeta; +import org.apache.hop.core.row.value.ValueMetaPlugin; +import org.apache.hop.core.row.value.ValueMetaPluginType; +import org.apache.hop.core.row.value.ValueMetaString; +import org.apache.hop.pipeline.transform.ITransformIOMeta; +import org.apache.hop.pipeline.transform.stream.IStream; +import org.apache.hop.pipeline.transforms.userdefinedjavaclass.UserDefinedJavaClassMeta.FieldInfo; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Tests the static utility methods on {@link TransformClassBase} that do not require a running + * transform instance: {@code getInfoTransforms}, {@code getFields}, and {@code getTransformIOMeta}. + */ +class TransformClassBaseStaticMethodsTest { + + @BeforeAll + static void initPlugins() throws Exception { + PluginRegistry registry = PluginRegistry.getInstance(); + registry.registerPluginClass( + ValueMetaString.class.getName(), ValueMetaPluginType.class, ValueMetaPlugin.class); + } + + // ------------------------------------------------------------------ getInfoTransforms + + @Test + void getInfoTransforms_returnsNull() { + assertNull(TransformClassBase.getInfoTransforms()); + } + + // ------------------------------------------------------------------ getFields + + @Test + void getFields_appendMode_addsFieldsWithoutClearing() throws Exception { + RowMeta row = new RowMeta(); + row.addValueMeta(new ValueMetaString("existing")); + + FieldInfo fi = new FieldInfo("newCol", IValueMeta.TYPE_STRING, 50, 0); + + TransformClassBase.getFields(false, row, "origin", null, null, null, List.of(fi)); + + assertEquals(2, row.size()); + assertEquals("existing", row.getValueMeta(0).getName()); + assertEquals("newCol", row.getValueMeta(1).getName()); + } + + @Test + void getFields_clearMode_clearsBeforeAdding() throws Exception { + RowMeta row = new RowMeta(); + row.addValueMeta(new ValueMetaString("toBeCleared")); + + FieldInfo fi = new FieldInfo("fresh", IValueMeta.TYPE_STRING, 10, 0); + + TransformClassBase.getFields(true, row, "origin", null, null, null, List.of(fi)); + + assertEquals(1, row.size()); + assertEquals("fresh", row.getValueMeta(0).getName()); + } + + @Test + void getFields_emptyList_leavesRowUnchanged() throws Exception { + RowMeta row = new RowMeta(); + row.addValueMeta(new ValueMetaString("keep")); + + TransformClassBase.getFields(false, row, "origin", null, null, null, Collections.emptyList()); + + assertEquals(1, row.size()); + assertEquals("keep", row.getValueMeta(0).getName()); + } + + @Test + void getFields_clearWithEmptyList_producesEmptyRow() throws Exception { + RowMeta row = new RowMeta(); + row.addValueMeta(new ValueMetaString("gone")); + + TransformClassBase.getFields(true, row, "origin", null, null, null, Collections.emptyList()); + + assertEquals(0, row.size()); + } + + @Test + void getFields_setsOriginOnAddedField() throws Exception { + RowMeta row = new RowMeta(); + FieldInfo fi = new FieldInfo("col", IValueMeta.TYPE_STRING, 5, 0); + + TransformClassBase.getFields(false, row, "MyTransform", null, null, null, List.of(fi)); + + assertEquals("MyTransform", row.getValueMeta(0).getOrigin()); + } + + // ------------------------------------------------------------------ getTransformIOMeta + + @Test + void getTransformIOMeta_noInfoNoTarget_returnsEmptyStreams() { + UserDefinedJavaClassMeta meta = new UserDefinedJavaClassMeta(); + + ITransformIOMeta ioMeta = TransformClassBase.getTransformIOMeta(meta); + + assertNotNull(ioMeta); + long infoStreams = + ioMeta.getInfoStreams().stream() + .filter(s -> s.getStreamType() == IStream.StreamType.INFO) + .count(); + long targetStreams = + ioMeta.getTargetStreams().stream() + .filter(s -> s.getStreamType() == IStream.StreamType.TARGET) + .count(); + assertEquals(0, infoStreams); + assertEquals(0, targetStreams); + } + + @Test + void getTransformIOMeta_withInfoDefinition_includesInfoStream() { + UserDefinedJavaClassMeta meta = new UserDefinedJavaClassMeta(); + InfoTransformDefinition infoDef = new InfoTransformDefinition(); + infoDef.setTag("lookup"); + infoDef.setDescription("Lookup data"); + meta.getInfoTransformDefinitions().add(infoDef); + + ITransformIOMeta ioMeta = TransformClassBase.getTransformIOMeta(meta); + + IRowMeta infoStreams = null; // unused, just verify count + long count = + ioMeta.getInfoStreams().stream() + .filter(s -> s.getStreamType() == IStream.StreamType.INFO) + .count(); + assertEquals(1, count); + } + + @Test + void getTransformIOMeta_withTargetDefinition_includesTargetStream() { + UserDefinedJavaClassMeta meta = new UserDefinedJavaClassMeta(); + TargetTransformDefinition targetDef = new TargetTransformDefinition(); + targetDef.tag = "positive"; + targetDef.description = "Positive rows"; + meta.getTargetTransformDefinitions().add(targetDef); + + ITransformIOMeta ioMeta = TransformClassBase.getTransformIOMeta(meta); + + long count = + ioMeta.getTargetStreams().stream() + .filter(s -> s.getStreamType() == IStream.StreamType.TARGET) + .count(); + assertEquals(1, count); + } +} diff --git a/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/TransformDefinitionsTest.java b/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/TransformDefinitionsTest.java new file mode 100644 index 00000000000..6f7dbfea005 --- /dev/null +++ b/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/TransformDefinitionsTest.java @@ -0,0 +1,212 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hop.pipeline.transforms.userdefinedjavaclass; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; + +import org.apache.hop.pipeline.transform.TransformMeta; +import org.junit.jupiter.api.Test; + +/** + * Tests for the four lightweight definition/parameter value objects: {@link TransformDefinition}, + * {@link InfoTransformDefinition}, {@link TargetTransformDefinition}, and {@link UsageParameter}. + */ +class TransformDefinitionsTest { + + // ================================================================== TransformDefinition + + @Test + void transformDefinition_defaultConstructor_emptyStringsNullMeta() { + TransformDefinition td = new TransformDefinition(); + assertEquals("", td.tag); + assertEquals("", td.transformName); + assertNull(td.transformMeta); + assertEquals("", td.description); + } + + @Test + void transformDefinition_fullConstructor_setsAllFields() { + TransformMeta tm = mock(TransformMeta.class); + TransformDefinition td = new TransformDefinition("t", "name", tm, "desc"); + assertEquals("t", td.tag); + assertEquals("name", td.transformName); + assertEquals(tm, td.transformMeta); + assertEquals("desc", td.description); + } + + @Test + void transformDefinition_copyConstructor_nullTransformMeta() { + TransformDefinition original = new TransformDefinition("tag", "step", null, "d"); + TransformDefinition copy = new TransformDefinition(original); + assertNotSame(original, copy); + assertEquals(original.tag, copy.tag); + assertEquals(original.transformName, copy.transformName); + assertEquals(original.description, copy.description); + assertNull(copy.transformMeta); + } + + @Test + void transformDefinition_clone_createsDistinctInstance() { + TransformDefinition td = new TransformDefinition("a", "b", null, "c"); + TransformDefinition cloned = (TransformDefinition) td.clone(); + assertNotSame(td, cloned); + assertEquals(td.tag, cloned.tag); + assertEquals(td.transformName, cloned.transformName); + assertEquals(td.description, cloned.description); + } + + // ================================================================== InfoTransformDefinition + + @Test + void infoTransformDefinition_defaultConstructor_emptyStrings() { + InfoTransformDefinition itd = new InfoTransformDefinition(); + assertNotNull(itd); + assertNull(itd.transformMeta); + } + + @Test + void infoTransformDefinition_copyConstructor_noTransformMeta() { + InfoTransformDefinition original = new InfoTransformDefinition(); + original.setTag("info"); + original.setTransformName("src"); + original.setDescription("info desc"); + original.transformMeta = null; + + InfoTransformDefinition copy = new InfoTransformDefinition(original); + assertNotSame(original, copy); + assertEquals("info", copy.getTag()); + assertEquals("src", copy.getTransformName()); + assertEquals("info desc", copy.getDescription()); + assertNull(copy.transformMeta); + } + + @Test + void infoTransformDefinition_copyConstructor_withTransformMeta_clonesIt() { + InfoTransformDefinition original = new InfoTransformDefinition(); + original.setTag("x"); + TransformMeta tm = mock(TransformMeta.class); + original.transformMeta = tm; + + InfoTransformDefinition copy = new InfoTransformDefinition(original); + // clone() should have been called — the field must not be the same reference + assertNotSame(tm, copy.transformMeta); + } + + @Test + void infoTransformDefinition_clone_returnsDistinctInstance() { + InfoTransformDefinition itd = new InfoTransformDefinition(); + itd.setTag("y"); + InfoTransformDefinition cloned = (InfoTransformDefinition) itd.clone(); + assertNotSame(itd, cloned); + assertEquals("y", cloned.getTag()); + } + + // ================================================================== TargetTransformDefinition + + @Test + void targetTransformDefinition_defaultConstructor_emptyStrings() { + TargetTransformDefinition ttd = new TargetTransformDefinition(); + assertNotNull(ttd); + assertNull(ttd.transformMeta); + } + + @Test + void targetTransformDefinition_copyConstructor_noTransformMeta() { + TargetTransformDefinition original = new TargetTransformDefinition(); + original.tag = "tgt"; + original.transformName = "dest"; + original.description = "tgt desc"; + original.transformMeta = null; + + TargetTransformDefinition copy = new TargetTransformDefinition(original); + assertNotSame(original, copy); + assertEquals("tgt", copy.tag); + assertEquals("dest", copy.transformName); + assertEquals("tgt desc", copy.description); + assertNull(copy.transformMeta); + } + + @Test + void targetTransformDefinition_copyConstructor_withTransformMeta_clonesIt() { + TargetTransformDefinition original = new TargetTransformDefinition(); + original.tag = "z"; + TransformMeta tm = mock(TransformMeta.class); + original.transformMeta = tm; + + TargetTransformDefinition copy = new TargetTransformDefinition(original); + assertNotSame(tm, copy.transformMeta); + } + + @Test + void targetTransformDefinition_clone_returnsDistinctInstance() { + TargetTransformDefinition ttd = new TargetTransformDefinition(); + ttd.tag = "alpha"; + TargetTransformDefinition cloned = (TargetTransformDefinition) ttd.clone(); + assertNotSame(ttd, cloned); + assertEquals("alpha", cloned.tag); + } + + // ================================================================== UsageParameter + + @Test + void usageParameter_defaultConstructor_allNull() { + UsageParameter up = new UsageParameter(); + assertNull(up.getTag()); + assertNull(up.getValue()); + assertNull(up.getDescription()); + } + + @Test + void usageParameter_copyConstructor_copiesAllFields() { + UsageParameter original = new UsageParameter(); + original.setTag("param1"); + original.setValue("val1"); + original.setDescription("a parameter"); + + UsageParameter copy = new UsageParameter(original); + assertNotSame(original, copy); + assertEquals("param1", copy.getTag()); + assertEquals("val1", copy.getValue()); + assertEquals("a parameter", copy.getDescription()); + } + + @Test + void usageParameter_clone_returnsDistinctInstance() { + UsageParameter up = new UsageParameter(); + up.setTag("p"); + up.setValue("v"); + UsageParameter cloned = up.clone(); + assertNotSame(up, cloned); + assertEquals("p", cloned.getTag()); + assertEquals("v", cloned.getValue()); + } + + @Test + void usageParameter_setters_updateState() { + UsageParameter up = new UsageParameter(); + up.setTag("t"); + up.setValue("42"); + up.setDescription("desc"); + assertEquals("t", up.getTag()); + assertEquals("42", up.getValue()); + assertEquals("desc", up.getDescription()); + } +} diff --git a/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassCodeSnippetsTest.java b/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassCodeSnippetsTest.java new file mode 100644 index 00000000000..5a0aeb23497 --- /dev/null +++ b/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassCodeSnippetsTest.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hop.pipeline.transforms.userdefinedjavaclass; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.apache.hop.core.logging.HopLogStore; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class UserDefinedJavaClassCodeSnippetsTest { + + @BeforeAll + static void initLogStore() { + HopLogStore.init(); + } + + @Test + void getSnippetsHelper_returnsNonNull() throws Exception { + assertNotNull(UserDefinedJavaClassCodeSnippets.getSnippetsHelper()); + } + + @Test + void getSnippetsHelper_singleton_returnsSameInstance() throws Exception { + UserDefinedJavaClassCodeSnippets first = UserDefinedJavaClassCodeSnippets.getSnippetsHelper(); + UserDefinedJavaClassCodeSnippets second = UserDefinedJavaClassCodeSnippets.getSnippetsHelper(); + assertEquals(first, second); + } + + @Test + void getSnippets_isNotEmpty() throws Exception { + assertFalse(UserDefinedJavaClassCodeSnippets.getSnippetsHelper().getSnippets().isEmpty()); + } + + @Test + void getDefaultCode_isNotEmpty() throws Exception { + String code = UserDefinedJavaClassCodeSnippets.getSnippetsHelper().getDefaultCode(); + assertNotNull(code); + assertFalse(code.isEmpty()); + } + + @Test + void getCode_knownSnippet_returnsNonEmpty() throws Exception { + UserDefinedJavaClassCodeSnippets helper = UserDefinedJavaClassCodeSnippets.getSnippetsHelper(); + // "Implement processRow" is the canonical default snippet + String code = helper.getCode("Implement processRow"); + assertNotNull(code); + assertFalse(code.isEmpty(), "Expected code for 'Implement processRow' but got empty"); + } + + @Test + void getCode_unknownSnippet_returnsEmpty() throws Exception { + String code = UserDefinedJavaClassCodeSnippets.getSnippetsHelper().getCode("NONEXISTENT"); + assertEquals("", code); + } + + @Test + void getSample_unknownSnippet_returnsEmpty() throws Exception { + String sample = UserDefinedJavaClassCodeSnippets.getSnippetsHelper().getSample("NONEXISTENT"); + assertEquals("", sample); + } + + @Test + void getSnippets_isUnmodifiable() throws Exception { + assertThrows( + UnsupportedOperationException.class, + () -> UserDefinedJavaClassCodeSnippets.getSnippetsHelper().getSnippets().clear()); + } +} diff --git a/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassDefTest.java b/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassDefTest.java new file mode 100644 index 00000000000..25b3a26d0ce --- /dev/null +++ b/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassDefTest.java @@ -0,0 +1,153 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hop.pipeline.transforms.userdefinedjavaclass; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.hop.pipeline.transforms.userdefinedjavaclass.UserDefinedJavaClassDef.ClassType; +import org.junit.jupiter.api.Test; + +class UserDefinedJavaClassDefTest { + + private static UserDefinedJavaClassDef normal(String name, String src) { + return new UserDefinedJavaClassDef(ClassType.NORMAL_CLASS, name, src); + } + + private static UserDefinedJavaClassDef transform(String name, String src) { + return new UserDefinedJavaClassDef(ClassType.TRANSFORM_CLASS, name, src); + } + + // ------------------------------------------------------------------ constructors + + @Test + void defaultConstructor_fieldsAreNull() { + UserDefinedJavaClassDef def = new UserDefinedJavaClassDef(); + assertNull(def.getClassType()); + assertNull(def.getClassName()); + assertNull(def.getSource()); + } + + @Test + void fullConstructor_setsAllFields() { + UserDefinedJavaClassDef def = normal("Foo", "int x = 1;"); + assertEquals(ClassType.NORMAL_CLASS, def.getClassType()); + assertEquals("Foo", def.getClassName()); + assertEquals("int x = 1;", def.getSource()); + } + + @Test + void copyConstructor_copiesAllFields() { + UserDefinedJavaClassDef original = transform("Bar", "void run(){}"); + UserDefinedJavaClassDef copy = new UserDefinedJavaClassDef(original); + assertNotSame(original, copy); + assertEquals(original.getClassType(), copy.getClassType()); + assertEquals(original.getClassName(), copy.getClassName()); + assertEquals(original.getSource(), copy.getSource()); + } + + // ------------------------------------------------------------------ clone + + @Test + void clone_returnsDistinctEqualInstance() throws CloneNotSupportedException { + UserDefinedJavaClassDef def = normal("Cloneable", "body"); + UserDefinedJavaClassDef cloned = (UserDefinedJavaClassDef) def.clone(); + assertNotSame(def, cloned); + assertEquals(def.getClassType(), cloned.getClassType()); + assertEquals(def.getClassName(), cloned.getClassName()); + assertEquals(def.getSource(), cloned.getSource()); + } + + // ------------------------------------------------------------------ isTransformClass + + @Test + void isTransformClass_normalClass_returnsFalse() { + assertFalse(normal("X", "").isTransformClass()); + } + + @Test + void isTransformClass_transformClass_returnsTrue() { + assertTrue(transform("X", "").isTransformClass()); + } + + // ------------------------------------------------------------------ getChecksum + + @Test + void getChecksum_stableForSameContent() throws Exception { + UserDefinedJavaClassDef a = normal("Foo", "int x = 1;"); + UserDefinedJavaClassDef b = normal("Foo", "int x = 1;"); + assertEquals(a.getChecksum(), b.getChecksum()); + } + + @Test + void getChecksum_differsForDifferentSource() throws Exception { + UserDefinedJavaClassDef a = normal("Foo", "int x = 1;"); + UserDefinedJavaClassDef b = normal("Foo", "int y = 2;"); + assertNotEquals(a.getChecksum(), b.getChecksum()); + } + + @Test + void getChecksum_differsForDifferentClassName() throws Exception { + UserDefinedJavaClassDef a = normal("Alpha", "int x = 1;"); + UserDefinedJavaClassDef b = normal("Beta", "int x = 1;"); + assertNotEquals(a.getChecksum(), b.getChecksum()); + } + + @Test + void getChecksum_isHexString() throws Exception { + String checksum = normal("Foo", "src").getChecksum(); + assertTrue(checksum.matches("[0-9a-f]+"), "Expected lowercase hex but got: " + checksum); + } + + // ------------------------------------------------------------------ getTransformedSource + + @Test + void getTransformedSource_containsOriginalSource() { + UserDefinedJavaClassDef def = transform("MyClass", "public void run(){}"); + assertTrue(def.getTransformedSource().contains("public void run(){}")); + } + + @Test + void getTransformedSource_containsClassName() { + UserDefinedJavaClassDef def = transform("Processor", ""); + String transformed = def.getTransformedSource(); + assertTrue(transformed.contains("Processor")); + } + + @Test + void getTransformedSource_containsSuperConstructorCall() { + UserDefinedJavaClassDef def = transform("MyTransform", ""); + assertTrue(def.getTransformedSource().contains("super(parent,meta,data)")); + } + + // ------------------------------------------------------------------ setters (Lombok) + + @Test + void setters_updateFields() { + UserDefinedJavaClassDef def = new UserDefinedJavaClassDef(); + def.setClassType(ClassType.TRANSFORM_CLASS); + def.setClassName("Updated"); + def.setSource("new source"); + assertEquals(ClassType.TRANSFORM_CLASS, def.getClassType()); + assertEquals("Updated", def.getClassName()); + assertEquals("new source", def.getSource()); + } +} diff --git a/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassMetaTest.java b/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassMetaTest.java index 55a11f04fd5..9a307d333d7 100644 --- a/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassMetaTest.java +++ b/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassMetaTest.java @@ -20,23 +20,35 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.apache.hop.core.ICheckResult; import org.apache.hop.core.exception.HopException; import org.apache.hop.core.exception.HopRuntimeException; +import org.apache.hop.core.exception.HopTransformException; import org.apache.hop.core.plugins.PluginRegistry; +import org.apache.hop.core.row.IRowMeta; import org.apache.hop.core.row.IValueMeta; +import org.apache.hop.core.row.RowMeta; import org.apache.hop.core.row.value.ValueMetaDate; import org.apache.hop.core.row.value.ValueMetaInteger; import org.apache.hop.core.row.value.ValueMetaNumber; import org.apache.hop.core.row.value.ValueMetaPlugin; import org.apache.hop.core.row.value.ValueMetaPluginType; import org.apache.hop.core.row.value.ValueMetaString; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.PipelineMeta; import org.apache.hop.pipeline.transform.TransformMeta; import org.apache.hop.pipeline.transform.TransformSerializationTestUtil; +import org.apache.hop.pipeline.transforms.janino.JaninoMeta; +import org.codehaus.commons.compiler.CompileException; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -195,6 +207,8 @@ void testRoundTrip() throws Exception { private void validate(UserDefinedJavaClassMeta meta) { assertFalse(meta.isClearingResultFields()); + assertEquals(17, meta.getJavaTargetVersion()); + assertEquals(17, meta.getEffectiveJavaTargetVersion()); // Definitions assertEquals(2, meta.getDefinitions().size()); @@ -263,4 +277,363 @@ private void validate(UserDefinedJavaClassMeta meta) { assertEquals("parameterValue2", p.getValue()); assertEquals("parameterDescription2", p.getDescription()); } + + // ------------------------------------------------------------------ + // getEffectiveJavaTargetVersion + + @Test + void effectiveVersion_default_returnsDefault() { + UserDefinedJavaClassMeta meta = new UserDefinedJavaClassMeta(); + assertEquals(JaninoMeta.JAVA_TARGET_VERSION_DEFAULT, meta.getEffectiveJavaTargetVersion()); + } + + @Test + void effectiveVersion_validValues_returnAsIs() { + UserDefinedJavaClassMeta meta = new UserDefinedJavaClassMeta(); + meta.setJavaTargetVersion(8); + assertEquals(8, meta.getEffectiveJavaTargetVersion()); + meta.setJavaTargetVersion(11); + assertEquals(11, meta.getEffectiveJavaTargetVersion()); + meta.setJavaTargetVersion(JaninoMeta.JAVA_TARGET_VERSION_MIN); + assertEquals(JaninoMeta.JAVA_TARGET_VERSION_MIN, meta.getEffectiveJavaTargetVersion()); + meta.setJavaTargetVersion(JaninoMeta.JAVA_TARGET_VERSION_MAX); + assertEquals(JaninoMeta.JAVA_TARGET_VERSION_MAX, meta.getEffectiveJavaTargetVersion()); + } + + @Test + void effectiveVersion_tooLow_returnsDefault() { + UserDefinedJavaClassMeta meta = new UserDefinedJavaClassMeta(); + meta.setJavaTargetVersion(0); + assertEquals(JaninoMeta.JAVA_TARGET_VERSION_DEFAULT, meta.getEffectiveJavaTargetVersion()); + meta.setJavaTargetVersion(-5); + assertEquals(JaninoMeta.JAVA_TARGET_VERSION_DEFAULT, meta.getEffectiveJavaTargetVersion()); + } + + @Test + void effectiveVersion_tooHigh_returnsDefault() { + UserDefinedJavaClassMeta meta = new UserDefinedJavaClassMeta(); + meta.setJavaTargetVersion(99); + assertEquals(JaninoMeta.JAVA_TARGET_VERSION_DEFAULT, meta.getEffectiveJavaTargetVersion()); + } + + // ------------------------------------------------------------------ setJavaTargetVersion marks + // hasChanged + + @Test + void setJavaTargetVersion_differentValue_marksHasChanged() throws HopException { + String code = + "public boolean processRow() throws HopException { setOutputDone(); return false; }\n"; + UserDefinedJavaClassMeta meta = new UserDefinedJavaClassMeta(); + // Cook once so hasChanged becomes false + UserDefinedJavaClassDef def = + new UserDefinedJavaClassDef( + UserDefinedJavaClassDef.ClassType.TRANSFORM_CLASS, "Proc", code); + meta.replaceDefinitions(new ArrayList<>(Collections.singletonList(def))); + meta.cookClasses(); + + // Now change the version — should set hasChanged + meta.setJavaTargetVersion(8); + assertEquals(8, meta.getJavaTargetVersion()); + } + + @Test + void setJavaTargetVersion_sameValue_doesNotAlterValue() { + UserDefinedJavaClassMeta meta = new UserDefinedJavaClassMeta(); + int initial = meta.getJavaTargetVersion(); + meta.setJavaTargetVersion(initial); // same value + assertEquals(initial, meta.getJavaTargetVersion()); + } + + // ------------------------------------------------------------------ clone copies version + + @Test + void clone_copiesJavaTargetVersion() { + UserDefinedJavaClassMeta original = new UserDefinedJavaClassMeta(); + original.setJavaTargetVersion(17); + UserDefinedJavaClassMeta copy = original.clone(); + + assertNotSame(original, copy); + assertEquals(17, copy.getJavaTargetVersion()); + assertEquals(17, copy.getEffectiveJavaTargetVersion()); + } + + // ------------------------------------------------------------------ cookClass: different target + // versions + + @Test + void cookClass_target8_staticInterfaceMethod_succeeds() throws Exception { + String code = + "public int cmp() {\n" + + " return java.util.Comparator.naturalOrder().compare(Integer.valueOf(3), Integer.valueOf(5));\n" + + "}\n"; + UserDefinedJavaClassMeta meta = new UserDefinedJavaClassMeta(); + meta.setJavaTargetVersion(8); + UserDefinedJavaClassDef def = + new UserDefinedJavaClassDef( + UserDefinedJavaClassDef.ClassType.NORMAL_CLASS, "CmpClass", code); + + Class cooked = meta.cookClass(def, null); + assertNotSame(null, cooked); + assertEquals("CmpClass", cooked.getSimpleName()); + } + + @Test + void cookClass_target17_staticInterfaceMethod_succeeds() throws Exception { + String code = + "public int cmp() {\n" + + " return java.util.Comparator.naturalOrder().compare(Integer.valueOf(3), Integer.valueOf(5));\n" + + "}\n"; + UserDefinedJavaClassMeta meta = new UserDefinedJavaClassMeta(); + meta.setJavaTargetVersion(17); + UserDefinedJavaClassDef def = + new UserDefinedJavaClassDef( + UserDefinedJavaClassDef.ClassType.NORMAL_CLASS, "CmpClass17", code); + + Class cooked = meta.cookClass(def, null); + assertNotSame(null, cooked); + } + + @Test + void cookClass_target6_staticInterfaceMethod_throwsCompileException() { + String code = + "public int cmp() {\n" + + " return java.util.Comparator.naturalOrder().compare(Integer.valueOf(3), Integer.valueOf(5));\n" + + "}\n"; + UserDefinedJavaClassMeta meta = new UserDefinedJavaClassMeta(); + meta.setJavaTargetVersion(6); + UserDefinedJavaClassDef def = + new UserDefinedJavaClassDef( + UserDefinedJavaClassDef.ClassType.NORMAL_CLASS, "CmpClass6", code); + + assertThrows(CompileException.class, () -> meta.cookClass(def, null)); + } + + // ------------------------------------------------------------------ cache: different versions + // use separate entries + + @Test + void cookClass_sameCodeDifferentTargets_returnsDifferentClasses() throws Exception { + String code = "public int val() { return 1; }\n"; + + UserDefinedJavaClassMeta meta8 = new UserDefinedJavaClassMeta(); + meta8.setJavaTargetVersion(8); + UserDefinedJavaClassDef def8 = + new UserDefinedJavaClassDef( + UserDefinedJavaClassDef.ClassType.NORMAL_CLASS, "ValClass", code); + + UserDefinedJavaClassMeta meta17 = new UserDefinedJavaClassMeta(); + meta17.setJavaTargetVersion(17); + UserDefinedJavaClassDef def17 = + new UserDefinedJavaClassDef( + UserDefinedJavaClassDef.ClassType.NORMAL_CLASS, "ValClass", code); + + Class cooked8 = meta8.cookClass(def8, null); + Class cooked17 = meta17.cookClass(def17, null); + // Both succeed; they must be non-null + assertNotSame(null, cooked8); + assertNotSame(null, cooked17); + } + + // ------------------------------------------------------------------ check() + + @Test + void check_withInputTransforms_addsOk() { + UserDefinedJavaClassMeta meta = new UserDefinedJavaClassMeta(); + List remarks = new ArrayList<>(); + meta.check( + remarks, + mock(PipelineMeta.class), + mock(TransformMeta.class), + new RowMeta(), + new String[] {"in"}, + new String[0], + mock(IRowMeta.class), + null, + mock(IHopMetadataProvider.class)); + + assertTrue(remarks.stream().anyMatch(r -> r.getType() == ICheckResult.TYPE_RESULT_OK)); + } + + @Test + void check_noInputTransforms_addsError() { + UserDefinedJavaClassMeta meta = new UserDefinedJavaClassMeta(); + List remarks = new ArrayList<>(); + meta.check( + remarks, + mock(PipelineMeta.class), + mock(TransformMeta.class), + new RowMeta(), + new String[0], + new String[0], + mock(IRowMeta.class), + null, + mock(IHopMetadataProvider.class)); + + assertTrue(remarks.stream().anyMatch(r -> r.getType() == ICheckResult.TYPE_RESULT_ERROR)); + } + + // ------------------------------------------------------------------ supportsErrorHandling / + // excludeFromRowLayoutVerification + + @Test + void supportsErrorHandling_returnsTrue() { + assertTrue(new UserDefinedJavaClassMeta().supportsErrorHandling()); + } + + @Test + void excludeFromRowLayoutVerification_returnsTrue() { + assertTrue(new UserDefinedJavaClassMeta().excludeFromRowLayoutVerification()); + } + + // ------------------------------------------------------------------ FieldInfo + + @Test + void fieldInfo_constructorAndGetters() { + UserDefinedJavaClassMeta.FieldInfo f = + new UserDefinedJavaClassMeta.FieldInfo("myField", IValueMeta.TYPE_INTEGER, 9, 2); + assertEquals("myField", f.getName()); + assertEquals(IValueMeta.TYPE_INTEGER, f.getType()); + assertEquals(9, f.getLength()); + assertEquals(2, f.getPrecision()); + } + + @Test + void fieldInfo_copyConstructor_copiesAllFields() { + UserDefinedJavaClassMeta.FieldInfo original = + new UserDefinedJavaClassMeta.FieldInfo("x", IValueMeta.TYPE_STRING, 50, -1); + UserDefinedJavaClassMeta.FieldInfo copy = new UserDefinedJavaClassMeta.FieldInfo(original); + assertNotSame(original, copy); + assertEquals("x", copy.getName()); + assertEquals(IValueMeta.TYPE_STRING, copy.getType()); + assertEquals(50, copy.getLength()); + assertEquals(-1, copy.getPrecision()); + } + + @Test + void fieldInfo_setters() { + UserDefinedJavaClassMeta.FieldInfo f = new UserDefinedJavaClassMeta.FieldInfo(); + f.setName("n"); + f.setType(IValueMeta.TYPE_NUMBER); + f.setLength(10); + f.setPrecision(3); + assertEquals("n", f.getName()); + assertEquals(IValueMeta.TYPE_NUMBER, f.getType()); + assertEquals(10, f.getLength()); + assertEquals(3, f.getPrecision()); + } + + // ------------------------------------------------------------------ replaceFields / setFieldInfo + + @Test + void replaceFields_updatesFieldsList() { + UserDefinedJavaClassMeta meta = new UserDefinedJavaClassMeta(); + List newFields = new ArrayList<>(); + newFields.add(new UserDefinedJavaClassMeta.FieldInfo("a", IValueMeta.TYPE_STRING, 10, -1)); + meta.replaceFields(newFields); + assertEquals(1, meta.getFields().size()); + assertEquals("a", meta.getFields().get(0).getName()); + } + + @Test + void setFieldInfo_delegatesToReplaceFields() { + UserDefinedJavaClassMeta meta = new UserDefinedJavaClassMeta(); + List fields = new ArrayList<>(); + fields.add(new UserDefinedJavaClassMeta.FieldInfo("b", IValueMeta.TYPE_INTEGER, 9, 0)); + meta.setFieldInfo(fields); + assertEquals(1, meta.getFields().size()); + assertEquals("b", meta.getFields().get(0).getName()); + } + + // ------------------------------------------------------------------ replaceDefinitions + + @Test + void replaceDefinitions_ordersNormalBeforeTransformClasses() { + UserDefinedJavaClassMeta meta = new UserDefinedJavaClassMeta(); + List defs = new ArrayList<>(); + defs.add( + new UserDefinedJavaClassDef( + UserDefinedJavaClassDef.ClassType.TRANSFORM_CLASS, + "Proc", + "public boolean processRow() { return true; }\n")); + defs.add( + new UserDefinedJavaClassDef( + UserDefinedJavaClassDef.ClassType.NORMAL_CLASS, + "Helper", + "public int x() { return 1; }\n")); + meta.replaceDefinitions(defs); + + // Normal classes come first + assertEquals("Helper", meta.getDefinitions().get(0).getClassName()); + assertEquals("Proc", meta.getDefinitions().get(1).getClassName()); + } + + // ------------------------------------------------------------------ + // searchInfoAndTargetTransforms + + @Test + void searchInfoAndTargetTransforms_resolvesMatchingTransforms() { + UserDefinedJavaClassMeta meta = new UserDefinedJavaClassMeta(); + + InfoTransformDefinition infoDef = new InfoTransformDefinition(); + infoDef.setTag("lookup"); + infoDef.setTransformName("LookupStep"); + meta.getInfoTransformDefinitions().add(infoDef); + + TargetTransformDefinition targetDef = new TargetTransformDefinition(); + targetDef.tag = "out"; + targetDef.transformName = "OutputStep"; + meta.getTargetTransformDefinitions().add(targetDef); + + TransformMeta lookupMeta = Mockito.mock(TransformMeta.class); + Mockito.when(lookupMeta.getName()).thenReturn("LookupStep"); + TransformMeta outputMeta = Mockito.mock(TransformMeta.class); + Mockito.when(outputMeta.getName()).thenReturn("OutputStep"); + + meta.searchInfoAndTargetTransforms(List.of(lookupMeta, outputMeta)); + + assertEquals(lookupMeta, infoDef.transformMeta); + assertEquals(outputMeta, targetDef.transformMeta); + } + + @Test + void searchInfoAndTargetTransforms_noMatchSetsNull() { + UserDefinedJavaClassMeta meta = new UserDefinedJavaClassMeta(); + + InfoTransformDefinition infoDef = new InfoTransformDefinition(); + infoDef.setTag("lookup"); + infoDef.setTransformName("NonExistent"); + meta.getInfoTransformDefinitions().add(infoDef); + + TransformMeta otherMeta = Mockito.mock(TransformMeta.class); + Mockito.when(otherMeta.getName()).thenReturn("SomeOtherStep"); + + meta.searchInfoAndTargetTransforms(List.of(otherMeta)); + + assertNull(infoDef.transformMeta); + } + + // ------------------------------------------------------------------ getFields: cook errors + + @Test + void getFields_withCookErrors_throwsHopTransformException() throws Exception { + UserDefinedJavaClassMeta meta = new UserDefinedJavaClassMeta(); + UserDefinedJavaClassDef badDef = Mockito.mock(UserDefinedJavaClassDef.class); + Mockito.when(badDef.isTransformClass()).thenReturn(false); + Mockito.when(badDef.getSource()).thenReturn("THIS IS NOT JAVA !!!"); + Mockito.when(badDef.getClassName()).thenReturn("Bad"); + // getChecksum() throws HopTransformException – use doReturn to avoid the checked-exception + // surfacing in the when() call itself + Mockito.doReturn("badchecksum-unique-" + System.nanoTime()).when(badDef).getChecksum(); + + UserDefinedJavaClassMeta spy = Mockito.spy(meta); + Mockito.when(spy.getDefinitions()).thenReturn(Collections.singletonList(badDef)); + + TransformMeta transformMeta = Mockito.mock(TransformMeta.class); + Mockito.when(transformMeta.getName()).thenReturn("UDJC"); + spy.setParentTransformMeta(transformMeta); + + assertThrows( + HopTransformException.class, + () -> spy.getFields(new RowMeta(), "step", null, null, null, null)); + } } diff --git a/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassTest.java b/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassTest.java new file mode 100644 index 00000000000..396457f25a4 --- /dev/null +++ b/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassTest.java @@ -0,0 +1,188 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hop.pipeline.transforms.userdefinedjavaclass; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import org.apache.hop.core.logging.HopLogStore; +import org.apache.hop.core.logging.ILoggingObject; +import org.apache.hop.core.plugins.PluginRegistry; +import org.apache.hop.core.row.value.ValueMetaInteger; +import org.apache.hop.core.row.value.ValueMetaPlugin; +import org.apache.hop.core.row.value.ValueMetaPluginType; +import org.apache.hop.core.row.value.ValueMetaString; +import org.apache.hop.pipeline.transforms.mock.TransformMockHelper; +import org.apache.hop.pipeline.transforms.userdefinedjavaclass.UserDefinedJavaClassDef.ClassType; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link UserDefinedJavaClass} lifecycle: constructor, {@code processRow()}, and + * {@code init()}. + */ +class UserDefinedJavaClassTest { + + private TransformMockHelper helper; + + @BeforeAll + static void initPlugins() throws Exception { + HopLogStore.init(); + PluginRegistry registry = PluginRegistry.getInstance(); + registry.registerPluginClass( + ValueMetaString.class.getName(), ValueMetaPluginType.class, ValueMetaPlugin.class); + registry.registerPluginClass( + ValueMetaInteger.class.getName(), ValueMetaPluginType.class, ValueMetaPlugin.class); + } + + @BeforeEach + void setUp() { + helper = + new TransformMockHelper<>( + "UDJC TEST", UserDefinedJavaClassMeta.class, UserDefinedJavaClassData.class); + when(helper.logChannelFactory.create(any(), any(ILoggingObject.class))) + .thenReturn(helper.iLogChannel); + when(helper.pipeline.isRunning()).thenReturn(true); + } + + @AfterEach + void tearDown() { + helper.cleanUp(); + } + + // ------------------------------------------------------------------ helpers + + /** + * Builds a {@link UserDefinedJavaClass} backed by the given meta. Uses {@code copyNr=0} so the + * constructor calls {@code cookClasses()}. + */ + private UserDefinedJavaClass build(UserDefinedJavaClassMeta meta) { + return new UserDefinedJavaClass( + helper.transformMeta, + meta, + new UserDefinedJavaClassData(), + 0, + helper.pipelineMeta, + helper.pipeline); + } + + // ------------------------------------------------------------------ processRow: null child + + @Test + void processRow_withNullChild_returnsFalse() throws Exception { + // Meta with no definitions → no transform class → child stays null + UserDefinedJavaClassMeta meta = new UserDefinedJavaClassMeta(); + UserDefinedJavaClass transform = build(meta); + + assertNull(transform.getChild()); + assertFalse(transform.processRow()); + } + + // ------------------------------------------------------------------ init: no cooked class + + @Test + void init_noTransformClassDefined_returnsFalse() { + // Meta with no definitions → cookedTransformClass remains null + UserDefinedJavaClassMeta meta = new UserDefinedJavaClassMeta(); + UserDefinedJavaClass transform = build(meta); + + assertFalse(transform.init()); + } + + // ------------------------------------------------------------------ init: cook errors + + @Test + void init_withCookErrors_returnsFalse() { + UserDefinedJavaClassMeta meta = new UserDefinedJavaClassMeta(); + // Duplicate method → compilation error + String badSource = + "public boolean processRow() { return true; }\n" + + "public boolean processRow() { return true; }\n"; + meta.getDefinitions() + .add(new UserDefinedJavaClassDef(ClassType.NORMAL_CLASS, "BadClass", badSource)); + + UserDefinedJavaClass transform = build(meta); + + assertFalse(meta.getCookErrors().isEmpty()); + assertFalse(transform.init()); + } + + // ------------------------------------------------------------------ constructor: copyNr != 0 + // skips explicit cook + + @Test + void constructor_copyNrNonZero_constructsWithoutThrowingAndNoCookErrors() { + // copyNr=1: explicit cookClasses() call in the constructor body is skipped; + // cooking happens lazily inside newChildInstance() via checkClassCooked(). + // For a clean meta with no definitions, no cook errors occur. + UserDefinedJavaClassMeta meta = new UserDefinedJavaClassMeta(); + + new UserDefinedJavaClass( + helper.transformMeta, + meta, + new UserDefinedJavaClassData(), + 1, + helper.pipelineMeta, + helper.pipeline); + + assertTrue(meta.getCookErrors().isEmpty()); + } + + // ------------------------------------------------------------------ cookClasses: TRANSFORM_CLASS + + @Test + void cookClasses_withValidTransformClass_setsCookedClass() throws Exception { + // Call cookClasses() directly to verify compilation independent of child instantiation + // (child instantiation fails in tests because pipelineMeta.getPrevTransformFields returns + // null). + String source = + "public boolean processRow() throws org.apache.hop.core.exception.HopException {\n" + + " setOutputDone();\n" + + " return false;\n" + + "}\n"; + + UserDefinedJavaClassMeta meta = new UserDefinedJavaClassMeta(); + meta.getDefinitions() + .add(new UserDefinedJavaClassDef(ClassType.TRANSFORM_CLASS, "Processor", source)); + + meta.cookClasses(); + + assertTrue(meta.getCookErrors().isEmpty(), "Expected no cook errors"); + assertFalse(meta.getCookedTransformClass() == null, "Expected cookedTransformClass to be set"); + } + + // ------------------------------------------------------------------ getDefinitions / hasChanged + + @Test + void metaGetDefinitions_initiallyEmpty() { + UserDefinedJavaClassMeta meta = new UserDefinedJavaClassMeta(); + assertTrue(meta.getDefinitions().isEmpty()); + } + + @Test + void metaReplaceDefinitions_setsHasChanged() { + UserDefinedJavaClassMeta meta = new UserDefinedJavaClassMeta(); + meta.replaceDefinitions(Collections.emptyList()); + assertTrue(meta.isHasChanged()); + } +} diff --git a/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/util/JaninoCheckerUtilTest.java b/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/util/JaninoCheckerUtilTest.java new file mode 100644 index 00000000000..fff1da2d38d --- /dev/null +++ b/plugins/transforms/janino/src/test/java/org/apache/hop/pipeline/transforms/util/JaninoCheckerUtilTest.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hop.pipeline.transforms.util; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.apache.hop.core.logging.HopLogStore; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class JaninoCheckerUtilTest { + + @BeforeAll + static void init() { + HopLogStore.init(); + } + + // ------------------------------------------------------------------ constructor + + @Test + void constructor_noExclusionsFile_doesNotThrow() { + // The codeExclusions.xml file will not be found in test env → logged, no throw + assertDoesNotThrow(JaninoCheckerUtil::new); + } + + @Test + void constructor_whenFileNotFound_matchesListIsEmpty() { + JaninoCheckerUtil util = new JaninoCheckerUtil(); + // No exclusions file in test classpath → empty matches + assertTrue(util.matches.isEmpty()); + } + + // ------------------------------------------------------------------ checkCode: no exclusions + + @Test + void checkCode_noExclusions_alwaysReturnsEmpty() { + JaninoCheckerUtil util = new JaninoCheckerUtil(); + assertTrue(util.checkCode("System.exit(0);").isEmpty()); + assertTrue(util.checkCode("Runtime.getRuntime().exec(\"cmd\")").isEmpty()); + assertTrue(util.checkCode("").isEmpty()); + } + + // ------------------------------------------------------------------ checkCode: with exclusions + + @Test + void checkCode_matchingExclusion_returnsIt() { + JaninoCheckerUtil util = new JaninoCheckerUtil(); + util.matches.add("System.exit"); + + List violations = util.checkCode("System.exit(0);"); + assertEquals(1, violations.size()); + assertEquals("System.exit", violations.get(0)); + } + + @Test + void checkCode_noMatchingExclusion_returnsEmpty() { + JaninoCheckerUtil util = new JaninoCheckerUtil(); + util.matches.add("System.exit"); + + assertTrue(util.checkCode("Math.abs(-1)").isEmpty()); + } + + @Test + void checkCode_multipleExclusions_onlySomeMatch() { + JaninoCheckerUtil util = new JaninoCheckerUtil(); + util.matches.add("System.exit"); + util.matches.add("Runtime.exec"); + util.matches.add("ProcessBuilder"); + + List violations = util.checkCode("Runtime.exec(\"cmd\")"); + assertEquals(1, violations.size()); + assertEquals("Runtime.exec", violations.get(0)); + } + + @Test + void checkCode_multipleExclusionsAllMatch_returnsAll() { + JaninoCheckerUtil util = new JaninoCheckerUtil(); + util.matches.add("System.exit"); + util.matches.add("Runtime.exec"); + + List violations = util.checkCode("System.exit(0); Runtime.exec(\"cmd\")"); + assertEquals(2, violations.size()); + assertTrue(violations.contains("System.exit")); + assertTrue(violations.contains("Runtime.exec")); + } + + @Test + void checkCode_partialMatch_returnsMatch() { + JaninoCheckerUtil util = new JaninoCheckerUtil(); + util.matches.add("exit"); + + // "exit" appears as a substring within "System.exit" + assertFalse(util.checkCode("System.exit(1)").isEmpty()); + } + + // ------------------------------------------------------------------ getJarPath + + @Test + void getJarPath_returnsNonNullNonEmpty() { + JaninoCheckerUtil util = new JaninoCheckerUtil(); + String path = util.getJarPath(); + assertNotNull(path); + assertFalse(path.isBlank()); + } +} diff --git a/plugins/transforms/janino/src/test/resources/java-filter.xml b/plugins/transforms/janino/src/test/resources/java-filter.xml new file mode 100644 index 00000000000..a08dd68a842 --- /dev/null +++ b/plugins/transforms/janino/src/test/resources/java-filter.xml @@ -0,0 +1,22 @@ + + + + amount > 0 + positiveRows + negativeRows + diff --git a/plugins/transforms/janino/src/test/resources/user-defined-java-class.xml b/plugins/transforms/janino/src/test/resources/user-defined-java-class.xml index 63fdbdb6bb0..553e9697063 100644 --- a/plugins/transforms/janino/src/test/resources/user-defined-java-class.xml +++ b/plugins/transforms/janino/src/test/resources/user-defined-java-class.xml @@ -50,6 +50,7 @@ N + 17 info1