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 f10a477714 issue #3137 : sample target hops and show data preview
icons on hop arrows (#7595)
f10a477714 is described below
commit f10a47771495187401088c164a59db098f58259a
Author: Matt Casters <[email protected]>
AuthorDate: Wed Jul 22 15:02:24 2026 +0200
issue #3137 : sample target hops and show data preview icons on hop arrows
(#7595)
Add IRowToListener so putRowTo destinations (Filter, Switch/Case, etc.) can
be sampled separately by hop. Draw clickable data-grid icons on hops near
the
source transform for full pipeline transparency.
---
.../java/org/apache/hop/core/gui/AreaOwner.java | 5 +
.../org/apache/hop/pipeline/PipelinePainter.java | 65 ++++++++
.../pipeline/canvas/PipelineCanvasSvgRenderer.java | 2 +
.../hop/pipeline/engine/EngineComponent.java | 20 +++
.../hop/pipeline/engine/IEngineComponent.java | 21 +++
.../hop/pipeline/transform/BaseTransform.java | 28 +++-
.../hop/pipeline/transform/IRowToListener.java | 46 ++++++
.../apache/hop/pipeline/transform/ITransform.java | 22 +++
.../hop/pipeline/transform/RowToAdapter.java | 42 +++++
.../hop/pipeline/transform/BaseTransformTest.java | 40 +++++
.../hop/beam/core/transform/BeamRowHandler.java | 8 +-
.../org/apache/hop/spark/core/SparkRowHandler.java | 8 +-
.../transforms/javascript/ScriptValuesDummy.java | 16 ++
.../hopgui/file/pipeline/HopGuiPipelineGraph.java | 68 +++++++-
.../ui/hopgui/file/shared/DrillDownGuiPlugin.java | 23 ++-
.../file/shared/PipelineRowSamplerHelper.java | 183 ++++++++++++++++-----
16 files changed, 534 insertions(+), 63 deletions(-)
diff --git a/engine/src/main/java/org/apache/hop/core/gui/AreaOwner.java
b/engine/src/main/java/org/apache/hop/core/gui/AreaOwner.java
index 156703c6fd..9c5f1e0434 100644
--- a/engine/src/main/java/org/apache/hop/core/gui/AreaOwner.java
+++ b/engine/src/main/java/org/apache/hop/core/gui/AreaOwner.java
@@ -56,6 +56,11 @@ public class AreaOwner {
* there are available output rows.
*/
TRANSFORM_OUTPUT_DATA(true),
+ /**
+ * Data preview icon on a pipeline hop (near the source) for rows sampled
on that hop, including
+ * target hops such as Filter true/false branches.
+ */
+ HOP_OUTPUT_DATA(true),
/** The pipeline hop decoration */
TRANSFORM_TARGET_HOP_ICON(true),
diff --git a/engine/src/main/java/org/apache/hop/pipeline/PipelinePainter.java
b/engine/src/main/java/org/apache/hop/pipeline/PipelinePainter.java
index f1fd885fd3..d1da36872b 100644
--- a/engine/src/main/java/org/apache/hop/pipeline/PipelinePainter.java
+++ b/engine/src/main/java/org/apache/hop/pipeline/PipelinePainter.java
@@ -80,6 +80,10 @@ public class PipelinePainter extends
BasePainter<PipelineHopMeta, TransformMeta>
private IPipelineEngine<PipelineMeta> pipeline;
private boolean slowTransformIndicatorEnabled;
private Map<String, RowBuffer> outputRowsMap;
+
+ /** Hop key (origin\\tdestination) → sampled rows for target hops /
putRowTo. */
+ private Map<String, RowBuffer> outputHopRowsMap;
+
private Map<String, Object> stateMap;
private boolean showingSelectedTransformMetrics = true;
@@ -131,6 +135,7 @@ public class PipelinePainter extends
BasePainter<PipelineHopMeta, TransformMeta>
this.slowTransformIndicatorEnabled = slowTransformIndicatorEnabled;
this.outputRowsMap = outputRowsMap;
+ this.outputHopRowsMap = null;
transformLogMap = null;
@@ -737,6 +742,46 @@ public class PipelinePainter extends
BasePainter<PipelineHopMeta, TransformMeta>
}
}
+ /**
+ * Draw a data-preview icon on the hop near the source transform when
hop-level samples exist. Key
+ * format matches UI hop sampling: {@code origin + "\t" + destination}.
+ */
+ private void drawHopOutputDataIndicator(
+ PipelineHopMeta pipelineHop,
+ TransformMeta fromTransform,
+ TransformMeta toTransform,
+ int x1,
+ int y1,
+ int x2,
+ int y2)
+ throws HopException {
+ if (Utils.isEmpty(outputHopRowsMap) || pipelineHop == null ||
fromTransform == null) {
+ return;
+ }
+ String hopKey = fromTransform.getName() + "\t" + toTransform.getName();
+ RowBuffer rowBuffer = outputHopRowsMap.get(hopKey);
+ if (rowBuffer == null || rowBuffer.isEmpty()) {
+ return;
+ }
+
+ // Place at ~30% of the hop length from the source transform
+ double hopDataPosition = 0.30;
+ int iconX = (int) (x1 + hopDataPosition * (x2 - x1)) - miniIconSize / 2;
+ int iconY = (int) (y1 + hopDataPosition * (y2 - y1)) - miniIconSize / 2;
+
+ gc.drawImage(EImage.DATA, iconX, iconY, magnification);
+ areaOwners.add(
+ new AreaOwner(
+ AreaType.HOP_OUTPUT_DATA,
+ iconX,
+ iconY,
+ miniIconSize,
+ miniIconSize,
+ offset,
+ pipelineHop,
+ rowBuffer));
+ }
+
private void drawTextRightAligned(String txt, int x, int y) {
int off = gc.textExtent(txt).x;
x -= off;
@@ -1288,6 +1333,10 @@ public class PipelinePainter extends
BasePainter<PipelineHopMeta, TransformMeta>
}
}
}
+
+ // Data preview icon near the source transform for hop-level samples
(target hops, etc.)
+ //
+ drawHopOutputDataIndicator(pipelineHop, fs, ts, x1, y1, x2, y2);
}
PipelinePainterExtension extension =
@@ -1429,6 +1478,22 @@ public class PipelinePainter extends
BasePainter<PipelineHopMeta, TransformMeta>
this.outputRowsMap = outputRowsMap;
}
+ /**
+ * Gets outputHopRowsMap
+ *
+ * @return hop-keyed sample buffers
+ */
+ public Map<String, RowBuffer> getOutputHopRowsMap() {
+ return outputHopRowsMap;
+ }
+
+ /**
+ * @param outputHopRowsMap hop key → RowBuffer samples for target hops
+ */
+ public void setOutputHopRowsMap(Map<String, RowBuffer> outputHopRowsMap) {
+ this.outputHopRowsMap = outputHopRowsMap;
+ }
+
/**
* Gets stateMap
*
diff --git
a/engine/src/main/java/org/apache/hop/pipeline/canvas/PipelineCanvasSvgRenderer.java
b/engine/src/main/java/org/apache/hop/pipeline/canvas/PipelineCanvasSvgRenderer.java
index 8f2f986101..fe519c4344 100644
---
a/engine/src/main/java/org/apache/hop/pipeline/canvas/PipelineCanvasSvgRenderer.java
+++
b/engine/src/main/java/org/apache/hop/pipeline/canvas/PipelineCanvasSvgRenderer.java
@@ -63,6 +63,7 @@ public final class PipelineCanvasSvgRenderer {
public boolean slowTransformIndicatorEnabled;
public double zoomFactor;
public Map<String, RowBuffer> outputRowsMap;
+ public Map<String, RowBuffer> outputHopRowsMap;
public boolean drawingBorderAroundName;
public String mouseOverName;
public Map<String, Object> stateMap;
@@ -124,6 +125,7 @@ public final class PipelineCanvasSvgRenderer {
ctx.stateMap);
pipelinePainter.setMagnification(ctx.magnification);
+ pipelinePainter.setOutputHopRowsMap(ctx.outputHopRowsMap);
pipelinePainter.setTransformLogMap(ctx.transformLogMap);
pipelinePainter.setStartHopTransform(ctx.startHopTransform);
pipelinePainter.setEndHopLocation(ctx.endHopLocation);
diff --git
a/engine/src/main/java/org/apache/hop/pipeline/engine/EngineComponent.java
b/engine/src/main/java/org/apache/hop/pipeline/engine/EngineComponent.java
index cc6b6d3867..7cb2aac259 100644
--- a/engine/src/main/java/org/apache/hop/pipeline/engine/EngineComponent.java
+++ b/engine/src/main/java/org/apache/hop/pipeline/engine/EngineComponent.java
@@ -17,7 +17,9 @@
package org.apache.hop.pipeline.engine;
+import java.util.Collections;
import java.util.Date;
+import java.util.List;
import java.util.Objects;
import lombok.Setter;
import org.apache.hop.core.exception.HopRuntimeException;
@@ -27,6 +29,7 @@ import org.apache.hop.core.logging.LogLevel;
import org.apache.hop.i18n.BaseMessages;
import org.apache.hop.pipeline.transform.BaseTransform;
import org.apache.hop.pipeline.transform.IRowListener;
+import org.apache.hop.pipeline.transform.IRowToListener;
public class EngineComponent implements IEngineComponent {
@@ -195,6 +198,23 @@ public class EngineComponent implements IEngineComponent {
"Removing a row listener to this transform is not possible as it's not
part of a running engine");
}
+ @Override
+ public void addRowToListener(IRowToListener rowToListener) {
+ throw new HopRuntimeException(
+ "Adding a row-to listener to this transform is not possible as it's
not part of a running engine");
+ }
+
+ @Override
+ public void removeRowToListener(IRowToListener rowToListener) {
+ throw new HopRuntimeException(
+ "Removing a row-to listener to this transform is not possible as it's
not part of a running engine");
+ }
+
+ @Override
+ public List<IRowToListener> getRowToListeners() {
+ return Collections.emptyList();
+ }
+
/**
* Gets linesRead
*
diff --git
a/engine/src/main/java/org/apache/hop/pipeline/engine/IEngineComponent.java
b/engine/src/main/java/org/apache/hop/pipeline/engine/IEngineComponent.java
index 9b3a74929a..39c77c9713 100644
--- a/engine/src/main/java/org/apache/hop/pipeline/engine/IEngineComponent.java
+++ b/engine/src/main/java/org/apache/hop/pipeline/engine/IEngineComponent.java
@@ -18,9 +18,11 @@
package org.apache.hop.pipeline.engine;
import java.util.Date;
+import java.util.List;
import org.apache.hop.core.logging.ILogChannel;
import org.apache.hop.core.logging.LogLevel;
import org.apache.hop.pipeline.transform.IRowListener;
+import org.apache.hop.pipeline.transform.IRowToListener;
/**
* An identifiable component of an execution engine {@link IPipelineEngine} In
a pipeline engine
@@ -135,6 +137,25 @@ public interface IEngineComponent {
void removeRowListener(IRowListener rowListener);
+ /**
+ * Add a destination-aware row listener for rows written via target hops
({@code putRowTo}).
+ *
+ * @param rowToListener the listener to add
+ */
+ void addRowToListener(IRowToListener rowToListener);
+
+ /**
+ * Remove a destination-aware row listener.
+ *
+ * @param rowToListener the listener to remove
+ */
+ void removeRowToListener(IRowToListener rowToListener);
+
+ /**
+ * @return the installed destination-aware row listeners
+ */
+ List<IRowToListener> getRowToListeners();
+
/**
* Get the execution status of the component
*
diff --git
a/engine/src/main/java/org/apache/hop/pipeline/transform/BaseTransform.java
b/engine/src/main/java/org/apache/hop/pipeline/transform/BaseTransform.java
index 5600a65210..3cac9a4d45 100644
--- a/engine/src/main/java/org/apache/hop/pipeline/transform/BaseTransform.java
+++ b/engine/src/main/java/org/apache/hop/pipeline/transform/BaseTransform.java
@@ -252,6 +252,9 @@ public class BaseTransform<Meta extends ITransformMeta,
Data extends ITransformD
/** The list of IRowListener interfaces */
protected List<IRowListener> rowListeners;
+ /** The list of destination-aware IRowToListener interfaces (target hops /
putRowTo) */
+ protected List<IRowToListener> rowToListeners;
+
/**
* Map of files that are generated or used by this transform. After
execution, these can be added
* to result. The entry to the map is the filename
@@ -428,6 +431,7 @@ public class BaseTransform<Meta extends ITransformMeta,
Data extends ITransformD
rowDistribution = transformMeta.getRowDistribution();
rowListeners = new CopyOnWriteArrayList<>();
+ rowToListeners = new CopyOnWriteArrayList<>();
resultFiles = new HashMap<>();
resultFilesLock = new ReentrantReadWriteLock();
@@ -1422,9 +1426,12 @@ public class BaseTransform<Meta extends ITransformMeta,
Data extends ITransformD
}
}
- // Do not call the row listeners for targeted rows.
- // It can cause rows with varying layouts to arrive at the same listener
without a way to keep
- // them apart.
+ // Do not call IRowListener for targeted rows: mixed layouts can arrive
without a way to keep
+ // them apart. Use IRowToListener instead — it includes the destination
IRowSet.
+ //
+ for (IRowToListener listener : rowToListeners) {
+ listener.rowWrittenTo(rowMeta, row, rowSet);
+ }
// Keep adding to terminator_rows buffer...
if (terminator && terminatorRows != null) {
@@ -3280,6 +3287,21 @@ public class BaseTransform<Meta extends ITransformMeta,
Data extends ITransformD
return Collections.unmodifiableList(rowListeners);
}
+ @Override
+ public void addRowToListener(IRowToListener rowToListener) {
+ rowToListeners.add(rowToListener);
+ }
+
+ @Override
+ public void removeRowToListener(IRowToListener rowToListener) {
+ rowToListeners.remove(rowToListener);
+ }
+
+ @Override
+ public List<IRowToListener> getRowToListeners() {
+ return Collections.unmodifiableList(rowToListeners);
+ }
+
/**
* Adds the result file.
*
diff --git
a/engine/src/main/java/org/apache/hop/pipeline/transform/IRowToListener.java
b/engine/src/main/java/org/apache/hop/pipeline/transform/IRowToListener.java
new file mode 100644
index 0000000000..1e9b60c437
--- /dev/null
+++ b/engine/src/main/java/org/apache/hop/pipeline/transform/IRowToListener.java
@@ -0,0 +1,46 @@
+/*
+ * 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.transform;
+
+import org.apache.hop.core.IRowSet;
+import org.apache.hop.core.exception.HopTransformException;
+import org.apache.hop.core.row.IRowMeta;
+
+/**
+ * Listener for rows written to a specific destination rowset (target hops).
+ *
+ * <p>Unlike {@link IRowListener}, this interface includes the destination
{@link IRowSet} so
+ * listeners can keep rows with different layouts apart (e.g. Filter
true/false branches,
+ * Switch/Case targets). Used by GUI hop sampling and other destination-aware
consumers.
+ *
+ * @see IRowListener
+ * @see BaseTransform#putRowTo(IRowMeta, Object[], IRowSet)
+ */
+public interface IRowToListener {
+
+ /**
+ * Called when a row is written to a specific destination rowset (typically
via {@code putRowTo} /
+ * {@code handlePutRowTo}).
+ *
+ * @param rowMeta the metadata of the row
+ * @param row the data of the row
+ * @param rowSet the destination rowset the row was written to
+ * @throws HopTransformException an exception that can be thrown to hard
stop the transform
+ */
+ void rowWrittenTo(IRowMeta rowMeta, Object[] row, IRowSet rowSet) throws
HopTransformException;
+}
diff --git
a/engine/src/main/java/org/apache/hop/pipeline/transform/ITransform.java
b/engine/src/main/java/org/apache/hop/pipeline/transform/ITransform.java
index 30e8f8ffed..a31892dfdc 100644
--- a/engine/src/main/java/org/apache/hop/pipeline/transform/ITransform.java
+++ b/engine/src/main/java/org/apache/hop/pipeline/transform/ITransform.java
@@ -244,6 +244,28 @@ public interface ITransform extends IVariables,
IHasLogChannel, IEngineComponent
*/
List<IRowListener> getRowListeners();
+ /**
+ * Add a destination-aware row listener for rows written via target hops
({@code putRowTo}).
+ *
+ * @param rowToListener the listener to add
+ */
+ @Override
+ void addRowToListener(IRowToListener rowToListener);
+
+ /**
+ * Remove a destination-aware row listener from this transform.
+ *
+ * @param rowToListener the listener to remove
+ */
+ @Override
+ void removeRowToListener(IRowToListener rowToListener);
+
+ /**
+ * @return a list of the installed destination-aware row listeners
+ */
+ @Override
+ List<IRowToListener> getRowToListeners();
+
/**
* @return The list of active input rowsets for the transform
*/
diff --git
a/engine/src/main/java/org/apache/hop/pipeline/transform/RowToAdapter.java
b/engine/src/main/java/org/apache/hop/pipeline/transform/RowToAdapter.java
new file mode 100644
index 0000000000..2b5af37297
--- /dev/null
+++ b/engine/src/main/java/org/apache/hop/pipeline/transform/RowToAdapter.java
@@ -0,0 +1,42 @@
+/*
+ * 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.transform;
+
+import org.apache.hop.core.IRowSet;
+import org.apache.hop.core.exception.HopTransformException;
+import org.apache.hop.core.row.IRowMeta;
+
+/**
+ * Empty adapter for {@link IRowToListener}. Exists as a convenience for
creating destination-aware
+ * row listeners.
+ *
+ * @see IRowToListener
+ * @see RowAdapter
+ */
+public class RowToAdapter implements IRowToListener {
+
+ public RowToAdapter() {
+ // Do nothing
+ }
+
+ @Override
+ public void rowWrittenTo(IRowMeta rowMeta, Object[] row, IRowSet rowSet)
+ throws HopTransformException {
+ // Do nothing
+ }
+}
diff --git
a/engine/src/test/java/org/apache/hop/pipeline/transform/BaseTransformTest.java
b/engine/src/test/java/org/apache/hop/pipeline/transform/BaseTransformTest.java
index 9410f39796..e1d096436f 100644
---
a/engine/src/test/java/org/apache/hop/pipeline/transform/BaseTransformTest.java
+++
b/engine/src/test/java/org/apache/hop/pipeline/transform/BaseTransformTest.java
@@ -17,9 +17,11 @@
package org.apache.hop.pipeline.transform;
+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.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@@ -29,6 +31,7 @@ import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -362,6 +365,43 @@ class BaseTransformTest {
}
}
+ @Test
+ void putRowToInvokesRowToListenersNotRowListeners() throws HopException {
+ BaseTransform<ITransformMeta, ITransformData> baseTransform =
+ new BaseTransform<>(
+ mockHelper.transformMeta,
+ mockHelper.iTransformMeta,
+ mockHelper.iTransformData,
+ 0,
+ mockHelper.pipelineMeta,
+ mockHelper.pipeline);
+
+ IRowMeta rowMeta = new RowMeta();
+ rowMeta.addValueMeta(new ValueMetaString("col"));
+ Object[] row = new Object[] {"value"};
+ QueueRowSet rowSet = new QueueRowSet();
+
+ IRowListener rowListener = mock(IRowListener.class);
+ baseTransform.addRowListener(rowListener);
+
+ final IRowSet[] receivedRowSet = new IRowSet[1];
+ final Object[][] receivedRow = new Object[1][];
+ baseTransform.addRowToListener(
+ (meta, data, dest) -> {
+ receivedRowSet[0] = dest;
+ receivedRow[0] = data;
+ });
+
+ baseTransform.putRowTo(rowMeta, row, rowSet);
+
+ assertSame(rowSet, receivedRowSet[0]);
+ assertSame(row, receivedRow[0]);
+ assertEquals(1L, baseTransform.getLinesWritten());
+ verify(rowListener, never()).rowWrittenEvent(any(), any());
+ verify(rowListener, never()).rowReadEvent(any(), any());
+ verify(rowListener, never()).errorRowWrittenEvent(any(), any());
+ }
+
private IRowHandler rowHandlerWithDefaultMethods() {
return new IRowHandler() {
@Override
diff --git
a/plugins/engines/beam/src/main/java/org/apache/hop/beam/core/transform/BeamRowHandler.java
b/plugins/engines/beam/src/main/java/org/apache/hop/beam/core/transform/BeamRowHandler.java
index 87dac2a9ec..cf77d28cb6 100644
---
a/plugins/engines/beam/src/main/java/org/apache/hop/beam/core/transform/BeamRowHandler.java
+++
b/plugins/engines/beam/src/main/java/org/apache/hop/beam/core/transform/BeamRowHandler.java
@@ -26,6 +26,7 @@ import org.apache.hop.core.row.IRowMeta;
import org.apache.hop.pipeline.transform.BaseTransform;
import org.apache.hop.pipeline.transform.IRowHandler;
import org.apache.hop.pipeline.transform.IRowListener;
+import org.apache.hop.pipeline.transform.IRowToListener;
/**
* Reading and writing rows from/to row sets is simpler in a Beam context
since we're using a single
@@ -104,9 +105,10 @@ public class BeamRowHandler implements IRowHandler {
@Override
public void putRowTo(IRowMeta rowMeta, Object[] row, IRowSet rowSet)
throws HopTransformException {
- List<IRowListener> rowListeners = transform.getRowListeners();
- for (IRowListener listener : rowListeners) {
- listener.rowWrittenEvent(rowMeta, row);
+ // Destination-aware listeners (not IRowListener — mixed layouts need the
target rowset)
+ List<IRowToListener> rowToListeners = transform.getRowToListeners();
+ for (IRowToListener listener : rowToListeners) {
+ listener.rowWrittenTo(rowMeta, row, rowSet);
}
rowSet.putRow(rowMeta, row);
diff --git
a/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/SparkRowHandler.java
b/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/SparkRowHandler.java
index dff5a7a393..b72ae64360 100644
---
a/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/SparkRowHandler.java
+++
b/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/SparkRowHandler.java
@@ -25,6 +25,7 @@ import org.apache.hop.core.row.IRowMeta;
import org.apache.hop.pipeline.transform.BaseTransform;
import org.apache.hop.pipeline.transform.IRowHandler;
import org.apache.hop.pipeline.transform.IRowListener;
+import org.apache.hop.pipeline.transform.IRowToListener;
/**
* Non-blocking row I/O for single-threaded mini-pipelines on Spark executors
(same idea as Beam's
@@ -105,9 +106,10 @@ public class SparkRowHandler implements IRowHandler {
@Override
public void putRowTo(IRowMeta rowMeta, Object[] row, IRowSet rowSet)
throws HopTransformException {
- List<IRowListener> rowListeners = transform.getRowListeners();
- for (IRowListener listener : rowListeners) {
- listener.rowWrittenEvent(rowMeta, row);
+ // Destination-aware listeners (not IRowListener — mixed layouts need the
target rowset)
+ List<IRowToListener> rowToListeners = transform.getRowToListeners();
+ for (IRowToListener listener : rowToListeners) {
+ listener.rowWrittenTo(rowMeta, row, rowSet);
}
rowSet.putRow(rowMeta, row);
transform.incrementLinesWritten();
diff --git
a/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDummy.java
b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDummy.java
index 2606272718..41e303e52c 100644
---
a/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDummy.java
+++
b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDummy.java
@@ -36,6 +36,7 @@ import org.apache.hop.pipeline.Pipeline;
import org.apache.hop.pipeline.PipelineMeta;
import org.apache.hop.pipeline.engine.EngineComponent;
import org.apache.hop.pipeline.transform.IRowListener;
+import org.apache.hop.pipeline.transform.IRowToListener;
import org.apache.hop.pipeline.transform.ITransform;
import org.apache.hop.pipeline.transform.ITransformData;
import org.apache.hop.pipeline.transform.ITransformFinishedListener;
@@ -63,6 +64,11 @@ public class ScriptValuesDummy implements ITransform {
// Do nothing
}
+ @Override
+ public void addRowToListener(IRowToListener rowToListener) {
+ // Do nothing
+ }
+
@Override
public void dispose() {
// Do nothing
@@ -147,6 +153,11 @@ public class ScriptValuesDummy implements ITransform {
return null;
}
+ @Override
+ public List<IRowToListener> getRowToListeners() {
+ return null;
+ }
+
@Override
public String getTransformPluginId() {
return null;
@@ -210,6 +221,11 @@ public class ScriptValuesDummy implements ITransform {
// Do nothing
}
+ @Override
+ public void removeRowToListener(IRowToListener rowToListener) {
+ // Do nothing
+ }
+
public void run() {
// Do nothing
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
index 3d6edc3ad8..6d839e21fb 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
@@ -439,6 +439,10 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
private PipelineHopMeta clickedPipelineHop;
@Getter @Setter protected Map<String, RowBuffer> outputRowsMap;
+
+ /** Hop key (origin\\tdestination) → sampled rows for target hops (Filter,
Switch/Case, …). */
+ @Getter @Setter protected Map<String, RowBuffer> outputHopRowsMap;
+
private boolean avoidContextDialog;
public void setCurrentNote(NotePadMeta ni) {
@@ -812,6 +816,10 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
done = true;
break;
+ case HOP_OUTPUT_DATA:
+ done = true;
+ break;
+
case HOP_COPY_ICON:
done = true;
break;
@@ -1141,6 +1149,11 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
return;
}
break;
+ case HOP_OUTPUT_DATA:
+ if (showHopOutputData(areaOwner)) {
+ return;
+ }
+ break;
case TRANSFORM_ICON:
if (startHopTransform != null) {
// Mouse up while drawing a hop candidate
@@ -1429,21 +1442,37 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
return showTransformOutputData(dataTransform, rowBuffer);
}
+ public boolean showHopOutputData(AreaOwner areaOwner) {
+ PipelineHopMeta hop = (PipelineHopMeta) areaOwner.getParent();
+ RowBuffer rowBuffer = (RowBuffer) areaOwner.getOwner();
+ if (hop == null || rowBuffer == null) {
+ return false;
+ }
+ String fromName = hop.getFromTransform() != null ?
hop.getFromTransform().getName() : "?";
+ String toName = hop.getToTransform() != null ?
hop.getToTransform().getName() : "?";
+ String hopLabel = fromName + " → " + toName;
+ return showOutputDataDialog(hopLabel, hopLabel, rowBuffer);
+ }
+
private boolean showTransformOutputData(TransformMeta dataTransformMeta,
RowBuffer rowBuffer) {
+ if (dataTransformMeta == null) {
+ return false;
+ }
+ return showOutputDataDialog(
+ dataTransformMeta.getName(), dataTransformMeta.getName(), rowBuffer);
+ }
+
+ private boolean showOutputDataDialog(String titleName, String messageName,
RowBuffer rowBuffer) {
if (rowBuffer != null) {
synchronized (rowBuffer.getBuffer()) {
if (!rowBuffer.isEmpty()) {
try {
String title =
BaseMessages.getString(
- PKG,
- "PipelineGraph.ViewOutput.OutputDialog.Header",
- dataTransformMeta.getName());
+ PKG, "PipelineGraph.ViewOutput.OutputDialog.Header",
titleName);
String message =
BaseMessages.getString(
- PKG,
- "PipelineGraph.ViewOutput.OutputDialog.OutputRows.Text",
- dataTransformMeta.getName());
+ PKG,
"PipelineGraph.ViewOutput.OutputDialog.OutputRows.Text", messageName);
String prefix = "";
if (pipeline != null && pipeline.getPipelineRunConfiguration() !=
null) {
@@ -3740,6 +3769,22 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
tipImage = GuiResource.getInstance().getImageData();
}
break;
+ case HOP_OUTPUT_DATA:
+ RowBuffer hopRowBuffer = (RowBuffer) areaOwner.getOwner();
+ if (hopRowBuffer != null && !hopRowBuffer.isEmpty()) {
+ PipelineHopMeta hopMeta = (PipelineHopMeta) areaOwner.getParent();
+ if (hopMeta != null
+ && hopMeta.getFromTransform() != null
+ && hopMeta.getToTransform() != null) {
+ tip.append(hopMeta.getFromTransform().getName())
+ .append(" → ")
+ .append(hopMeta.getToTransform().getName())
+ .append(Const.CR);
+ }
+ tip.append("Available output rows: " + hopRowBuffer.size());
+ tipImage = GuiResource.getInstance().getImageData();
+ }
+ break;
default:
// For plugins...
//
@@ -4030,6 +4075,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
context.slowTransformIndicatorEnabled =
propsUi.isIndicateSlowPipelineTransformsEnabled();
context.zoomFactor = propsUi.getZoomFactor();
context.outputRowsMap = outputRowsMap;
+ context.outputHopRowsMap = outputHopRowsMap;
context.drawingBorderAroundName = propsUi.isBorderDrawnAroundCanvasNames();
context.mouseOverName = mouseOverName;
context.stateMap = stateMap;
@@ -4090,6 +4136,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
stateMap);
pipelinePainter.setMagnification((float) (magnification *
PropsUi.getNativeZoomFactor()));
+ pipelinePainter.setOutputHopRowsMap(outputHopRowsMap);
pipelinePainter.setTransformLogMap(transformLogMap);
pipelinePainter.setStartHopTransform(startHopTransform);
pipelinePainter.setEndHopLocation(endHopLocation);
@@ -5088,7 +5135,8 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
private void addRowsSamplerToPipeline(IPipelineEngine<PipelineMeta>
pipeline) {
try {
outputRowsMap = new HashMap<>();
- PipelineRowSamplerHelper.addRowSamplersToPipeline(pipeline,
outputRowsMap);
+ outputHopRowsMap = new HashMap<>();
+ PipelineRowSamplerHelper.addRowSamplersToPipeline(pipeline,
outputRowsMap, outputHopRowsMap);
} catch (Exception e) {
// Ignore: run config not local or sample type not set
}
@@ -5104,7 +5152,8 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
return;
}
outputRowsMap = new HashMap<>();
- PipelineRowSamplerHelper.addRowSamplersToPipeline(pipeline, outputRowsMap);
+ outputHopRowsMap = new HashMap<>();
+ PipelineRowSamplerHelper.addRowSamplersToPipeline(pipeline, outputRowsMap,
outputHopRowsMap);
}
public void showSaveFileMessage() {
@@ -5429,8 +5478,11 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
// wasn't open)
Map<String, RowBuffer> existingBuffers =
DrillDownGuiPlugin.getDataSnifferBuffersForPipeline(runningPipeline.getLogChannelId());
+ Map<String, RowBuffer> existingHopBuffers =
+
DrillDownGuiPlugin.getDataSnifferHopBuffersForPipeline(runningPipeline.getLogChannelId());
if (existingBuffers != null) {
outputRowsMap = existingBuffers;
+ outputHopRowsMap = existingHopBuffers != null ? existingHopBuffers : new
HashMap<>();
} else {
addRowsSamplerToAttachedPipeline();
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/DrillDownGuiPlugin.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/DrillDownGuiPlugin.java
index 5ef8525bb3..a33b656b19 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/DrillDownGuiPlugin.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/DrillDownGuiPlugin.java
@@ -86,6 +86,7 @@ public class DrillDownGuiPlugin {
runningPipelines.clear();
runningWorkflows.clear();
dataSnifferBuffersByLogChannelId.clear();
+ dataSnifferHopBuffersByLogChannelId.clear();
}
/**
@@ -96,6 +97,13 @@ public class DrillDownGuiPlugin {
private static final Map<String, Map<String, RowBuffer>>
dataSnifferBuffersByLogChannelId =
new ConcurrentHashMap<>();
+ /**
+ * Per-run hop-level sniffer buffers (logChannelId -> hop key -> RowBuffer)
for target hops
+ * ({@code putRowTo}).
+ */
+ private static final Map<String, Map<String, RowBuffer>>
dataSnifferHopBuffersByLogChannelId =
+ new ConcurrentHashMap<>();
+
/**
* Attach row listeners to a pipeline at start so we capture output rows for
debugging. Uses the
* pipeline's run configuration (sample type and size in GUI) when
available; otherwise no
@@ -110,7 +118,10 @@ public class DrillDownGuiPlugin {
Map<String, RowBuffer> buffers =
dataSnifferBuffersByLogChannelId.computeIfAbsent(
logChannelId, k -> new ConcurrentHashMap<>());
- PipelineRowSamplerHelper.addRowSamplersToPipeline(pipeline, buffers);
+ Map<String, RowBuffer> hopBuffers =
+ dataSnifferHopBuffersByLogChannelId.computeIfAbsent(
+ logChannelId, k -> new ConcurrentHashMap<>());
+ PipelineRowSamplerHelper.addRowSamplersToPipeline(pipeline, buffers,
hopBuffers);
}
/**
@@ -121,6 +132,16 @@ public class DrillDownGuiPlugin {
return dataSnifferBuffersByLogChannelId.get(logChannelId);
}
+ /**
+ * Return hop-level data sniffer buffers for a pipeline run (if any).
+ *
+ * @param logChannelId pipeline log channel id
+ * @return hop key → RowBuffer, or null
+ */
+ public static Map<String, RowBuffer>
getDataSnifferHopBuffersForPipeline(String logChannelId) {
+ return dataSnifferHopBuffersByLogChannelId.get(logChannelId);
+ }
+
// ==================== TRANSFORM CONTEXT ====================
@GuiContextAction(
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/PipelineRowSamplerHelper.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/PipelineRowSamplerHelper.java
index f8bd920cfa..632858f014 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/PipelineRowSamplerHelper.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/PipelineRowSamplerHelper.java
@@ -22,8 +22,11 @@ import java.util.Collections;
import java.util.LinkedList;
import java.util.Map;
import java.util.Random;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.lang3.StringUtils;
import org.apache.hop.core.Const;
+import org.apache.hop.core.IRowSet;
import org.apache.hop.core.exception.HopTransformException;
import org.apache.hop.core.exception.HopValueException;
import org.apache.hop.core.row.IRowMeta;
@@ -35,16 +38,49 @@ import
org.apache.hop.pipeline.engines.IMeasuringLocalPipelineRunConfiguration;
import org.apache.hop.pipeline.engines.local.LocalPipelineEngine;
import
org.apache.hop.pipeline.engines.local.LocalPipelineRunConfiguration.SampleType;
import org.apache.hop.pipeline.transform.RowAdapter;
+import org.apache.hop.pipeline.transform.RowToAdapter;
/**
* Helper to attach row listeners to a pipeline so output rows are sampled
into a target map (e.g.
* for execution preview or drill-down). Uses the pipeline's run configuration
(sample type and size
* in GUI).
+ *
+ * <p>Supports both transform-level samples ({@code IRowListener} / {@code
putRow}) and hop-level
+ * samples ({@code IRowToListener} / {@code putRowTo}) for target hops such as
Filter and
+ * Switch/Case.
*/
public final class PipelineRowSamplerHelper {
+ private static final String HOP_KEY_SEPARATOR = "\t";
+
private PipelineRowSamplerHelper() {}
+ /**
+ * Build a stable map key for a hop from origin transform name to
destination transform name.
+ *
+ * @param originTransformName source transform name
+ * @param destinationTransformName target transform name
+ * @return hop key used in hop sample maps
+ */
+ public static String hopKey(String originTransformName, String
destinationTransformName) {
+ return Const.NVL(originTransformName, "")
+ + HOP_KEY_SEPARATOR
+ + Const.NVL(destinationTransformName, "");
+ }
+
+ /**
+ * Build a hop key from a rowset's origin/destination names.
+ *
+ * @param rowSet the destination rowset
+ * @return hop key, or null if rowSet is null
+ */
+ public static String hopKey(IRowSet rowSet) {
+ if (rowSet == null) {
+ return null;
+ }
+ return hopKey(rowSet.getOriginTransformName(),
rowSet.getDestinationTransformName());
+ }
+
/**
* Attach row listeners to a pipeline so output rows are sampled into {@code
targetMap} (transform
* name → RowBuffer). Uses the pipeline's run configuration (sample type and
size in GUI). Does
@@ -55,6 +91,21 @@ public final class PipelineRowSamplerHelper {
*/
public static void addRowSamplersToPipeline(
IPipelineEngine<PipelineMeta> pipeline, Map<String, RowBuffer>
targetMap) {
+ addRowSamplersToPipeline(pipeline, targetMap, null);
+ }
+
+ /**
+ * Attach transform-level and optional hop-level row samplers.
+ *
+ * @param pipeline the pipeline engine
+ * @param targetMap transform name → RowBuffer (for {@code putRow} / {@code
IRowListener})
+ * @param hopTargetMap hop key → RowBuffer (for {@code putRowTo} / {@code
IRowToListener}); may be
+ * null to skip hop sampling
+ */
+ public static void addRowSamplersToPipeline(
+ IPipelineEngine<PipelineMeta> pipeline,
+ Map<String, RowBuffer> targetMap,
+ Map<String, RowBuffer> hopTargetMap) {
if (pipeline == null || targetMap == null) {
return;
}
@@ -85,60 +136,102 @@ public final class PipelineRowSamplerHelper {
return;
}
final Random random = new Random();
+ // Per-key counters for Random sampling (transform name or hop key)
+ final Map<String, AtomicInteger> rowCounters = new ConcurrentHashMap<>();
for (final String transformName : meta.getTransformNames()) {
IEngineComponent component = pipeline.findComponent(transformName, 0);
- if (component != null) {
- component.addRowListener(
- new RowAdapter() {
- int nrRows = 0;
+ if (component == null) {
+ continue;
+ }
+
+ // Transform-level samples (main putRow path)
+ component.addRowListener(
+ new RowAdapter() {
+ @Override
+ public void rowWrittenEvent(IRowMeta rowMeta, Object[] row)
+ throws HopTransformException {
+ applySample(
+ targetMap,
+ transformName,
+ rowMeta,
+ row,
+ sampleType,
+ sampleSize,
+ random,
+ rowCounters);
+ }
+ });
+ // Hop-level samples (putRowTo / target hops)
+ if (hopTargetMap != null) {
+ component.addRowToListener(
+ new RowToAdapter() {
@Override
- public void rowWrittenEvent(IRowMeta rowMeta, Object[] row)
+ public void rowWrittenTo(IRowMeta rowMeta, Object[] row, IRowSet
rowSet)
throws HopTransformException {
- RowBuffer rowBuffer = targetMap.get(transformName);
- if (rowBuffer == null) {
- rowBuffer = new RowBuffer(rowMeta);
- if (sampleType == SampleType.Last) {
- rowBuffer.setBuffer(Collections.synchronizedList(new
LinkedList<>()));
- } else {
- rowBuffer.setBuffer(Collections.synchronizedList(new
ArrayList<>()));
- }
- targetMap.put(transformName, rowBuffer);
- }
- try {
- row = rowMeta.cloneRow(row);
- } catch (HopValueException e) {
- throw new HopTransformException("Error copying row for
preview", e);
- }
- switch (sampleType) {
- case First:
- if (rowBuffer.size() < sampleSize) {
- rowBuffer.addRow(row);
- }
- break;
- case Last:
- rowBuffer.addRow(0, row);
- if (rowBuffer.size() > sampleSize) {
- rowBuffer.removeRow(rowBuffer.size() - 1);
- }
- break;
- case Random:
- nrRows++;
- if (rowBuffer.size() < sampleSize) {
- rowBuffer.addRow(row);
- } else {
- int randomIndex = random.nextInt(nrRows);
- if (randomIndex < sampleSize) {
- rowBuffer.setRow(randomIndex, row);
- }
- }
- break;
- default:
- break;
+ String key = hopKey(rowSet);
+ if (StringUtils.isEmpty(key) || key.equals(HOP_KEY_SEPARATOR))
{
+ return;
}
+ applySample(
+ hopTargetMap, key, rowMeta, row, sampleType, sampleSize,
random, rowCounters);
}
});
}
}
}
+
+ /** Apply First/Last/Random sampling into the map. */
+ private static void applySample(
+ Map<String, RowBuffer> targetMap,
+ String key,
+ IRowMeta rowMeta,
+ Object[] row,
+ SampleType sampleType,
+ int sampleSize,
+ Random random,
+ Map<String, AtomicInteger> rowCounters)
+ throws HopTransformException {
+ RowBuffer rowBuffer = targetMap.get(key);
+ if (rowBuffer == null) {
+ rowBuffer = new RowBuffer(rowMeta);
+ if (sampleType == SampleType.Last) {
+ rowBuffer.setBuffer(Collections.synchronizedList(new LinkedList<>()));
+ } else {
+ rowBuffer.setBuffer(Collections.synchronizedList(new ArrayList<>()));
+ }
+ targetMap.put(key, rowBuffer);
+ }
+ try {
+ row = rowMeta.cloneRow(row);
+ } catch (HopValueException e) {
+ throw new HopTransformException("Error copying row for preview", e);
+ }
+ switch (sampleType) {
+ case First:
+ if (rowBuffer.size() < sampleSize) {
+ rowBuffer.addRow(row);
+ }
+ break;
+ case Last:
+ rowBuffer.addRow(0, row);
+ if (rowBuffer.size() > sampleSize) {
+ rowBuffer.removeRow(rowBuffer.size() - 1);
+ }
+ break;
+ case Random:
+ int nrRows = rowCounters.computeIfAbsent(key, k -> new
AtomicInteger(0)).incrementAndGet();
+ if (rowBuffer.size() < sampleSize) {
+ rowBuffer.addRow(row);
+ } else {
+ int randomIndex = random.nextInt(nrRows);
+ if (randomIndex < sampleSize) {
+ rowBuffer.setRow(randomIndex, row);
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ }
}