This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 959866bd702b CAMEL-25723: camel-zipfile - ZipAggregationStrategy keeps
an entry whose name comes again under a numbered name (#28031)
959866bd702b is described below
commit 959866bd702b7dd94ecf41bdb22e3c677715439d
Author: allthingssecurity <[email protected]>
AuthorDate: Sun Oct 18 21:01:09 2026 +0530
CAMEL-25723: camel-zipfile - ZipAggregationStrategy keeps an entry whose
name comes again under a numbered name (#28031)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../camel/catalog/docs/zipFile-dataformat.adoc | 4 +
.../src/main/docs/zipFile-dataformat.adoc | 4 +
.../aggregate/zipfile/ZipAggregationStrategy.java | 31 ++++
.../ZipAggregationStrategySameFileNameTest.java | 206 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 8 +
5 files changed, 253 insertions(+)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/zipFile-dataformat.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/zipFile-dataformat.adoc
index 238261b48da2..eb8970c15a58 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/zipFile-dataformat.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/zipFile-dataformat.adoc
@@ -253,6 +253,10 @@ from("file:input/directory?antInclude=*/.txt")
.to("file:output/directory");
----
+When the zip file already has an entry with the same name, for example two
files `orders.csv` from different folders
+of a recursive file consumer, the later entry is added with a number before
its extension (`orders (1).csv`,
+`orders (2).csv`, ...), so no content is lost. Use `preserveFolderStructure`
to keep the folders in the entry names.
+
== Dependencies
To use Zip files in your camel routes, you need to add a dependency on
diff --git a/components/camel-zipfile/src/main/docs/zipFile-dataformat.adoc
b/components/camel-zipfile/src/main/docs/zipFile-dataformat.adoc
index 238261b48da2..eb8970c15a58 100644
--- a/components/camel-zipfile/src/main/docs/zipFile-dataformat.adoc
+++ b/components/camel-zipfile/src/main/docs/zipFile-dataformat.adoc
@@ -253,6 +253,10 @@ from("file:input/directory?antInclude=*/.txt")
.to("file:output/directory");
----
+When the zip file already has an entry with the same name, for example two
files `orders.csv` from different folders
+of a recursive file consumer, the later entry is added with a number before
its extension (`orders (1).csv`,
+`orders (2).csv`, ...), so no content is lost. Use `preserveFolderStructure`
to keep the folders in the entry names.
+
== Dependencies
To use Zip files in your camel routes, you need to add a dependency on
diff --git
a/components/camel-zipfile/src/main/java/org/apache/camel/processor/aggregate/zipfile/ZipAggregationStrategy.java
b/components/camel-zipfile/src/main/java/org/apache/camel/processor/aggregate/zipfile/ZipAggregationStrategy.java
index 47713868811f..74de4788693a 100644
---
a/components/camel-zipfile/src/main/java/org/apache/camel/processor/aggregate/zipfile/ZipAggregationStrategy.java
+++
b/components/camel-zipfile/src/main/java/org/apache/camel/processor/aggregate/zipfile/ZipAggregationStrategy.java
@@ -50,6 +50,11 @@ import org.apache.camel.util.FileUtil;
* useFilenameHeader is set to true.
* </p>
* <p>
+ * When the ZIP file already has an entry with the same name (for example two
files {@code orders.csv} from different
+ * folders), the new entry gets a number before its extension ({@code orders
(1).csv}, {@code orders (2).csv}, ...) so
+ * that the earlier entry is kept.
+ * </p>
+ * <p>
* <b>Note:</b> Please note that this aggregation strategy requires eager
completion check to work properly.
* </p>
*/
@@ -316,6 +321,7 @@ public class ZipAggregationStrategy implements
AggregationStrategy {
Path parent = dest.getParent();
if (parent != null) {
Files.createDirectories(parent);
+ dest = freeEntryName(dest);
Files.copy(file.toPath(), dest,
StandardCopyOption.REPLACE_EXISTING);
}
}
@@ -331,11 +337,36 @@ public class ZipAggregationStrategy implements
AggregationStrategy {
Path parent = dest.getParent();
if (parent != null) {
Files.createDirectories(parent);
+ dest = freeEntryName(dest);
Files.write(dest, buffer, StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING);
}
}
}
+ /**
+ * Returns the given entry when the zip file has no entry with that name,
otherwise the first free name with
+ * {@code " (n)"} before the extension: {@code orders.csv} becomes {@code
orders (1).csv}, then
+ * {@code orders (2).csv}. Writing to an existing entry would replace it,
and the content of the earlier message
+ * would be lost. The loop ends: every taken name is an entry of the zip
file, so at most one more name than there
+ * are entries is checked.
+ */
+ private static Path freeEntryName(Path dest) {
+ if (Files.notExists(dest)) {
+ return dest;
+ }
+ String name = dest.getFileName().toString();
+ int dot = name.lastIndexOf('.');
+ // a leading dot (".profile") is part of the name, not an extension
+ String base = dot > 0 ? name.substring(0, dot) : name;
+ String extension = dot > 0 ? name.substring(dot) : "";
+ for (int i = 1;; i++) {
+ Path candidate = dest.resolveSibling(base + " (" + i + ")" +
extension);
+ if (Files.notExists(candidate)) {
+ return candidate;
+ }
+ }
+ }
+
private static URI getZipURI(File zipFile) throws URISyntaxException {
return new URI("jar", zipFile.toURI().toString(), null);
}
diff --git
a/components/camel-zipfile/src/test/java/org/apache/camel/processor/aggregate/zipfile/ZipAggregationStrategySameFileNameTest.java
b/components/camel-zipfile/src/test/java/org/apache/camel/processor/aggregate/zipfile/ZipAggregationStrategySameFileNameTest.java
new file mode 100644
index 000000000000..4e2e9a4a8987
--- /dev/null
+++
b/components/camel-zipfile/src/test/java/org/apache/camel/processor/aggregate/zipfile/ZipAggregationStrategySameFileNameTest.java
@@ -0,0 +1,206 @@
+/*
+ * 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.camel.processor.aggregate.zipfile;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.support.DefaultExchange;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.apache.camel.test.junit6.TestSupport.deleteDirectory;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * Two messages with the same entry name (files with the same name from
different folders, or the same CamelFileName)
+ * must not replace each other in the zip file: the later one is added as
{@code name (1).ext}, {@code name (2).ext},
+ * ...
+ */
+class ZipAggregationStrategySameFileNameTest extends CamelTestSupport {
+
+ private static final String DIR =
"target/ZipAggregationStrategySameFileNameTest";
+
+ @BeforeEach
+ void cleanDirectories() {
+ deleteDirectory(DIR);
+ }
+
+ @Test
+ void secondFileWithSameNameIsAddedUnderANumberedName() throws Exception {
+ write(DIR + "/files/a/orders.csv", "from A");
+ write(DIR + "/files/b/orders.csv", "from B");
+ ZipAggregationStrategy strategy = new ZipAggregationStrategy();
+
+ Exchange answer = strategy.aggregate(null, fileExchange(DIR +
"/files/a/orders.csv"));
+ answer = strategy.aggregate(answer, fileExchange(DIR +
"/files/b/orders.csv"));
+
+ assertEquals(List.of("orders (1).csv=from B", "orders.csv=from A"),
entries(answer.getIn().getBody(File.class)));
+ }
+
+ @Test
+ void secondBodyWithSameFileNameHeaderIsAddedUnderANumberedName() throws
Exception {
+ ZipAggregationStrategy strategy = new ZipAggregationStrategy(false,
true);
+
+ Exchange answer = strategy.aggregate(null, bytesExchange("report.txt",
"first"));
+ answer = strategy.aggregate(answer, bytesExchange("report.txt",
"second"));
+
+ assertEquals(List.of("report (1).txt=second", "report.txt=first"),
entries(answer.getIn().getBody(File.class)));
+ }
+
+ @Test
+ void manyFilesWithSameNameAreAllKept() throws Exception {
+ write(DIR + "/files/a/orders.csv", "from A");
+ write(DIR + "/files/b/orders (1).csv", "from B, a file with this
name");
+ write(DIR + "/files/c/orders.csv", "from C");
+ write(DIR + "/files/d/orders.csv", "from D");
+ write(DIR + "/files/e/orders.csv", "from E");
+ ZipAggregationStrategy strategy = new ZipAggregationStrategy();
+
+ Exchange answer = null;
+ for (String path : List.of("a/orders.csv", "b/orders (1).csv",
"c/orders.csv", "d/orders.csv", "e/orders.csv")) {
+ answer = strategy.aggregate(answer, fileExchange(DIR + "/files/" +
path));
+ }
+
+ assertEquals(List.of(
+ "orders (1).csv=from B, a file with this name",
+ "orders (2).csv=from C",
+ "orders (3).csv=from D",
+ "orders (4).csv=from E",
+ "orders.csv=from A"),
entries(answer.getIn().getBody(File.class)));
+ }
+
+ @Test
+ void namesWithoutExtensionGetTheNumberAtTheEnd() throws Exception {
+ ZipAggregationStrategy strategy = new ZipAggregationStrategy(false,
true);
+
+ Exchange answer = strategy.aggregate(null, bytesExchange("README",
"one"));
+ answer = strategy.aggregate(answer, bytesExchange("README", "two"));
+ answer = strategy.aggregate(answer, bytesExchange(".profile",
"three"));
+ answer = strategy.aggregate(answer, bytesExchange(".profile", "four"));
+
+ assertEquals(List.of(".profile (1)=four", ".profile=three", "README
(1)=two", "README=one"),
+ entries(answer.getIn().getBody(File.class)));
+ }
+
+ @Test
+ void recursivePollWithTwoFilesOfTheSameNameZipsBoth() throws Exception {
+ // only the two files: the duplicate is the last exchange of the batch
and completes the group (eager check)
+ write(DIR + "/two/a/orders.csv", "from A");
+ write(DIR + "/two/b/orders.csv", "from B");
+
+ assertEquals(List.of("orders (1).csv=from B", "orders.csv=from A"),
zipOnce("two"));
+ }
+
+ @Test
+ void recursivePollWithFilesOfTheSameNameZipsAll() throws Exception {
+ write(DIR + "/three/a/orders.csv", "from A");
+ write(DIR + "/three/b/orders.csv", "from B");
+ write(DIR + "/three/c/other.csv", "from C");
+
+ assertEquals(List.of("orders (1).csv=from B", "orders.csv=from A",
"other.csv=from C"), zipOnce("three"));
+ }
+
+ @Test
+ void preserveFolderStructureKeepsTheFolderNames() throws Exception {
+ // the entry names include the folders, so they differ and are not
numbered (same result as before the fix)
+ write(DIR + "/preserve/a/orders.csv", "from A");
+ write(DIR + "/preserve/b/orders.csv", "from B");
+
+ assertEquals(List.of("a/orders.csv=from A", "b/orders.csv=from B"),
zipOnce("preserve"));
+ }
+
+ private List<String> zipOnce(String routeId) throws Exception {
+ MockEndpoint mock = getMockEndpoint("mock:" + routeId);
+ mock.expectedMessageCount(1);
+ context.getRouteController().startRoute(routeId);
+ MockEndpoint.assertIsSatisfied(context);
+
+ File[] zips = new File(DIR + "/out-" + routeId).listFiles();
+ assertNotNull(zips);
+ assertEquals(1, zips.length);
+ return entries(zips[0]);
+ }
+
+ private Exchange fileExchange(String path) {
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getIn().setBody(new File(path));
+ return exchange;
+ }
+
+ private Exchange bytesExchange(String fileName, String content) {
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getIn().setBody(content.getBytes(StandardCharsets.UTF_8));
+ exchange.getIn().setHeader(Exchange.FILE_NAME, fileName);
+ return exchange;
+ }
+
+ private static List<String> entries(File zip) throws IOException {
+ List<String> answer = new ArrayList<>();
+ try (ZipInputStream zin = new ZipInputStream(new
FileInputStream(zip))) {
+ for (ZipEntry ze = zin.getNextEntry(); ze != null; ze =
zin.getNextEntry()) {
+ if (!ze.isDirectory()) {
+ answer.add(ze.getName() + "=" + new
String(zin.readAllBytes(), StandardCharsets.UTF_8));
+ }
+ }
+ }
+ answer.sort(null);
+ return answer;
+ }
+
+ private static void write(String path, String content) throws IOException {
+ Path p = Path.of(path);
+ Files.createDirectories(p.getParent());
+ Files.writeString(p, content, StandardCharsets.UTF_8);
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ zipRoute("two", new ZipAggregationStrategy());
+ zipRoute("three", new ZipAggregationStrategy());
+ zipRoute("preserve", new ZipAggregationStrategy(true));
+ }
+
+ private void zipRoute(String id, ZipAggregationStrategy strategy) {
+ // the documented pattern: one zip per poll of a recursive
file consumer
+ from("file:" + DIR + "/" + id +
"?recursive=true&noop=true&sortBy=file:path&initialDelay=0&delay=100")
+ .routeId(id).autoStartup(false)
+ .aggregate(constant(true), strategy)
+ .completionFromBatchConsumer()
+ .eagerCheckCompletion()
+ .to("file:" + DIR + "/out-" + id)
+ .to("mock:" + id);
+ }
+ };
+ }
+}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 1016dd2e366d..74d6f61a5466 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -855,6 +855,14 @@ as `Failed to connect, because a certificate or security
check failed`. To resto
The certificate that the user name and password are encrypted with is checked
by the same validator. Endpoints that
use the security policy `None` are not affected.
+=== camel-zipfile - ZipAggregationStrategy keeps entries with the same name
+
+`ZipAggregationStrategy` replaced an entry of the zip file when a later
message had the same entry name, for example
+two files `orders.csv` from different folders of a recursive file consumer
(the folders are not kept by default), so
+the content of the earlier file was lost. The later entry is now added with a
number before its extension:
+`orders (1).csv`, `orders (2).csv`, and so on. Entry names can therefore
differ from the file names (or the
+`CamelFileName` header). Use `preserveFolderStructure=true` to keep the
folders in the entry names instead.
+
=== camel-lra - only an LRA hosted by the configured coordinator is joined
The LRA saga service now joins, completes or compensates an LRA only when its
URL is under the configured LRA