This is an automated email from the ASF dual-hosted git repository.
mattcasters 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 1d66dd648e issue #8662 - Exception when switching projects (#8666)
1d66dd648e is described below
commit 1d66dd648e56a0b47e10988cd0dbdbd37a7a2a0f
Author: Matt Casters <[email protected]>
AuthorDate: Thu Oct 1 20:55:02 2026 +0200
issue #8662 - Exception when switching projects (#8666)
---
.../metadata/serializer/xml/XmlMetadataUtil.java | 13 +++
.../xml/Issue8662MapMapNullValueTest.java | 102 +++++++++++++++++++++
2 files changed, 115 insertions(+)
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 001d32cbe2..a317883a10 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
@@ -567,6 +567,12 @@ public class XmlMetadataUtil {
private static void serializeMapKeyToXml(StringBuilder xml, String keyTag,
Map.Entry<?, ?> entry)
throws HopException {
Object keyObject = entry.getKey();
+ if (keyObject == null) {
+ // A null key has no type to read the key tag from. Write the bare
wrapper so the entry is
+ // not lost on the round trip.
+ xml.append(XmlHandler.addTagValue(Const.NVL(keyTag, "key"), (String)
null));
+ return;
+ }
Class<?> keyClass = keyObject.getClass();
HopMetadataProperty keyProperty =
keyClass.getAnnotation(HopMetadataProperty.class);
String keyTagKey = Const.NVL(keyTag, "key");
@@ -580,6 +586,13 @@ public class XmlMetadataUtil {
private static void serializeMapValueToXml(
StringBuilder xml, String valueTag, Map.Entry<?, ?> entry) throws
HopException {
Object valueObject = entry.getValue();
+ if (valueObject == null) {
+ // An empty value is legitimate: an attribute written as <value/> reads
back as null, and
+ // setAttribute(group, key, null) stores one. serializeObjectToXml
already renders a null
+ // object as an empty tag, so route through it instead of dereferencing
valueObject.
+ xml.append(XmlHandler.addTagValue(Const.NVL(valueTag, "value"), (String)
null));
+ return;
+ }
Class<?> valueClass = valueObject.getClass();
HopMetadataProperty keyProperty =
valueClass.getAnnotation(HopMetadataProperty.class);
String valueTagKey = Const.NVL(valueTag, "value");
diff --git
a/core/src/test/java/org/apache/hop/metadata/serializer/xml/Issue8662MapMapNullValueTest.java
b/core/src/test/java/org/apache/hop/metadata/serializer/xml/Issue8662MapMapNullValueTest.java
new file mode 100644
index 0000000000..b0b5eb87d3
--- /dev/null
+++
b/core/src/test/java/org/apache/hop/metadata/serializer/xml/Issue8662MapMapNullValueTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.metadata.serializer.xml;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.metadata.serializer.xml.classes.WithMapMap;
+import org.junit.jupiter.api.Test;
+import org.w3c.dom.Node;
+
+/**
+ * Reproduction for #8662: a {@code Map<String, Map<String, String>>} whose
inner value is empty
+ * round-trips into a map holding a null value, and serializing that map again
throws an NPE.
+ */
+class Issue8662MapMapNullValueTest {
+
+ /**
+ * An attribute written with an empty value - {@code <value/>} rather than
{@code
+ * <value>x</value>}.
+ */
+ private static final String XML_WITH_EMPTY_ATTRIBUTE_VALUE =
+ "<hop>"
+ + "<attributes>"
+ + " <group><name>group1</name>"
+ + " <attribute><key>empty</key><value/></attribute>"
+ + " </group>"
+ + "</attributes>"
+ + "</hop>";
+
+ @Test
+ void emptyAttributeValueDoesNotBreakSerialization() throws Exception {
+ Node node = XmlHandler.loadXmlString(XML_WITH_EMPTY_ATTRIBUTE_VALUE,
"hop");
+
+ WithMapMap deserialized =
+ XmlMetadataUtil.deSerializeFromXml(node, WithMapMap.class, new
MemoryMetadataProvider());
+
+ Map<String, String> group = deserialized.getAttributesMap().get("group1");
+ assertNull(group.get("empty"), "the empty value reads back as null");
+
+ // This is the NPE from the issue: serializeMapValueToXml calls
valueObject.getClass() on the
+ // null entry value.
+ assertDoesNotThrow(() ->
XmlMetadataUtil.serializeObjectToXml(deserialized));
+ }
+
+ /** A null value put in by hand, the shape setAttribute(group, key, null)
produces. */
+ @Test
+ void nullValueInAGroupDoesNotBreakSerialization() {
+ WithMapMap mapMap = new WithMapMap();
+ Map<String, String> group = new HashMap<>();
+ group.put("empty", null);
+ mapMap.getAttributesMap().put("group1", group);
+
+ assertDoesNotThrow(() -> XmlMetadataUtil.serializeObjectToXml(mapMap));
+ }
+
+ /** The serializer should keep a null value as an empty tag rather than
dropping the attribute. */
+ @Test
+ void nullValueIsWrittenAsAnEmptyValueTag() throws Exception {
+ WithMapMap mapMap = new WithMapMap();
+ Map<String, String> group = new HashMap<>();
+ group.put("empty", null);
+ mapMap.getAttributesMap().put("group1", group);
+
+ String xml = XmlMetadataUtil.serializeObjectToXml(mapMap);
+
+ assertEquals(
+ 1,
+ countOccurrences(xml, "<attribute>"),
+ "the attribute must survive serialization: " + xml);
+ }
+
+ private static int countOccurrences(String haystack, String needle) {
+ int count = 0;
+ int index = haystack.indexOf(needle);
+ while (index >= 0) {
+ count++;
+ index = haystack.indexOf(needle, index + needle.length());
+ }
+ return count;
+ }
+}