This is an automated email from the ASF dual-hosted git repository.
hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new b929b00687 Add option to select java version and tests, fixes #5172
(#7014)
b929b00687 is described below
commit b929b006872f01b6eed90706c5a360ee14b1623c
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Fri Apr 17 14:39:54 2026 +0200
Add option to select java version and tests, fixes #5172 (#7014)
---
.../hop/pipeline/transforms/janino/Janino.java | 9 +
.../pipeline/transforms/janino/JaninoDialog.java | 40 +-
.../hop/pipeline/transforms/janino/JaninoMeta.java | 28 ++
.../transforms/janino/function/FunctionLib.java | 12 +-
.../UserDefinedJavaClassDialog.java | 36 ++
.../UserDefinedJavaClassMeta.java | 43 +-
.../janino/messages/messages_en_US.properties | 1 +
.../messages/messages_en_US.properties | 1 +
.../transforms/janino/JaninoMetaFunctionTest.java | 115 +++++
.../pipeline/transforms/janino/JaninoMetaTest.java | 243 +++++++++-
.../hop/pipeline/transforms/janino/JaninoTest.java | 383 ++++++++++++++++
.../transforms/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 ++++++++
.../transforms/util/JaninoCheckerUtilTest.java | 124 ++++++
.../janino/src/test/resources/java-filter.xml | 22 +
.../src/test/resources/user-defined-java-class.xml | 1 +
23 files changed, 3145 insertions(+), 9 deletions(-)
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 00bff1e971..3136dceadc 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 @@ public class Janino extends BaseTransform<JaninoMeta,
JaninoData> {
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 a340064cd4..7cae557251 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.ColumnInfo;
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 @@ import org.eclipse.swt.widgets.TableItem;
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 class JaninoDialog extends BaseTransformDialog {
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 @@ public class JaninoDialog extends BaseTransformDialog {
/** 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 @@ public class JaninoDialog extends BaseTransformDialog {
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 1cd6d46443..5127f2ac0f 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 @@ import org.apache.hop.pipeline.transform.TransformMeta;
public class JaninoMeta extends BaseTransformMeta<Janino, JaninoData> {
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<Janino,
JaninoData> {
injectionGroupDescription = "Janino.Injection.FORMULA")
private List<JaninoMetaFunction> 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 class JaninoMeta extends BaseTransformMeta<Janino,
JaninoData> {
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 1eb0f1761e..37e4072725 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.Comparator;
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 class FunctionLib {
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 7324381566..2377d09de1 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.Pipeline;
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.pipeline.transform.BaseTransformDialog;
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 class UserDefinedJavaClassDialog extends
BaseTransformDialog {
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 class UserDefinedJavaClassDialog extends
BaseTransformDialog {
wClearResultFields.setSelection(input.isClearingResultFields());
+
wJavaTargetVersion.setText(Integer.toString(input.getEffectiveJavaTargetVersion()));
+
wFields.setRowNums();
wFields.optWidth(true);
@@ -1118,6 +1151,9 @@ public class UserDefinedJavaClassDialog extends
BaseTransformDialog {
}
private void getInfo(UserDefinedJavaClassMeta meta) {
+ meta.setJavaTargetVersion(
+ Const.toInt(wJavaTargetVersion.getText(),
JaninoMeta.JAVA_TARGET_VERSION_DEFAULT));
+
int nrFields = wFields.nrNonEmpty();
List<FieldInfo> 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 9552e1b3f7..4e10d11a8b 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.lang.reflect.Method;
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.PipelineMeta;
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 class UserDefinedJavaClassMeta
injectionGroupDescription = "UserDefinedJavaClass.Injection.PARAMETERS")
private List<UsageParameter> 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 class UserDefinedJavaClassMeta
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 @@ public class UserDefinedJavaClassMeta
"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 571ff8de87..480f1c13ce 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 293503295a..e857301ccd 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 0000000000..9df82d231e
--- /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 9eee4ed8ee..2f08e89149 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.ValueMetaPlugin;
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 @@ class JaninoMetaTest {
}
}
+ // ------------------------------------------------------------------
load/save
+
@Test
void testLoadSave() throws Exception {
Path path =
Paths.get(Objects.requireNonNull(getClass().getResource("/janino.xml")).toURI());
@@ -62,10 +76,9 @@ class JaninoMetaTest {
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 @@ class JaninoMetaTest {
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 @@ class JaninoMetaTest {
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<ICheckResult> 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<ICheckResult> 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<ICheckResult> 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<ICheckResult> 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 0000000000..cf4f00eef7
--- /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<JaninoMeta, JaninoData> 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<Object[]> 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<Object[]> 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<Object[]> 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<Object[]> 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<Object[]> 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<Object[]> 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 0000000000..2262c835be
--- /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<IStream> 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<IStream> 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<IStream> 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<ICheckResult> 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<ICheckResult> 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<ICheckResult> 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<ICheckResult> 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<ICheckResult> 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<ICheckResult> 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<ICheckResult> 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<ICheckResult> 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<ICheckResult> 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<ICheckResult> 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<ICheckResult> 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 0000000000..aa84e8aadb
--- /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<JavaFilterMeta, JavaFilterData> 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<Object[]> 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 ef58fca2d0..9271c00128 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 @@ package
org.apache.hop.pipeline.transforms.userdefinedjavaclass;
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 @@ class FieldHelperTest {
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 0000000000..445aa5fcb4
--- /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 0000000000..6f7dbfea00
--- /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 0000000000..5a0aeb2349
--- /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 0000000000..25b3a26d0c
--- /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 55a11f04fd..9a307d333d 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 @@ 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.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 @@ class UserDefinedJavaClassMetaTest {
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 @@ class UserDefinedJavaClassMetaTest {
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<ICheckResult> 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<ICheckResult> 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<UserDefinedJavaClassMeta.FieldInfo> 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<UserDefinedJavaClassMeta.FieldInfo> 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<UserDefinedJavaClassDef> 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 0000000000..396457f25a
--- /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<UserDefinedJavaClassMeta,
UserDefinedJavaClassData> 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 0000000000..fff1da2d38
--- /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<String> 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<String> 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<String> 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 0000000000..a08dd68a84
--- /dev/null
+++ b/plugins/transforms/janino/src/test/resources/java-filter.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+<transform>
+ <condition>amount > 0</condition>
+ <send_true_to>positiveRows</send_true_to>
+ <send_false_to>negativeRows</send_false_to>
+</transform>
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 63fdbdb6bb..553e969706 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 @@
</field>
</fields>
<clear_result_fields>N</clear_result_fields>
+ <java_target_version>17</java_target_version>
<info_transforms>
<info_transform>
<transform_tag>info1</transform_tag>