You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hop.apache.org by ha...@apache.org on 2026/04/09 09:24:07 UTC
(hop) branch main updated: fallback to getXml and loadXml if they still exist, fixes #6972 (#6976)
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 8e7e22a6bb fallback to getXml and loadXml if they still exist, fixes #6972 (#6976)
8e7e22a6bb is described below
commit 8e7e22a6bba690833d97664f109c6d6318e56882
Author: Hans Van Akelyen <ha...@gmail.com>
AuthorDate: Thu Apr 9 11:24:01 2026 +0200
fallback to getXml and loadXml if they still exist, fixes #6972 (#6976)
---
.../hop/metadata/serializer/xml/ILegacyXml.java | 13 ++++
.../metadata/serializer/xml/XmlMetadataUtil.java | 74 ++++++++++++++++++-
.../serializer/xml/XmlMetadataUtilTest.java | 7 ++
.../hop/pipeline/transform/BaseTransformMeta.java | 57 ++++++++++++++-
.../hop/pipeline/transform/LegacyOnlyTestMeta.java | 84 ++++++++++++++++++++++
.../transform/LegacyTransformXmlBridgeTest.java | 50 +++++++++++++
6 files changed, 282 insertions(+), 3 deletions(-)
diff --git a/core/src/main/java/org/apache/hop/metadata/serializer/xml/ILegacyXml.java b/core/src/main/java/org/apache/hop/metadata/serializer/xml/ILegacyXml.java
index a96c4df17a..8ada630a8a 100644
--- a/core/src/main/java/org/apache/hop/metadata/serializer/xml/ILegacyXml.java
+++ b/core/src/main/java/org/apache/hop/metadata/serializer/xml/ILegacyXml.java
@@ -19,6 +19,7 @@
package org.apache.hop.metadata.serializer.xml;
import org.apache.hop.core.exception.HopException;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
import org.w3c.dom.Node;
public interface ILegacyXml {
@@ -30,4 +31,16 @@ public interface ILegacyXml {
* @throws HopException In case there's an unexpected error in the old XML format.
*/
void convertLegacyXml(Node node) throws HopException;
+
+ /**
+ * Same as {@link #convertLegacyXml(Node)} with access to the metadata provider used while loading
+ * pipeline/workflow XML. Default delegates to {@link #convertLegacyXml(Node)}.
+ *
+ * @param node The node containing the object properties (often the full transform/action node)
+ * @param metadataProvider May be null when not available
+ */
+ default void convertLegacyXml(Node node, IHopMetadataProvider metadataProvider)
+ throws HopException {
+ convertLegacyXml(node);
+ }
}
diff --git a/core/src/main/java/org/apache/hop/metadata/serializer/xml/XmlMetadataUtil.java b/core/src/main/java/org/apache/hop/metadata/serializer/xml/XmlMetadataUtil.java
index 12c8a2694c..ab867ec744 100644
--- a/core/src/main/java/org/apache/hop/metadata/serializer/xml/XmlMetadataUtil.java
+++ b/core/src/main/java/org/apache/hop/metadata/serializer/xml/XmlMetadataUtil.java
@@ -58,6 +58,16 @@ public class XmlMetadataUtil {
private static final String FQN_ACTION_FACTORY =
"org.apache.hop.workflow.action.IAction$ActionFactory";
+ private static final String BASE_TRANSFORM_META_CLASS =
+ "org.apache.hop.pipeline.transform.BaseTransformMeta";
+
+ /**
+ * Prevents infinite recursion when {@code getXml()} on a transform delegates back to {@link
+ * #serializeObjectToXml(Object)}.
+ */
+ private static final ThreadLocal<Integer> LEGACY_TRANSFORM_GET_XML_DEPTH =
+ ThreadLocal.withInitial(() -> 0);
+
private XmlMetadataUtil() {
// Hides the public constructor
}
@@ -88,6 +98,41 @@ public class XmlMetadataUtil {
return null;
}
+ /**
+ * @return true if the class (or a superclass) declares at least one {@link HopMetadataProperty}
+ * on a field or on a {@code get*()} method, using the same rules as XML serialization.
+ */
+ public static boolean hasHopMetadataSerializableProperties(Class<?> clazz) {
+ Set<String> serializeOnly = Set.of();
+ Set<String> childKeysToIgnore = Set.of();
+ List<Field> fields =
+ ReflectionUtil.findAllFields(clazz, new MetadataPropertyKeyFunction(), false);
+ for (Field field : fields) {
+ if (getValidFieldAnnotation(field, serializeOnly, childKeysToIgnore) != null) {
+ return true;
+ }
+ }
+ for (Method getter : ReflectionUtil.findAllMethods(clazz, "get")) {
+ if (getter.getAnnotation(HopMetadataProperty.class) != null) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean isAssignableFromBaseTransformMeta(Class<?> clazz) {
+ try {
+ ClassLoader cl = clazz.getClassLoader();
+ if (cl == null) {
+ cl = XmlMetadataUtil.class.getClassLoader();
+ }
+ Class<?> base = Class.forName(BASE_TRANSFORM_META_CLASS, false, cl);
+ return base.isAssignableFrom(clazz);
+ } catch (ClassNotFoundException | LinkageError e) {
+ return false;
+ }
+ }
+
/**
* This method looks at the fields in the class of the provided parentObject. It then sees which
* fields have annotation HopMetadataProperty and proceeds to serialize the values of those fields
@@ -186,7 +231,32 @@ public class XmlMetadataUtil {
xml.append(XmlHandler.closeTag(wrapper.tag()));
}
- return xml.toString();
+ String result = xml.toString();
+ if (StringUtils.isNotBlank(result)) {
+ return result;
+ }
+ if (!hasHopMetadataSerializableProperties(objectClass)
+ && isAssignableFromBaseTransformMeta(objectClass)
+ && LEGACY_TRANSFORM_GET_XML_DEPTH.get() == 0) {
+ try {
+ LEGACY_TRANSFORM_GET_XML_DEPTH.set(1);
+ Method m = parentObject.getClass().getMethod("getXml");
+ m.trySetAccessible();
+ if (m.getParameterCount() == 0
+ && String.class.equals(m.getReturnType())
+ && !BASE_TRANSFORM_META_CLASS.equals(m.getDeclaringClass().getName())) {
+ Object out = m.invoke(parentObject);
+ if (out instanceof String legacy && StringUtils.isNotEmpty(legacy)) {
+ return legacy;
+ }
+ }
+ } catch (ReflectiveOperationException e) {
+ throw new HopException("Unable to invoke legacy getXml() on " + objectClass.getName(), e);
+ } finally {
+ LEGACY_TRANSFORM_GET_XML_DEPTH.set(0);
+ }
+ }
+ return result;
}
private static void serializeFieldValueToXml(
@@ -740,7 +810,7 @@ public class XmlMetadataUtil {
if (object instanceof ILegacyXml legacyXml) {
try {
- legacyXml.convertLegacyXml(node);
+ legacyXml.convertLegacyXml(node, metadataProvider);
} catch (HopException e) {
throw new HopXmlException("Error de-serializing legacy XML", e);
}
diff --git a/core/src/test/java/org/apache/hop/metadata/serializer/xml/XmlMetadataUtilTest.java b/core/src/test/java/org/apache/hop/metadata/serializer/xml/XmlMetadataUtilTest.java
index 796c681f21..48d5d25613 100644
--- a/core/src/test/java/org/apache/hop/metadata/serializer/xml/XmlMetadataUtilTest.java
+++ b/core/src/test/java/org/apache/hop/metadata/serializer/xml/XmlMetadataUtilTest.java
@@ -18,6 +18,7 @@
package org.apache.hop.metadata.serializer.xml;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -214,6 +215,12 @@ class XmlMetadataUtilTest {
assertEquals(withCopy.getHops().size(), listRef.getHops().size());
}
+ @Test
+ void hasHopMetadataSerializablePropertiesReflectsAnnotatedFields() {
+ assertTrue(XmlMetadataUtil.hasHopMetadataSerializableProperties(MetaData.class));
+ assertFalse(XmlMetadataUtil.hasHopMetadataSerializableProperties(Object.class));
+ }
+
@Test
void testListReferenceSerializationWithEmptyReference() throws Exception {
String xml =
diff --git a/engine/src/main/java/org/apache/hop/pipeline/transform/BaseTransformMeta.java b/engine/src/main/java/org/apache/hop/pipeline/transform/BaseTransformMeta.java
index 2e60fb5bdd..1facaafe89 100644
--- a/engine/src/main/java/org/apache/hop/pipeline/transform/BaseTransformMeta.java
+++ b/engine/src/main/java/org/apache/hop/pipeline/transform/BaseTransformMeta.java
@@ -18,12 +18,16 @@
package org.apache.hop.pipeline.transform;
import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
+import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.hop.core.ICheckResult;
import org.apache.hop.core.IHopAttribute;
@@ -68,6 +72,14 @@ import org.w3c.dom.Node;
public class BaseTransformMeta<Main extends ITransform, Data extends ITransformData>
implements ITransformMeta, Cloneable {
+ /**
+ * Prevents infinite recursion when {@link #loadXml(Node, IHopMetadataProvider)} calls {@code
+ * super.loadXml}, which runs {@link XmlMetadataUtil#deSerializeFromXml} and reaches {@link
+ * #convertLegacyXml(Node, IHopMetadataProvider)} again for the same instance.
+ */
+ private static final Set<Object> LEGACY_LOAD_XML_REENTRANT_GUARD =
+ Collections.newSetFromMap(new IdentityHashMap<>());
+
public static final ILoggingObject loggingObject =
new SimpleLoggingObject("Transform metadata", LoggingObjectType.TRANSFORM_META, null);
@@ -897,8 +909,51 @@ public class BaseTransformMeta<Main extends ITransform, Data extends ITransformD
return null;
}
+ /**
+ * When a transform has no {@link org.apache.hop.metadata.api.HopMetadataProperty} fields,
+ * pipeline XML loaded through {@link org.apache.hop.metadata.serializer.xml.XmlMetadataUtil}
+ * never calls a custom {@code loadXml} unless we bridge here. External plugins that override
+ * {@link #loadXml(Node, IHopMetadataProvider)} are handled by this path.
+ *
+ * <p>The default {@link #loadXml(Node, IHopMetadataProvider)} only re-enters {@link
+ * XmlMetadataUtil#deSerializeFromXml}; calling it from here would recurse with {@link
+ * #convertLegacyXml(Node, IHopMetadataProvider)} (see {@link #declaresOwnLoadXml()}).
+ */
+ @Override
+ public void convertLegacyXml(Node node, IHopMetadataProvider metadataProvider)
+ throws HopException {
+ if (XmlMetadataUtil.hasHopMetadataSerializableProperties(getClass())) {
+ convertLegacyXml(node);
+ return;
+ }
+ if (declaresOwnLoadXml()) {
+ if (!LEGACY_LOAD_XML_REENTRANT_GUARD.add(this)) {
+ return;
+ }
+ try {
+ loadXml(node, metadataProvider);
+ } finally {
+ LEGACY_LOAD_XML_REENTRANT_GUARD.remove(this);
+ }
+ }
+ }
+
+ /**
+ * @return true if this class overrides {@link #loadXml(Node, IHopMetadataProvider)}; the base
+ * implementation must not be invoked from {@link #convertLegacyXml(Node,
+ * IHopMetadataProvider)} because it only runs annotation deserialization again.
+ */
+ private boolean declaresOwnLoadXml() {
+ try {
+ Method m = getClass().getMethod("loadXml", Node.class, IHopMetadataProvider.class);
+ return !BaseTransformMeta.class.equals(m.getDeclaringClass());
+ } catch (NoSuchMethodException e) {
+ return false;
+ }
+ }
+
@Override
public void convertLegacyXml(Node node) throws HopException {
- // Nothing by default
+ // Migrated transforms may override to read leftover legacy tags after annotation loading.
}
}
diff --git a/engine/src/test/java/org/apache/hop/pipeline/transform/LegacyOnlyTestMeta.java b/engine/src/test/java/org/apache/hop/pipeline/transform/LegacyOnlyTestMeta.java
new file mode 100644
index 0000000000..a6849c7e7a
--- /dev/null
+++ b/engine/src/test/java/org/apache/hop/pipeline/transform/LegacyOnlyTestMeta.java
@@ -0,0 +1,84 @@
+/*
+ * 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 java.util.List;
+import org.apache.hop.core.CheckResult;
+import org.apache.hop.core.ICheckResult;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.exception.HopTransformException;
+import org.apache.hop.core.exception.HopXmlException;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.transforms.dummy.Dummy;
+import org.apache.hop.pipeline.transforms.dummy.DummyData;
+import org.w3c.dom.Node;
+
+/**
+ * Public test double mimicking an external plugin: no {@code @HopMetadataProperty}, only {@code
+ * getXml}/{@code loadXml}. Must be top-level public so {@link
+ * org.apache.hop.metadata.serializer.xml.XmlMetadataUtil} can reflectively invoke {@code getXml}.
+ */
+public class LegacyOnlyTestMeta extends BaseTransformMeta<Dummy, DummyData> {
+ String secret = "";
+
+ public String getSecret() {
+ return secret;
+ }
+
+ @Override
+ public String getXml() throws HopException {
+ return XmlHandler.addTagValue("secret", secret);
+ }
+
+ @Override
+ public void loadXml(Node transformNode, IHopMetadataProvider metadataProvider)
+ throws HopXmlException {
+ secret = XmlHandler.getTagValue(transformNode, "secret");
+ }
+
+ @Override
+ public void setDefault() {}
+
+ @Override
+ public void getFields(
+ IRowMeta inputRowMeta,
+ String name,
+ IRowMeta[] info,
+ TransformMeta nextTransform,
+ IVariables variables,
+ IHopMetadataProvider metadataProvider)
+ throws HopTransformException {}
+
+ @Override
+ public void check(
+ List<ICheckResult> remarks,
+ PipelineMeta pipelineMeta,
+ TransformMeta transformMeta,
+ IRowMeta prev,
+ String[] input,
+ String[] output,
+ IRowMeta info,
+ IVariables variables,
+ IHopMetadataProvider metadataProvider) {
+ remarks.add(new CheckResult(ICheckResult.TYPE_RESULT_OK, "ok", transformMeta));
+ }
+}
diff --git a/engine/src/test/java/org/apache/hop/pipeline/transform/LegacyTransformXmlBridgeTest.java b/engine/src/test/java/org/apache/hop/pipeline/transform/LegacyTransformXmlBridgeTest.java
new file mode 100644
index 0000000000..a540c19973
--- /dev/null
+++ b/engine/src/test/java/org/apache/hop/pipeline/transform/LegacyTransformXmlBridgeTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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 static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.metadata.serializer.xml.XmlMetadataUtil;
+import org.junit.jupiter.api.Test;
+import org.w3c.dom.Node;
+
+class LegacyTransformXmlBridgeTest {
+
+ @Test
+ void serializeUsesOverriddenGetXmlWhenNoMetadataProperties() throws HopException {
+ LegacyOnlyTestMeta meta = new LegacyOnlyTestMeta();
+ meta.secret = "external-plugin";
+ String xml = XmlMetadataUtil.serializeObjectToXml(meta);
+ assertTrue(xml.contains("external-plugin"));
+ }
+
+ @Test
+ void deserializeInvokesLoadXmlForLegacyOnlyTransform() throws HopException {
+ String fragment =
+ "<transform><name>x</name><type>Dummy</type><secret>from-xml</secret></transform>";
+ Node transformNode = XmlHandler.getSubNode(XmlHandler.loadXmlString(fragment), "transform");
+ LegacyOnlyTestMeta meta = new LegacyOnlyTestMeta();
+ XmlMetadataUtil.deSerializeFromXml(
+ transformNode, LegacyOnlyTestMeta.class, meta, new MemoryMetadataProvider());
+ assertEquals("from-xml", meta.getSecret());
+ }
+}