This is an automated email from the ASF dual-hosted git repository.

bamaer 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 c233657883 Issue #2368 : Document User Defined Java Class execution, 
fields, and options (#8673)
c233657883 is described below

commit c233657883a3de7d9989f2e52962dade9833994f
Author: Matt Casters <[email protected]>
AuthorDate: Thu Oct 1 08:54:00 2026 +0200

    Issue #2368 : Document User Defined Java Class execution, fields, and 
options (#8673)
    
    * Issue #2368 : Document User Defined Java Class execution, fields, and 
options
    
    * Fixes #2368 : Fix getInputRowMeta sample, Janino language limits and code 
exclusions in UDJC docs
    
    ---------
    
    Co-authored-by: Bart Maertens <[email protected]>
---
 .../pipeline/transforms/userdefinedjavaclass.adoc  | 585 +++++++++++++++------
 1 file changed, 421 insertions(+), 164 deletions(-)

diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/userdefinedjavaclass.adoc
 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/userdefinedjavaclass.adoc
index e13f8f0e4b..c3a3dde61a 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/userdefinedjavaclass.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/userdefinedjavaclass.adoc
@@ -16,239 +16,496 @@ under the License.
 ////
 :documentationPath: /pipeline/transforms/
 :language: en_US
-:description: The User Defined Java Class transform allows you to enter User 
Defined Java Class to drive the functionality of a complete transform.
+:description: The User Defined Java Class transform runs a Java class body you 
type in the dialog, compiled at runtime with Janino, as the transform.
 :page-engines: Hop Engine=yes, Single Threaded=yes, Native Spark=yes, Beam 
Spark=maybe, Beam Flink=maybe, Beam Dataflow=maybe
 
 = image:transforms/icons/userdefinedjavaclass.svg[User Defined Java Class 
transform Icon, role="image-doc-icon"] User Defined Java Class
 
 == Description
 
-The User Defined Java Class transform allows you to enter User Defined Java 
Class to drive the functionality of a complete transform.
-
-In essence, this transform allows you to program your own plugin in a 
transform.
-
-The goal of this transform is not to allow a user to do full-scale Java 
development inside a transform.
-
-Obviously we have a whole plugin system available to help with that part.
-
-The goal is to allow users to define methods and logic with as little as code 
as possible, executed as fast as possible.
-
-For this we use the https://janino-compiler.github.io/janino/[Janino^] project 
libraries that compile Java code in the form of classes at runtime.
+The User Defined Java Class transform runs Java code you type in the dialog as 
the implementation of one transform.
+
+It is not a place to build a full plugin.
+A real plugin is the right tool when the logic is reused, needs its own 
dialog, or needs more than a class body.
+This transform is for a small piece of logic that has to run as fast as a 
normal transform.
+
+The code is compiled when the pipeline starts by the 
https://janino-compiler.github.io/janino/[Janino^] libraries.
+You do not write a `.java` file, a `package` line, an `import` line, or a 
`class` declaration.
+Each tab is the *body* of one class: fields and methods only.
+The tab name is the class name, so it has to be a valid Java identifier.
+A new transform starts with one tab named `Processor`.
+
+Janino already imports these packages, so classes from them can be used by 
their simple name:
+
+* `org.apache.hop.pipeline.transforms.userdefinedjavaclass`
+* `org.apache.hop.pipeline.transform`
+* `org.apache.hop.core.row`
+* `org.apache.hop.core`
+* `org.apache.hop.core.exception`
+* `org.apache.hop.pipeline`
+* `org.apache.hop.pipeline.engine`
+* `org.apache.hop.workflow`
+* `org.apache.hop.workflow.action`
+* `org.apache.hop.core.plugins`
+* `org.apache.hop.core.variables`
+* `java.util`
+
+Anything else needs a fully qualified name.
+Put extra jars in `plugins/transforms/janino/lib` so this transform can load 
them.
+See xref:installation-configuration.adoc[Installation and configuration].
+
+One tab is the *transform class*.
+Right-click that tab and choose *Set as transform Class*.
+Hop compiles it as a subclass of its own transform base and adds the 
constructor, so do not write a constructor yourself.
+That class is the one Hop calls while the pipeline runs.
+Other tabs are helper classes.
+They are compiled first, in alphabetical order of the tab name, and the 
transform class can use them with `new`.
+A helper can use another helper only when that other helper's name sorts first.
+Choosing *OK* or *Test class* with no transform class asks whether the first 
tab should become one.
 
 == Options
 
 [options="header",cols="1a,3a"]
 |===
 |Option|Description
-|Transform name|Name of the transform.
-|Class code|The Java code.
-|Fields|List of output fields.
-
-- Fieldname: Output field name.
-- Type: Type of field.
-- Length: Length of the field.
-- Precision: Precision of the field.
-|Parameters|You can use the Parameters table to avoid using hard-coded string 
values, such as field names (customer for example).
-
-- Tag: The parameter tag.
-- Value: The parameter value.
-- Description: Description of the parameter.
-|Info transforms|Additional transforms to read data from
-
-- Tag
-- Transform: Which transform to read from.
-- Description
-|Target transforms|Destination Transform
-
-- Tag
-- Transform: Which transform to output to.
-- Description
-|Test class|Tests the class.
+
+|Transform name
+|Name of the transform.
+This name has to be unique in the pipeline.
+
+|Java target version
+|Java language level Janino uses to compile every class tab.
+Choose a value from 6 to 21.
+A pipeline that does not store the option, or stores a value outside that 
range, is compiled as Java 6.
+The level does not unlock newer language features.
+Janino accepts generics, the diamond operator, for-each loops, `switch` on 
strings, and text blocks at every level.
+It rejects lambdas, method references, `var`, switch expressions, records, 
pattern matching in `instanceof`, and multi-type `catch` at every level, 
including 21.
+Use anonymous classes, explicit types, and plain `switch` statements instead.
+
+|Class code
+|The class body of the selected tab.
+See <<the-class>>.
+
+|Classes and code fragments
+|Tree of class tabs, code snippets, and the input, info, and output fields.
+Double-click or drag an entry to insert it at the cursor.
+See <<classes-and-snippets>>.
+
+|Fields
+|Output fields this transform adds.
+Each row is one field:
+
+* *Fieldname*: name of the output field.
+* *Type*: Hop data type. See xref:concepts.adoc#data-types[Data types].
+* *Length* and *Precision*: see 
xref:pipeline/formatting-values.adoc[Formatting numbers and dates].
+
+*Clear the result fields?* drops every incoming field from the output layout.
+Leave it unchecked to keep the incoming fields and append the rows in this 
table.
+`createOutputRow()` follows the same choice: it copies the input row when the 
box is unchecked, and allocates an empty output row when the box is checked.
+
+|Parameters
+|Named values the class reads with `getParameter(tag)`, so field names and 
other constants are not hard-coded.
+See <<parameters>>.
+
+* *Tag*: the name passed to `getParameter`.
+* *Value*: the text returned for that tag. Variables in the value are resolved.
+* *Description*: comment. Not visible to the class.
+
+|Info transforms
+|Extra input hops, separate from the main input that `getRow()` reads.
+See <<info-and-target>>.
+
+* *Tag*: the name passed to `findInfoRowSet`.
+* *Transform*: an upstream transform already connected to this one.
+* *Description*: label shown for that info hop.
+
+|Target transforms
+|Specific output hops the class writes to with `putRowTo`, instead of the 
ordinary output of `putRow`.
+See <<info-and-target>>.
+
+* *Tag*: the name passed to `findTargetRowSet`.
+* *Transform*: a downstream transform already connected to this one.
+* *Description*: label shown for that target hop.
+
+|Test class
+|Compiles the classes and previews them on generated rows.
+See <<test-class>>.
 |===
 
-== Usage
+[[the-class]]
+== The class
 
-=== Process rows
+=== Which methods are called
 
-The Processor code defines the processRow() method, which is the heart of the 
transform.
-This method is called by the pipeline in a tight loop and will continue until 
false is returned.
+Hop drives the transform class the same way it drives any other transform.
+Three methods matter:
 
-[source,java]
-----
-String firstnameField;
-String lastnameField;
-String nameField;
- 
-public boolean processRow() throws HopException
-{
-    // Let's look up parameters only once for performance reason.
-    //
-    if (first) {
-      firstnameField = getParameter("FIRSTNAME_FIELD");
-      lastnameField = getParameter("LASTNAME_FIELD");
-      nameField = getParameter("NAME_FIELD");
-      first=false;
-    }
- 
-    // First, get a row from the default input hop
-    //
-    Object[] r = getRow();
- 
-    // If the row object is null, we are done processing.
-    //
-    if (r == null) {
-      setOutputDone();
-      return false;
-    }
- 
-    // It is always safest to call createOutputRow() to ensure that your 
output row's Object[] is large
-    // enough to handle any new fields you are creating in this transform.
-    //
-    Object[] outputRow = createOutputRow(r, data.outputRowMeta.size());
- 
-    String firstname = get(Fields.In, firstnameField).getString(r);
-    String lastname = get(Fields.In, lastnameField).getString(r);
- 
-    // Set the value in the output field
-    //
-    String name = firstname+" "+lastname;
-    get(Fields.Out, nameField).setValue(outputRow, name);
- 
-    // putRow will send the row on to the default output hop.
-    //
-    putRow(data.outputRowMeta, outputRow);
- 
-    return true;
-----
+`init()`::
+Called once while the pipeline is preparing to start.
+Return `true` when initialization worked, or `false` to make the pipeline 
abort.
+The default implementation initializes the transform.
+Override it to open files or connections, and call `parent.initImpl()` (or 
`super.init()`) so that still happens.
+
+`processRow()`::
+The method that does the work.
+You have to implement it; the class does not compile without it.
+Once execution has started, Hop calls it in a tight loop.
+
+* Return `true` to be called again.
+* When there is nothing left to do, call `setOutputDone()` and return `false`.
+* `getRow()` blocks until the next row from the main input arrives, and 
returns `null` when that input is finished.
+  That `null` is the usual place to call `setOutputDone()` and return `false`.
+
++
+An exception thrown out of `processRow()` stops the pipeline: the error is 
logged, the error count is set, and the transform marks its output done.
+Catch a bad row yourself and send it to the error hop when the pipeline should 
keep going.
+See <<error-handling>>.
+
+`dispose()`::
+Called once when this transform stops running, after its last `processRow()` 
call.
+Close what `init()` opened.
+Call `parent.disposeImpl()` (or `super.dispose()`).
+
+`initBeforeStart()` exists as well and is called once after `init()`, just 
before the threads start.
+Most classes do not need it.
 
-=== Error handling
+The dialog inserts starter code for `processRow()`, `init()`, and `dispose()` 
from the *Code Snippets* tree (*Implement processRow*, *Implement init*, 
*Implement dispose*).
 
-If you want Hop to handle errors that may occur while running your class in a 
pipeline, you must implement for your own error handling code.
-Before adding any error handling code, right-click on the User Defined Java 
Class transform in the Hop client canvas and select Error Handling in the menu 
that appears.
-The resulting transform error handling settings dialog box contains options 
for specifying an error target transform and associated field names that you 
will use to implement error handling in your defined code.
+=== Example
+
+This class reads two input fields and writes a third.
+On the *Parameters* tab, add tags `FIRSTNAME_FIELD`, `LASTNAME_FIELD`, and 
`NAME_FIELD` with values `firstname`, `lastname`, and `name`.
+On the *Fields* tab, add one output field named `name`, type String.
+The transform class tab can stay named `Processor`.
 
 [source,java]
 ----
-try {
+String firstNameField;
+String lastNameField;
+String nameField;
 
-Object     numList = strsList.stream()
-                        .map( new ToInteger() )
-                     .sorted( new ReverseCase() )
-                     .collect( Collectors.toList() );
+public boolean processRow() throws HopException {
+  // Resolve the parameter tags once. The values are the field names.
+  //
+  if (first) {
+    firstNameField = getParameter("FIRSTNAME_FIELD");
+    lastNameField = getParameter("LASTNAME_FIELD");
+    nameField = getParameter("NAME_FIELD");
+    first = false;
+  }
 
-    get( Fields.Out, "reverseOrder" ).setValue( row, numList.toString() );
+  // Read one row from the main input hop. null means that input is finished.
+  //
+  Object[] r = getRow();
+  if (r == null) {
+    setOutputDone();
+    return false;
+  }
 
-} catch (NumberFormatException ex) {
-    // Number List contains a value that cannot be converteds to an Integer.
-    rowInError = true;
-    errMsg = ex.getMessage();
-    errCnt = errCnt + 1;
-}
+  String firstName = get(Fields.In, firstNameField).getString(r);
+  String lastName = get(Fields.In, lastNameField).getString(r);
 
-if ( !rowInError ) {
-    putRow( data.outputRowMeta, row );
-} else {
-    // Output errors to the error hop. Right click on transform and choose 
"Error Handling..."
-    putError(data.outputRowMeta, row, errCnt, errMsg, "Not allowed", "DEC_0");
+  // Copy the input row and make room for the fields added on the Fields tab.
+  // When "Clear the result fields?" is checked this is a new empty row 
instead,
+  // so the input values above must be read from r, not from outputRow.
+  //
+  Object[] outputRow = createOutputRow(r, data.outputRowMeta.size());
+  get(Fields.Out, nameField).setValue(outputRow, firstName + " " + lastName);
+
+  // Send the row to the ordinary output hops.
+  //
+  putRow(data.outputRowMeta, outputRow);
+  return true;
 }
 ----
 
-The try in the code sample above tests to see if numList contains valid 
numbers.
-If the list contains a number that is not valid, putError is used to handle 
the error and direct it to the wlog: ErrorPath transform in the sample pipeline.
-The ErrorPath transform is also specified in the Target transforms tab of the 
User Define Java Class transform.
+[[classes-and-snippets]]
+=== Classes and code snippets
 
-=== Logging 
+The left-hand tree has three kinds of entry.
 
-You need to implement logging in your defined transform if you want Hop to log 
data actions from your class, such as read, write, output, or update data.
-The following code is an example of how to implement logging:
+*Classes*::
+One entry per class tab.
+Double-click a name to show that tab.
+Right-click a tab for *Add new*, *Add copy*, *Set as transform Class*, and 
*Remove class type*.
+Right-click a class in the tree to rename or delete it.
 
-[source,java]
-----
-putRow( data.outputMeta, r );
+*Code Snippets*::
+Fragments for the methods this class can call, grouped into *Common use*, *Row 
manipulation*, *transform logging*, *transform status*, *transform/Row 
listeners*, and *Uncommon use*.
+Double-click a snippet to insert it, or right-click it and choose *Show 
Sample* to open the sample in a read-only tab.
 
-if ( checkFeedback( getLinesOutput() ) ) {
-  if ( log.isBasic() ) {
-    logBasic( "Have I got rows for you! " + getLinesOutput() );
-  }
-}
-----
+*Input fields*, *Info fields*, and *Output fields*::
+The fields reaching this transform, the fields on its info hops, and the 
fields it produces.
+Double-click a field to insert a `get(Fields.In, ...)` or `get(Fields.Out, 
...)` call, or open the field and insert the getter for its data type or a 
`setValue` call.
 
-=== Class and code fragments
+[[fields]]
+== Reading and writing fields
 
-You can navigate through your defined classes along with related code snippets 
and fields through the Classes and Code Fragments panel.
-You can right-click on any item in this tree to either Delete, Rename, or Show 
Sample.
+A row is an `Object[]`.
+The matching `IRowMeta` describes the field names, types, and order.
 
-**Classes**
+`getRow()` returns the next main-input row and, on the first call, refreshes 
`data.inputRowMeta`.
+`data.outputRowMeta` is that layout plus the fields from the *Fields* tab, or 
only those fields when *Clear the result fields?* is checked.
+`getInputRowMeta()` returns the same input layout, but only once `getRow()` 
has returned a row; before that it is `null`.
 
-The Classes folder indicates what classes have corresponding code block tabs 
in the Class Code panel.
+The usual way to read and write a field is `get(Fields.In, name)`, 
`get(Fields.Out, name)`, or `get(Fields.Info, name)`.
+Each returns a helper that remembers the field index, so the name is not 
searched again on every row.
+Pass the input row to an `In` getter and the output row to an `Out` setter.
 
-**Code Snippets**
+[options="header"]
+|===
+|Method|Java type|Hop type
+
+|`getString`
+|`String`
+|String
+
+|`getLong`
+|`Long`
+|Integer
+
+|`getDouble`
+|`Double`
+|Number
+
+|`getBigDecimal`
+|`BigDecimal`
+|BigNumber
+
+|`getBoolean`
+|`Boolean`
+|Boolean
 
-The Code Snippets folder contains ready to use fragments for the most common 
User Defined Java Class operations, grouped by category.
-Click a snippet to insert it at the cursor position in the active class tab, 
or right-click it and pick Show Sample to open it in a read-only tab for 
reference.
+|`getDate`
+|`java.util.Date`
+|Date
+
+|`getTimestamp`
+|`java.sql.Timestamp`
+|Timestamp
+
+|`getBinary`
+|`byte[]`
+|Binary
+
+|`getInetAddress`
+|`java.net.InetAddress`
+|Internet Address
+
+|`getObject`
+|`Object`
+|any, including Serializable
+|===
 
-**Input Fields**
+`setValue(row, value)` writes into that row at the field's index.
 
-The Input fields folder contains any input fields you define in your code.
-While working with your defined code, you will be handling input and output 
fields.
-Many ways exist for handling input fields.
-For example, to start, examine the following description of an input row.
+The same lookup by index, which is what the helper caches for you, looks like 
this:
 
 [source,java]
 ----
+Object[] r = getRow();
+if (r == null) {
+  setOutputDone();
+  return false;
+}
+// The input layout is only known after getRow() has returned a row.
+//
 IRowMeta inputRowMeta = getInputRowMeta();
+int yearIndex = inputRowMeta.indexOfValue(getParameter("YEAR"));
+if (yearIndex < 0) {
+  throw new HopException("Year field not found in the input row, check 
parameter 'YEAR'!");
+}
+Long year = inputRowMeta.getInteger(r, yearIndex);
 ----
 
-The inputRowMeta object contains the metadata of the input row.
-It includes all the fields, their data types, lengths, names, format masks, 
and more.
-You can use this object to look up input fields.
-For example, if you want to look for a field called customer, you would use 
the following code.
+`get(Fields.In, "year").getLong(r)` is the same read.
+`get(Fields.Info, name)` uses the field layout of the first info hop Hop finds 
on this transform, not every info hop.
+For any other info hop, take the layout from that row set, as in 
<<info-and-target>>.
 
-[source,java]
-----
-IValueMeta customer = inputRowMeta.searchValueMeta("year");
-----
+[[info-and-target]]
+== Info transforms and target transforms
+
+=== Why "info" and not "source" or "input"
+
+An info transform is not another name for the main input, and renaming the tab 
to *Source transforms* or *Input transforms* would hide that.
+
+Hop already calls this kind of hop an *info* stream.
+Stream Lookup uses the same idea: one hop is the stream of rows to process, 
and another hop only supplies data that helps process those rows.
+In this transform the split is:
+
+* Hops that are *not* listed on *Info transforms* are the main input. 
`getRow()` reads them, and also reads an info hop that has not been drained yet 
(see below).
+* Hops that *are* listed there are info hops. The canvas draws an info icon on 
the hop. Read them with `findInfoRowSet(tag)` and `getRowFrom(rowSet)`.
+
+"Input" is already the main hop.
+"Source" would not say which of the two upstream hops it is.
+
+A target transform is the same idea on the way out.
+`putRow(data.outputRowMeta, row)` writes to the ordinary output hops, copied 
or distributed according to the transform's hop settings.
+A hop listed on *Target transforms* is a specific destination. The canvas 
marks it with a target icon, and the class selects it with 
`findTargetRowSet(tag)` and `putRowTo`.
 
-Because looking up field names can be slow if you need to do it for every row 
that passes through a pipeline, you could look up field names in advance in a 
first block of code, as shown in the following example:
+=== Both lists can have several rows
+
+Each row is one transform, and both tables accept as many rows as you need.
+Give every row its own tag.
+The class then picks the hop by that tag:
 
 [source,java]
 ----
-if (first) {
- yearIndex = getInputRowMeta().indexOfValue(getParameter("YEAR"));
- if (yearIndex<0) {
-   throw new HopException("Year field not found in the input row, check 
parameter 'YEAR'\!");
- }
+public boolean processRow() throws HopException {
+  if (first) {
+    first = false;
+
+    // Drain every info hop before getRow(). Layouts differ, and getRow()
+    // would otherwise mix these rows into the main input.
+    //
+    IRowSet lookup = findInfoRowSet("lookup");
+    Object[] infoRow;
+    while ((infoRow = getRowFrom(lookup)) != null) {
+      String key = lookup.getRowMeta().getString(infoRow, "key", null);
+      // keep what the main rows need from this info hop
+    }
+  }
+
+  Object[] r = getRow();
+  if (r == null) {
+    setOutputDone();
+    return false;
+  }
+  boolean accepted = Boolean.TRUE.equals(get(Fields.In, "flag").getBoolean(r));
+  r = createOutputRow(r, data.outputRowMeta.size());
+
+  IRowSet target = findTargetRowSet(accepted ? "accepted" : "rejected");
+  putRowTo(data.outputRowMeta, r, target);
+  return true;
 }
 ----
 
-To get the Integer value contained in the year field, you can then use the 
following construct.
+`getRowFrom` removes an info row set from the main input once that hop is 
exhausted, which is why the loop above has to run first.
+`findInfoRowSet` and `findTargetRowSet` throw when the tag is missing or the 
hop is not connected.
+
+The transform named in an info or target row has to run as a single copy.
+`findInfoRowSet` also accepts an info transform that is partitioned in the 
same way as this transform.
+A main input and an info hop that share an upstream transform can stall a 
local pipeline; see xref:how-to-guides/avoiding-deadlocks.adoc[Avoiding 
deadlocks].
+
+=== What the tag is
+
+The tag is a name you choose.
+It is the only identifier the class should use.
+
+The *Transform* column is the transform's name on the canvas.
+The *Tag* column is a stable alias for that hop, so renaming the transform 
means editing the table instead of the code.
+`findInfoRowSet("lookup")` resolves the tag `lookup` to the transform name 
stored on that row, then finds the row set coming from that transform.
+`findTargetRowSet("accepted")` does the same for an output row set.
+The description is only a label, shown on the hop; the class never reads it.
+
+A tag on the *Parameters* tab is a different map.
+It does not name a transform.
+See <<parameters>>.
+
+=== Why list a transform that is already on the canvas
+
+The hop and the table do different jobs.
+Drawing the hop creates the pipe.
+The table tells Hop what kind of pipe it is, and the name the class uses for 
it.
+
+The *Transform* drop-down only offers transforms that are already connected: 
upstream transforms on the info tab, downstream transforms on the target tab.
+After you pick one, Hop treats that existing hop as an info hop or a target 
hop.
+Leave the table empty and every incoming hop stays main input (`getRow()`) and 
every outgoing hop stays an ordinary output (`putRow()`).
+The table does not replace the hops, and the hops do not record the tag.
+
+Target hops stay in the output list.
+A row passed to `putRow` is therefore copied or distributed to them as well as 
to any ordinary output.
+Call `putRowTo` when that row should go to one target and not to every output.
+
+[[parameters]]
+== Parameters
+
+`getParameter("TAG")` returns the *Value* of the parameter with that tag, with 
variables resolved, or `null` when the tag is not in the table.
+A value of `+${YEAR_FIELD}+` is resolved in the pipeline before the class sees 
it.
+
+`getVariable("NAME")` and `getVariable("NAME", "default")` read pipeline 
variables.
+`setVariable("NAME", "value")` sets one.
+Those are not the parameter table.
+
+[[test-class]]
+== Test class
+
+*Test class* does not run the pipeline on the canvas, and it does not open a 
dialog that asks you to type rows.
+
+. It checks that a tab is marked as the transform class.
+. It compiles every class tab the way a real run would.
+  A compile error is shown and the test stops.
+  The message names the forbidden text when a <<blocking-code,code exclusion>> 
matched.
+. It looks up this transform in the pipeline the dialog was opened from.
+  If the transform is not there, the test stops and reports that the fields 
from the previous transforms could not be read.
+. It builds a throwaway pipeline named after this transform with `++ - 
PREVIEW++` appended:
+  * A Generate Rows transform named `## TEST DATA ##` feeds the main input.
+    It produces 10 rows.
+    The fields are the main input fields of this transform, not the fields of 
the info transforms.
+    The sample value depends on the type: `test value test value` for a 
String, zero for an Integer, Number, or BigNumber, `Y` or `true` for a Boolean, 
the current date and time for a Date, and the bytes of `ABCDEFGHIJ` for Binary.
+  * Each info transform is replaced by a Generate Rows transform of the same 
name, using that transform's fields, hopped into the class.
+  * Each target transform is replaced by a Dummy transform of the same name, 
hopped out of the class.
+    Nothing is written to the real downstream transforms.
+. It runs that pipeline and previews up to 10 rows written by this class with 
`putRow`.
+  Rows sent only with `putRowTo` go to the dummy target and are not part of 
that preview.
+  When the run reports errors, the test shows the log as well.
+
+[[error-handling]]
+== Error handling
+
+The transform supports the standard error hop.
+Right-click it on the canvas and choose *Error Handling*, then pick the 
transform that receives rejected rows and the fields that will carry the error 
count, description, field name, and code.
+That hop is not an info transform and not a target transform.
+`putError` fails when this has not been set.
 
 [source,java]
 ----
-Object[] r = getRow();
-...
-Long year = inputRowMeta().getInteger(r, yearIndex);
+try {
+  Long amount = get(Fields.In, "amount").getLong(r);
+  get(Fields.Out, "doubled").setValue(outputRow, amount * 2);
+  putRow(data.outputRowMeta, outputRow);
+} catch (Exception ex) {
+  // nr errors, description, field name, error code
+  putError(data.outputRowMeta, outputRow, 1, ex.getMessage(), "amount", 
"UDJC001");
+}
 ----
 
-To make this process easier, you can use a shortcut in the following form.
+== Logging
+
+Logging is explicit.
+`logBasic`, `logMinimal`, `logDetailed`, `logDebug`, `logRowlevel`, and 
`logError` write to this transform's log channel.
+`checkFeedback(getLinesOutput())` is true every time the feedback line 
interval is reached, which keeps a long run from logging every row:
 
 [source,java]
 ----
-Long year = get(Fields.In, "year").getInteger(r);
+putRow(data.outputRowMeta, outputRow);
+
+if (checkFeedback(getLinesOutput())) {
+  logBasic("Wrote " + getLinesOutput() + " rows");
+}
 ----
 
-This method also takes into account the index-based optimization mentioned 
above.
+`getLinesInput`, `getLinesRead`, `getLinesWritten`, `getLinesUpdated`, 
`getLinesSkipped`, `getLinesRejected`, and `getErrors` return the counters.
+`setErrors(1)` and `stopAll()` fail the pipeline from inside the class.
+`addResultFile(resultFile)` adds a file to the pipeline result.
 
+[[blocking-code]]
 == Blocking specific code
 
-As a simple security measure you can block the execution of code containing 
specific strings.
-This can be done by adding exclusions to the `codeExclusions.xml` file located 
at <Hop Installation>/plugins/transforms/janino
+Code is checked, before it is compiled, against a list of forbidden substrings.
+A match is rejected with the text that matched.
+
+Hop reads `codeExclusions.xml` in the Janino plugin folder 
(`plugins/transforms/janino`).
+The file installed there is empty, so everything is allowed.
+Add an `exclusion` element for each piece of text to block:
 
-Example:
 [source,xml]
 ----
-    <exclusions>
-        <exclusion>System.</exclusion>
-        <exclusion>HopVfs.</exclusion>
-    </exclusions>
+<exclusions>
+    <exclusion>System.</exclusion>
+    <exclusion>HopVfs.</exclusion>
+</exclusions>
 ----
+
+The check is a substring search of the class source, not a Java parser.
+`System.` also matches that text inside a comment or a string.

Reply via email to