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

tballison pushed a commit to branch TIKA-4816-metadata-key-api
in repository https://gitbox.apache.org/repos/asf/tika.git

commit 24606fcd3bded3008f244b7b674a1be377080420
Author: tallison <[email protected]>
AuthorDate: Tue Aug 11 14:22:02 2026 -0400

    TIKA-4816 metadata-key stage 4: putAll(Metadata) public copy API
---
 .../java/org/apache/tika/metadata/Metadata.java    | 34 ++++++++
 .../java/org/apache/tika/utils/ParserUtils.java    | 11 +--
 .../apache/tika/metadata/MetadataPutAllTest.java   | 98 ++++++++++++++++++++++
 3 files changed, 133 insertions(+), 10 deletions(-)

diff --git a/tika-core/src/main/java/org/apache/tika/metadata/Metadata.java 
b/tika-core/src/main/java/org/apache/tika/metadata/Metadata.java
index 1f20a5ce09..9ea8385850 100644
--- a/tika-core/src/main/java/org/apache/tika/metadata/Metadata.java
+++ b/tika-core/src/main/java/org/apache/tika/metadata/Metadata.java
@@ -361,6 +361,40 @@ public class Metadata
         }
     }
 
+    /**
+     * Copies every key from {@code other} into this Metadata: for each name in
+     * {@code other}, this Metadata's values for that name are replaced 
wholesale with
+     * {@code other}'s values, in order (multi-values preserved); names absent 
from
+     * {@code other} are left untouched. Each value is written via
+     * {@link #reconstruct(String, String, boolean)}, so reserved {@code tk:} 
keys copy
+     * through their trusted route rather than the String-guarded route.
+     * <p>
+     * This is the supported replacement for a manual
+     * {@code for (String n : src.names()) dest.set(n, src.get(n))} copy loop, 
which
+     * silently collapses multi-valued keys to a single value today and throws 
on
+     * {@code tk:}-prefixed keys once the reserved-key guard is flipped from 
drop to throw.
+     *
+     * @param other the Metadata to copy from; {@code other == this} is a no-op
+     * @throws NullPointerException if other is null
+     * @since Apache Tika 4.0
+     */
+    public void putAll(Metadata other) {
+        Objects.requireNonNull(other, "other must not be null");
+        if (other == this) {
+            return;
+        }
+        for (String n : other.names()) {
+            String[] vals = other.getValues(n);
+            if (vals.length == 0) {
+                continue;
+            }
+            reconstruct(n, vals[0], false);
+            for (int i = 1; i < vals.length; i++) {
+                reconstruct(n, vals[i], true);
+            }
+        }
+    }
+
     /**
      * Add a metadata name/value mapping. Add the specified value to the list 
of
      * values associated to the specified metadata name.
diff --git a/tika-core/src/main/java/org/apache/tika/utils/ParserUtils.java 
b/tika-core/src/main/java/org/apache/tika/utils/ParserUtils.java
index e194536076..bf1a845247 100644
--- a/tika-core/src/main/java/org/apache/tika/utils/ParserUtils.java
+++ b/tika-core/src/main/java/org/apache/tika/utils/ParserUtils.java
@@ -43,16 +43,7 @@ public class ParserUtils {
      */
     public static Metadata cloneMetadata(Metadata m) {
         Metadata clone = new Metadata();
-
-        for (String n : m.names()) {
-            if (!m.isMultiValued(n)) {
-                clone.reconstruct(n, m.get(n), false);
-            } else {
-                for (String val : m.getValues(n)) {
-                    clone.reconstruct(n, val, true);
-                }
-            }
-        }
+        clone.putAll(m);
         return clone;
     }
 
diff --git 
a/tika-core/src/test/java/org/apache/tika/metadata/MetadataPutAllTest.java 
b/tika-core/src/test/java/org/apache/tika/metadata/MetadataPutAllTest.java
new file mode 100644
index 0000000000..003eda4585
--- /dev/null
+++ b/tika-core/src/test/java/org/apache/tika/metadata/MetadataPutAllTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.tika.metadata;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+/** {@link Metadata#putAll(Metadata)}: the provenance-preserving 
per-key-replace copy API. */
+public class MetadataPutAllTest {
+
+    @Test
+    public void testMultiValuedKeyCopiesAllValuesInOrder() {
+        Metadata src = new Metadata();
+        src.add("multi", "a");
+        src.add("multi", "b");
+        src.add("multi", "c");
+
+        Metadata dest = new Metadata();
+        dest.putAll(src);
+
+        // the manual `dest.set(n, src.get(n))` loop this replaces would 
collapse to just "a"
+        assertArrayEquals(new String[] {"a", "b", "c"}, 
dest.getValues("multi"));
+    }
+
+    @Test
+    public void testReservedKeyCopiesThroughTrustedRoute() {
+        Metadata src = new Metadata();
+        src.reconstruct(TikaCoreProperties.TIKA_CONTENT.getName(), "the 
content", false);
+
+        Metadata dest = new Metadata();
+        dest.putAll(src);
+
+        assertEquals("the content", dest.get(TikaCoreProperties.TIKA_CONTENT));
+    }
+
+    @Test
+    public void testPerKeyReplaceSemantics() {
+        Metadata src = new Metadata();
+        src.add("k", "only");
+
+        Metadata dest = new Metadata();
+        dest.add("k", "v1");
+        dest.add("k", "v2");
+        dest.add("k", "v3");
+
+        dest.putAll(src);
+
+        assertArrayEquals(new String[] {"only"}, dest.getValues("k"));
+    }
+
+    @Test
+    public void testKeysAbsentFromOtherAreUntouched() {
+        Metadata src = new Metadata();
+        src.add("inOther", "x");
+
+        Metadata dest = new Metadata();
+        dest.add("onlyInDest", "keepme");
+
+        dest.putAll(src);
+
+        assertEquals("keepme", dest.get("onlyInDest"));
+        assertEquals("x", dest.get("inOther"));
+    }
+
+    @Test
+    public void testSelfPutAllIsNoOp() {
+        Metadata metadata = new Metadata();
+        metadata.add("k", "v1");
+        metadata.add("k", "v2");
+
+        metadata.putAll(metadata);
+
+        assertArrayEquals(new String[] {"v1", "v2"}, metadata.getValues("k"));
+    }
+
+    @Test
+    public void testNullOtherThrowsNPE() {
+        Metadata metadata = new Metadata();
+        assertThrows(NullPointerException.class, () -> metadata.putAll(null));
+    }
+}

Reply via email to