This is an automated email from the ASF dual-hosted git repository.
Croway pushed a commit to branch camel-4.18.x
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/camel-4.18.x by this push:
new e3dc728b8594 CAMEL-23740: Fix WrappedFile converter for FTP/SFTP
remote files
e3dc728b8594 is described below
commit e3dc728b8594f8381ba44e4c100dac1c09f755e6
Author: Croway <[email protected]>
AuthorDate: Thu Jun 11 17:11:29 2026 +0200
CAMEL-23740: Fix WrappedFile converter for FTP/SFTP remote files
---
.../agent/LangChain4jAgentConverter.java | 66 ++++++++---
.../agent/LangChain4jAgentConverterTest.java | 122 +++++++++++++++++++++
2 files changed, 174 insertions(+), 14 deletions(-)
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentConverter.java
b/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentConverter.java
index 07ec8ca41e80..eb5e0ddf3e92 100644
---
a/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentConverter.java
+++
b/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentConverter.java
@@ -34,6 +34,7 @@ import dev.langchain4j.data.pdf.PdfFile;
import dev.langchain4j.data.video.Video;
import org.apache.camel.Converter;
import org.apache.camel.Exchange;
+import org.apache.camel.NoTypeConversionAvailableException;
import org.apache.camel.WrappedFile;
import org.apache.camel.component.langchain4j.agent.api.AiAgentBody;
import org.slf4j.Logger;
@@ -94,18 +95,9 @@ public final class LangChain4jAgentConverter {
*/
@Converter
public static AiAgentBody<?> toAiAgentBody(WrappedFile<?> wrappedFile,
Exchange exchange) {
- Object fileObj = wrappedFile.getFile();
- if (fileObj == null) {
- throw new IllegalArgumentException("WrappedFile contains null
file");
- }
- if (!(fileObj instanceof File)) {
- throw new IllegalArgumentException(
- "WrappedFile must contain a java.io.File instance, got: "
+ fileObj.getClass().getName());
- }
-
- File file = (File) fileObj;
- String mimeType = detectMimeType(file, exchange);
- byte[] fileData = readFileBytes(file);
+ String fileName = resolveFileName(wrappedFile, exchange);
+ byte[] fileData = resolveFileData(wrappedFile, exchange);
+ String mimeType = detectMimeType(fileName, exchange);
Content content = createContent(fileData, mimeType);
String userMessage = exchange.getIn().getHeader(USER_MESSAGE,
String.class);
@@ -121,6 +113,45 @@ public final class LangChain4jAgentConverter {
return body;
}
+ private static String resolveFileName(WrappedFile<?> wrappedFile, Exchange
exchange) {
+ String headerName = exchange.getIn().getHeader(Exchange.FILE_NAME,
String.class);
+ if (headerName != null) {
+ return headerName;
+ }
+ Object fileObj = wrappedFile.getFile();
+ if (fileObj instanceof File file) {
+ return file.getName();
+ }
+ Object body = wrappedFile.getBody();
+ if (body instanceof File file) {
+ return file.getName();
+ }
+ return null;
+ }
+
+ private static byte[] resolveFileData(WrappedFile<?> wrappedFile, Exchange
exchange) {
+ Object fileObj = wrappedFile.getFile();
+ if (fileObj instanceof File file) {
+ return readFileBytes(file);
+ }
+ Object body = wrappedFile.getBody();
+ if (body instanceof File file) {
+ return readFileBytes(file);
+ }
+ if (body instanceof byte[] bytes) {
+ return bytes;
+ }
+ if (body != null) {
+ try {
+ return
exchange.getContext().getTypeConverter().mandatoryConvertTo(byte[].class,
exchange, body);
+ } catch (NoTypeConversionAvailableException e) {
+ throw new IllegalArgumentException(
+ "WrappedFile body could not be converted to byte[]: "
+ body.getClass().getName(), e);
+ }
+ }
+ throw new IllegalArgumentException("WrappedFile body is null");
+ }
+
/**
* Converts a {@code byte[]} to an {@link AiAgentBody} with the
appropriate {@link Content} type.
* <p>
@@ -240,7 +271,7 @@ public final class LangChain4jAgentConverter {
* <li>Auto-detection from file extension</li>
* </ol>
*/
- private static String detectMimeType(File file, Exchange exchange) {
+ private static String detectMimeType(String fileName, Exchange exchange) {
// Check agent-specific header first (highest priority)
String mediaType = exchange.getIn().getHeader(MEDIA_TYPE,
String.class);
if (mediaType != null) {
@@ -253,8 +284,14 @@ public final class LangChain4jAgentConverter {
return fileContentType;
}
+ if (fileName == null) {
+ throw new IllegalArgumentException(
+ "Cannot determine MIME type: no file name available. "
+ + "Set the
CamelLangChain4jAgentMediaType, CamelFileContentType, or CamelFileName
header.");
+ }
+
// Auto-detect from file extension
- return detectMimeTypeFromExtension(file.getName());
+ return detectMimeTypeFromExtension(fileName);
}
/**
@@ -405,4 +442,5 @@ public final class LangChain4jAgentConverter {
throw new IllegalArgumentException("Failed to read file: " +
file.getAbsolutePath(), e);
}
}
+
}
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentConverterTest.java
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentConverterTest.java
new file mode 100644
index 000000000000..2c01e7091cae
--- /dev/null
+++
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentConverterTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.component.langchain4j.agent;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import dev.langchain4j.data.message.ImageContent;
+import dev.langchain4j.data.message.TextContent;
+import org.apache.camel.Exchange;
+import org.apache.camel.TypeConversionException;
+import org.apache.camel.component.file.GenericFile;
+import org.apache.camel.component.langchain4j.agent.api.AiAgentBody;
+import org.apache.camel.component.langchain4j.agent.api.Headers;
+import org.apache.camel.test.junit5.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class LangChain4jAgentConverterTest extends CamelTestSupport {
+
+ @TempDir
+ Path tempDir;
+
+ @Test
+ void shouldConvertLocalFile() throws Exception {
+ Exchange exchange =
context.getEndpoint("direct:test").createExchange();
+
+ File file = Files.writeString(tempDir.resolve("hello.txt"), "Hello
file").toFile();
+
+ GenericFile<File> genericFile = new GenericFile<>();
+ genericFile.setFile(file);
+ genericFile.setFileName(file.getName());
+
+ AiAgentBody<?> body = context.getTypeConverter()
+ .convertTo(AiAgentBody.class, exchange, genericFile);
+
+ assertNotNull(body);
+ assertInstanceOf(TextContent.class, body.getContent());
+ }
+
+ @Test
+ void shouldConvertRemoteWrappedFileUsingFileNameHeader() {
+ Exchange exchange =
context.getEndpoint("direct:test").createExchange();
+ exchange.getIn().setHeader(Exchange.FILE_NAME, "photo.png");
+
+ GenericFile<String> remoteFile = new GenericFile<>();
+ remoteFile.setFile("remote-handle");
+ remoteFile.setBody(new byte[] { 0x00, 0x01, 0x02 });
+
+ AiAgentBody<?> body = context.getTypeConverter()
+ .convertTo(AiAgentBody.class, exchange, remoteFile);
+
+ assertNotNull(body);
+ assertInstanceOf(ImageContent.class, body.getContent());
+ }
+
+ @Test
+ void shouldConvertRemoteWrappedFileWithMediaTypeHeader() {
+ Exchange exchange =
context.getEndpoint("direct:test").createExchange();
+ exchange.getIn().setHeader(Headers.MEDIA_TYPE, "text/plain");
+
+ GenericFile<String> remoteFile = new GenericFile<>();
+ remoteFile.setFile("remote-handle");
+ remoteFile.setBody("Hello remote".getBytes());
+
+ AiAgentBody<?> body = context.getTypeConverter()
+ .convertTo(AiAgentBody.class, exchange, remoteFile);
+
+ assertNotNull(body);
+ assertInstanceOf(TextContent.class, body.getContent());
+ }
+
+ @Test
+ void shouldConvertRemoteWrappedFileWithInputStreamBody() {
+ Exchange exchange =
context.getEndpoint("direct:test").createExchange();
+ exchange.getIn().setHeader(Exchange.FILE_NAME, "notes.txt");
+
+ GenericFile<String> remoteFile = new GenericFile<>();
+ remoteFile.setFile("remote-handle");
+ remoteFile.setBody(new ByteArrayInputStream("streamed
content".getBytes()));
+
+ AiAgentBody<?> body = context.getTypeConverter()
+ .convertTo(AiAgentBody.class, exchange, remoteFile);
+
+ assertNotNull(body);
+ assertInstanceOf(TextContent.class, body.getContent());
+ }
+
+ @Test
+ void shouldFailForRemoteWrappedFileWithNoFileNameAndNoHeaders() {
+ Exchange exchange =
context.getEndpoint("direct:test").createExchange();
+
+ GenericFile<String> remoteFile = new GenericFile<>();
+ remoteFile.setFile("remote-handle");
+ remoteFile.setBody("some data".getBytes());
+
+ assertThrows(
+ TypeConversionException.class,
+ () -> context.getTypeConverter()
+ .convertTo(AiAgentBody.class, exchange, remoteFile));
+ }
+}