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 066ccc624823 CAMEL-25751: a Map or List body says to marshal it first,
in the data format of the reply, the request or the file (#28048)
066ccc624823 is described below
commit 066ccc624823ec883d103684753292f39ae651c8
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Oct 18 16:00:59 2026 +0200
CAMEL-25751: a Map or List body says to marshal it first, in the data
format of the reply, the request or the file (#28048)
The platform-http reply, the file producer and the http producer cannot
write a Map or a List (a body unmarshalled from JSON, XML or CSV, or built
as a map). The reply and the file write only named the conversion ("No
type converter available to convert from type: java.util.LinkedHashMap to
the required type: java.io.InputStream"); now all three say what to do, in
the data format the target asks for: the Content-Type of the reply or the
request, the file name for a file write, and the usual ones (marshal: json,
marshal: jacksonXml or marshal: csv) when nothing names one. The REST
binding is offered only for json and xml, the formats it writes. The http
producer's sentence from CAMEL-25309 follows the same rule, and "Cannot
store file" now carries the cause's message when the body cannot be read
as a stream.
Signed-off-by: Claus Ibsen <[email protected]>
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
.../camel/component/file/FileOperations.java | 68 +++++++++++-
.../component/file/FileProducerMapBodyTest.java | 100 ++++++++++++++++++
.../apache/camel/component/http/HttpProducer.java | 39 ++++++-
.../component/http/HttpProducerMapBodyTest.java | 21 +++-
.../http/vertx/VertxPlatformHttpSupport.java | 54 +++++++++-
.../vertx/VertxPlatformHttpMapResponseTest.java | 117 +++++++++++++++++++++
6 files changed, 386 insertions(+), 13 deletions(-)
diff --git
a/components/camel-file/src/main/java/org/apache/camel/component/file/FileOperations.java
b/components/camel-file/src/main/java/org/apache/camel/component/file/FileOperations.java
index 24261c8c5d18..c30bac83e3eb 100644
---
a/components/camel-file/src/main/java/org/apache/camel/component/file/FileOperations.java
+++
b/components/camel-file/src/main/java/org/apache/camel/component/file/FileOperations.java
@@ -38,16 +38,22 @@ import java.nio.file.attribute.PosixFilePermissions;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
+import java.util.Collection;
import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.camel.Exchange;
import org.apache.camel.InvalidPayloadException;
+import org.apache.camel.Message;
import org.apache.camel.WrappedFile;
import org.apache.camel.util.FileUtil;
import org.apache.camel.util.IOHelper;
+import org.apache.camel.util.MimeTypeHelper;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.StringHelper;
import org.slf4j.Logger;
@@ -351,7 +357,7 @@ public class FileOperations implements
GenericFileOperations<File> {
// prefer FileInputStream over general stream
InputStream is =
exchange.getMessage().getBody(FileInputStream.class);
if (is == null) {
- is =
exchange.getMessage().getMandatoryBody(InputStream.class);
+ is = mandatoryBodyStream(exchange, exchange.getMessage(),
file);
}
dis = new DigestInputStream(is, digest);
}
@@ -378,11 +384,65 @@ public class FileOperations implements
GenericFileOperations<File> {
setPermissions(file);
return true;
- } catch (InvalidPayloadException | NoSuchAlgorithmException |
IOException e) {
+ } catch (InvalidPayloadException e) {
+ // say why: the body cannot be written as it is
+ throw new GenericFileOperationFailedException("Cannot store file:
" + file + ": " + e.getMessage(), e);
+ } catch (NoSuchAlgorithmException | IOException e) {
throw new GenericFileOperationFailedException("Cannot store file:
" + file, e);
}
}
+ /**
+ * The body as a stream to write. A Map or a List (a body unmarshalled
from JSON, XML or CSV, or built as a map)
+ * cannot become one, and the converter's message only names the
conversion: say what to do, in the data format the
+ * file name names, or name the usual ones when it names none
(CAMEL-25751).
+ */
+ static InputStream mandatoryBodyStream(Exchange exchange, Message message,
File file) throws InvalidPayloadException {
+ try {
+ return message.getMandatoryBody(InputStream.class);
+ } catch (InvalidPayloadException e) {
+ Object body = message.getBody();
+ if (body instanceof Map || body instanceof Collection) {
+ String format =
dataFormatOf(MimeTypeHelper.probeMimeType(file.getName()));
+ InvalidPayloadException answer = new InvalidPayloadException(
+ exchange, InputStream.class, message,
+ "a " + (body instanceof Map ? "Map" : body instanceof
List ? "List" : "Collection")
+ + " is not file
content: "
+ + (format != null
+ ?
"marshal it first (marshal: " + format
+ + ",
for the file name " + file.getName() + ")"
+ :
"marshal it first with the data format of the file (such as"
+ + "
marshal: json, marshal: jacksonXml or marshal: csv)")
+ + " or set the
body to the text");
+ answer.initCause(e.getCause() != null ? e.getCause() : e);
+ throw answer;
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * The data format that writes a content type, as the YAML DSL names it:
json, jacksonXml, csv or yaml; or null
+ * (CAMEL-25751; CAMEL-25749 moves this onto one helper).
+ */
+ static String dataFormatOf(String contentType) {
+ if (contentType == null) {
+ return null;
+ }
+ int semicolon = contentType.indexOf(';');
+ String type = (semicolon > 0 ? contentType.substring(0, semicolon) :
contentType).trim().toLowerCase(Locale.ROOT);
+ if (type.endsWith("/json") || type.endsWith("+json")) {
+ return "json";
+ } else if (type.endsWith("/xml") || type.endsWith("+xml")) {
+ return "jacksonXml";
+ } else if (type.endsWith("/csv") ||
type.endsWith("/tab-separated-values")) {
+ return "csv";
+ } else if (type.endsWith("/yaml") || type.endsWith("/x-yaml")) {
+ return "yaml";
+ }
+ return null;
+ }
+
@Override
public boolean storeFileDirectly(String name, String payload) throws
GenericFileOperationFailedException {
File file = new File(name);
@@ -399,7 +459,7 @@ public class FileOperations implements
GenericFileOperations<File> {
private void handleStreamAsFileSource(Exchange exchange, File file,
InputStream is)
throws InvalidPayloadException, IOException {
if (is == null) {
- is = exchange.getIn().getMandatoryBody(InputStream.class);
+ is = mandatoryBodyStream(exchange, exchange.getIn(), file);
}
writeFileByStream(is, file);
}
@@ -420,7 +480,7 @@ public class FileOperations implements
GenericFileOperations<File> {
// okay no direct reader conversion, so use an input stream
// (which a lot can be converted as)
if (is == null) {
- is = exchange.getIn().getMandatoryBody(InputStream.class);
+ is = mandatoryBodyStream(exchange, exchange.getIn(), file);
}
in = new InputStreamReader(is);
}
diff --git
a/components/camel-file/src/test/java/org/apache/camel/component/file/FileProducerMapBodyTest.java
b/components/camel-file/src/test/java/org/apache/camel/component/file/FileProducerMapBodyTest.java
new file mode 100644
index 000000000000..b90f5ce7c0c0
--- /dev/null
+++
b/components/camel-file/src/test/java/org/apache/camel/component/file/FileProducerMapBodyTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.file;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.InvalidPayloadException;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.support.DefaultExchange;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * CAMEL-25751: a Map or a List written to a file - a body unmarshalled from
JSON, XML or CSV - says to marshal it
+ * first, in the data format the file name names.
+ */
+public class FileProducerMapBodyTest {
+
+ @TempDir
+ Path dir;
+
+ @Test
+ public void testMapBody() throws Exception {
+ Map<String, Object> order = new LinkedHashMap<>();
+ order.put("orderId", "1001");
+ String message = failure(order, null, "order.json");
+ assertTrue(message.startsWith("Cannot store file: "), message);
+ assertTrue(message.contains("but has type: java.util.LinkedHashMap"),
message);
+ assertTrue(message.contains("a Map is not file content: marshal it
first (marshal: json, for the file name"
+ + " order.json) or set the body to the
text"),
+ message);
+ assertFalse(Files.exists(dir.resolve("order.json")));
+ }
+
+ @Test
+ public void testListBodyToACsvFileWithCharset() throws Exception {
+ // the file name says the data format; a charset writes through a
reader, made from the same stream
+ String message = failure(List.of(List.of("A1", "2")), "UTF-8",
"lines.csv");
+ assertTrue(message.contains("a List is not file content: marshal it
first (marshal: csv, for the file name"
+ + " lines.csv)"),
+ message);
+ }
+
+ @Test
+ public void testFileNameThatNamesNoDataFormat() throws Exception {
+ String message = failure(Map.of("sku", "A1"), null, "order.dat");
+ assertTrue(message.contains("a Map is not file content: marshal it
first with the data format of the file (such as"
+ + " marshal: json, marshal: jacksonXml or
marshal: csv)"),
+ message);
+ }
+
+ @Test
+ public void testOtherBodyKeepsTheMessage() throws Exception {
+ String message = failure(new Object(), null, "order.json");
+ assertTrue(message.contains("No body available of type:
java.io.InputStream"), message);
+ assertFalse(message.contains("is not file content"), message);
+ }
+
+ private String failure(Object body, String charset, String fileName)
throws Exception {
+ try (CamelContext context = new DefaultCamelContext()) {
+ FileEndpoint endpoint = new FileEndpoint("file:" + dir, new
FileComponent(context));
+ endpoint.setFile(dir.toFile());
+ if (charset != null) {
+ endpoint.setCharset(charset);
+ }
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getIn().setBody(body);
+ FileOperations operations = new FileOperations(endpoint);
+ GenericFileOperationFailedException e =
assertThrows(GenericFileOperationFailedException.class,
+ () ->
operations.storeFile(dir.resolve(fileName).toString(), exchange, -1));
+ assertInstanceOf(InvalidPayloadException.class, e.getCause());
+ return e.getMessage();
+ }
+ }
+}
diff --git
a/components/camel-http/src/main/java/org/apache/camel/component/http/HttpProducer.java
b/components/camel-http/src/main/java/org/apache/camel/component/http/HttpProducer.java
index a15243f1d4eb..789eb887943e 100644
---
a/components/camel-http/src/main/java/org/apache/camel/component/http/HttpProducer.java
+++
b/components/camel-http/src/main/java/org/apache/camel/component/http/HttpProducer.java
@@ -33,6 +33,7 @@ import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Stream;
@@ -742,8 +743,9 @@ public class HttpProducer extends DefaultProducer
implements LineNumberAware {
}
/**
- * The body as a stream for the request entity. A Map or a List (a body
unmarshalled from JSON, or built as a map)
- * cannot become one, and the converter's message only names the
conversion: say what to do (CAMEL-25309).
+ * The body as a stream for the request entity. A Map or a List (a body
unmarshalled from JSON, XML or CSV, or built
+ * as a map) cannot become one, and the converter's message only names the
conversion: say what to do, in the data
+ * format the Content-Type names, or name the usual ones when it names
none (CAMEL-25309, CAMEL-25751).
*/
private static InputStream mandatoryBodyStream(Exchange exchange, Message
in) throws InvalidPayloadException {
try {
@@ -751,11 +753,18 @@ public class HttpProducer extends DefaultProducer
implements LineNumberAware {
} catch (InvalidPayloadException e) {
Object body = in.getBody();
if (body instanceof Map || body instanceof Collection) {
+ String contentType = ExchangeHelper.getContentType(exchange);
+ String format = dataFormatOf(contentType);
InvalidPayloadException answer = new InvalidPayloadException(
exchange, InputStream.class, in,
"a " + (body instanceof Map ? "Map" : body instanceof
List ? "List" : "Collection")
- + " is not an HTTP
request body: marshal it to JSON first "
- + "(marshal: json) or
set the body to the JSON text");
+ + " is not an HTTP
request body: "
+ + (format != null
+ ? "marshal it
first (marshal: " + format
+ + ", for
the Content-Type " + contentType + ")"
+ : "marshal it
first with the data format of the request (such as"
+ + "
marshal: json, marshal: jacksonXml or marshal: csv)")
+ + " or set the body
to the text");
answer.initCause(e.getCause() != null ? e.getCause() : e);
throw answer;
}
@@ -763,6 +772,28 @@ public class HttpProducer extends DefaultProducer
implements LineNumberAware {
}
}
+ /**
+ * The data format that writes a content type, as the YAML DSL names it:
json, jacksonXml, csv or yaml; or null
+ * (CAMEL-25751; CAMEL-25749 moves this onto one helper).
+ */
+ static String dataFormatOf(String contentType) {
+ if (contentType == null) {
+ return null;
+ }
+ int semicolon = contentType.indexOf(';');
+ String type = (semicolon > 0 ? contentType.substring(0, semicolon) :
contentType).trim().toLowerCase(Locale.ROOT);
+ if (type.endsWith("/json") || type.endsWith("+json")) {
+ return "json";
+ } else if (type.endsWith("/xml") || type.endsWith("+xml")) {
+ return "jacksonXml";
+ } else if (type.endsWith("/csv") ||
type.endsWith("/tab-separated-values")) {
+ return "csv";
+ } else if (type.endsWith("/yaml") || type.endsWith("/x-yaml")) {
+ return "yaml";
+ }
+ return null;
+ }
+
/**
* Creates a holder object for the data to send to the remote server.
*
diff --git
a/components/camel-http/src/test/java/org/apache/camel/component/http/HttpProducerMapBodyTest.java
b/components/camel-http/src/test/java/org/apache/camel/component/http/HttpProducerMapBodyTest.java
index 6b75ae3810f2..7c087800b3f2 100644
---
a/components/camel-http/src/test/java/org/apache/camel/component/http/HttpProducerMapBodyTest.java
+++
b/components/camel-http/src/test/java/org/apache/camel/component/http/HttpProducerMapBodyTest.java
@@ -31,7 +31,8 @@ import static
org.apache.camel.component.http.HttpMethods.POST;
import static org.assertj.core.api.Assertions.assertThat;
/**
- * A Map or List body cannot be an HTTP request body; the error says to
marshal it to JSON first (CAMEL-25309).
+ * A Map or List body cannot be an HTTP request body; the error says to
marshal it first, in the data format the
+ * Content-Type names (CAMEL-25309, CAMEL-25751).
*/
public class HttpProducerMapBodyTest extends BaseHttpTest {
@@ -57,18 +58,30 @@ public class HttpProducerMapBodyTest extends BaseHttpTest {
}
@Test
- public void aMapBodySaysToMarshalItToJson() {
+ public void aMapBodySaysToMarshalIt() {
Exchange exchange = template.request(endpointUrl +
"/post?httpMethod=POST",
// the body after unmarshal: json with Jackson
e -> e.getIn().setBody(new LinkedHashMap<>(Map.of("orderId",
"ORD-1001", "qty", 2))));
assertThat(exchange.getException()).isInstanceOf(InvalidPayloadException.class)
.hasMessageContaining("java.util.LinkedHashMap").hasMessageContaining("a Map is
not an HTTP request body")
- .hasMessageContaining("marshal it to JSON first (marshal:
json)");
+ .hasMessageContaining("marshal it first with the data format
of the request (such as marshal: json,"
+ + " marshal: jacksonXml or marshal: csv)
or set the body to the text");
}
@Test
- public void aListBodySaysToMarshalItToJson() {
+ public void theContentTypeNamesTheDataFormat() {
+ Exchange exchange = template.request(endpointUrl +
"/post?httpMethod=POST", e -> {
+ e.getIn().setHeader(Exchange.CONTENT_TYPE, "application/xml");
+ e.getIn().setBody(new LinkedHashMap<>(Map.of("orderId",
"ORD-1001")));
+ });
+
+
assertThat(exchange.getException()).isInstanceOf(InvalidPayloadException.class)
+ .hasMessageContaining("marshal it first (marshal: jacksonXml,
for the Content-Type application/xml)");
+ }
+
+ @Test
+ public void aListBodySaysToMarshalIt() {
Exchange exchange = template.request(endpointUrl +
"/post?httpMethod=POST",
e -> e.getIn().setBody(List.of(Map.of("sku", "CAMEL-MUG"))));
diff --git
a/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpSupport.java
b/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpSupport.java
index bd789b64ae9c..df96cbaff3b1 100644
---
a/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpSupport.java
+++
b/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpSupport.java
@@ -21,8 +21,10 @@ import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.StandardCharsets;
+import java.util.Collection;
import java.util.Iterator;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import io.netty.handler.codec.http.HttpHeaderValues;
@@ -42,6 +44,7 @@ import org.apache.camel.NoTypeConversionAvailableException;
import org.apache.camel.TypeConverter;
import org.apache.camel.spi.HeaderFilterStrategy;
import org.apache.camel.support.ExceptionHelper;
+import org.apache.camel.support.ExchangeHelper;
import org.apache.camel.support.MessageHelper;
import org.apache.camel.support.ObjectHelper;
import org.apache.camel.support.http.HttpUtil;
@@ -250,7 +253,56 @@ public final class VertxPlatformHttpSupport {
writeResponseAs(promise, ctx, bb);
return;
}
- throw new NoTypeConversionAvailableException(body, InputStream.class);
+ throw noTypeConversion(body,
ExchangeHelper.getContentType(camelExchange));
+ }
+
+ /**
+ * A Map or a List (a body unmarshalled from JSON, XML or CSV, or built as
a map) cannot become the response, and
+ * the converter's message only names the conversion: say what to do, in
the data format the Content-Type names, or
+ * name the usual ones when it names none (CAMEL-25751).
+ */
+ static NoTypeConversionAvailableException noTypeConversion(Object body,
String contentType) {
+ if (body instanceof Map || body instanceof Collection) {
+ String format = dataFormatOf(contentType);
+ String marshal = format != null
+ ? "marshal it first (marshal: " + format + ", for the
Content-Type " + contentType + ")"
+ : "marshal it first with the data format of the reply
(such as marshal: json, marshal: jacksonXml or"
+ + " marshal: csv)";
+ // the REST binding writes json and xml
+ String binding = format == null ? ", or let the REST binding write
it (bindingMode: json or xml)"
+ : "json".equals(format) ? ", or let the REST binding write
it (bindingMode: json)"
+ : "jacksonXml".equals(format) ? ", or let the REST binding
write it (bindingMode: xml)" : "";
+ return new NoTypeConversionAvailableException(
+ NoTypeConversionAvailableException.createMessage(body,
InputStream.class) + ": a "
+ + (body instanceof
Map ? "Map" : body instanceof List ? "List"
+ :
"Collection")
+ + " is not an HTTP
response body: " + marshal
+ + " as the last step
of the route" + binding,
+ body, InputStream.class);
+ }
+ return new NoTypeConversionAvailableException(body, InputStream.class);
+ }
+
+ /**
+ * The data format that writes a content type, as the YAML DSL names it:
json, jacksonXml, csv or yaml; or null
+ * (CAMEL-25751; CAMEL-25749 moves this onto one helper).
+ */
+ static String dataFormatOf(String contentType) {
+ if (contentType == null) {
+ return null;
+ }
+ int semicolon = contentType.indexOf(';');
+ String type = (semicolon > 0 ? contentType.substring(0, semicolon) :
contentType).trim().toLowerCase(Locale.ROOT);
+ if (type.endsWith("/json") || type.endsWith("+json")) {
+ return "json";
+ } else if (type.endsWith("/xml") || type.endsWith("+xml")) {
+ return "jacksonXml";
+ } else if (type.endsWith("/csv") ||
type.endsWith("/tab-separated-values")) {
+ return "csv";
+ } else if (type.endsWith("/yaml") || type.endsWith("/x-yaml")) {
+ return "yaml";
+ }
+ return null;
}
private static void writeResponseAs(Promise<Void> promise, RoutingContext
ctx, ByteBuffer bb) {
diff --git
a/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpMapResponseTest.java
b/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpMapResponseTest.java
new file mode 100644
index 000000000000..3d0de403f1da
--- /dev/null
+++
b/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpMapResponseTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.platform.http.vertx;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.NoTypeConversionAvailableException;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.ExceptionHandler;
+import org.junit.jupiter.api.Test;
+
+import static io.restassured.RestAssured.given;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * CAMEL-25751: a Map or a List as the reply - a body unmarshalled from JSON,
XML or CSV - says to marshal it first, in
+ * the data format the Content-Type names.
+ */
+public class VertxPlatformHttpMapResponseTest {
+
+ @Test
+ public void testMapOrListReply() throws Exception {
+ final CamelContext context =
VertxPlatformHttpEngineTest.createCamelContext();
+ final RecordingExceptionHandler handler = new
RecordingExceptionHandler();
+ context.getRegistry().bind("recording", handler);
+
+ try {
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("platform-http:/stock?exceptionHandler=#recording")
+ .process(exchange -> {
+ Map<String, Object> stock = new
LinkedHashMap<>();
+ stock.put("sku", "A1");
+ exchange.getMessage().setBody(stock);
+ });
+ from("platform-http:/lines?exceptionHandler=#recording")
+ .setHeader(Exchange.CONTENT_TYPE,
constant("text/csv"))
+ .process(exchange ->
exchange.getMessage().setBody(List.of(List.of("A1", "2"))));
+ from("platform-http:/order?exceptionHandler=#recording")
+ .setHeader(Exchange.CONTENT_TYPE,
constant("application/xml"))
+ .process(exchange ->
exchange.getMessage().setBody(Map.of("orderId", "1001")));
+ }
+ });
+
+ VertxPlatformHttpEngineTest.startCamelContext(context);
+
+ given().when().get("/stock").then().statusCode(500);
+ given().when().get("/lines").then().statusCode(500);
+ given().when().get("/order").then().statusCode(500);
+
+ assertEquals(3, handler.handled.size(), () -> "Handled: " +
handler.handled);
+ // no Content-Type: the usual data formats
+ String map =
assertInstanceOf(NoTypeConversionAvailableException.class,
handler.handled.get(0)).getMessage();
+ assertTrue(map.startsWith("No type converter available to convert
from type: java.util.LinkedHashMap"
+ + " to the required type:
java.io.InputStream"),
+ map);
+ assertTrue(map.contains("a Map is not an HTTP response body:
marshal it first with the data format of the reply"
+ + " (such as marshal: json, marshal:
jacksonXml or marshal: csv) as the last step of the"
+ + " route, or let the REST binding write
it (bindingMode: json or xml)"),
+ map);
+ // the Content-Type names the data format; the REST binding does
not write csv
+ String csv = handler.handled.get(1).getMessage();
+ assertTrue(csv.contains("a List is not an HTTP response body:
marshal it first (marshal: csv, for the Content-Type"
+ + " text/csv) as the last step of the
route"),
+ csv);
+ assertFalse(csv.contains("REST binding"), csv);
+ String xml = handler.handled.get(2).getMessage();
+ assertTrue(xml.contains("marshal it first (marshal: jacksonXml,
for the Content-Type application/xml) as the last"
+ + " step of the route, or let the REST
binding write it (bindingMode: xml)"),
+ xml);
+ } finally {
+ context.stop();
+ }
+ }
+
+ private static final class RecordingExceptionHandler implements
ExceptionHandler {
+ private final List<Throwable> handled = new CopyOnWriteArrayList<>();
+
+ @Override
+ public void handleException(Throwable exception) {
+ handled.add(exception);
+ }
+
+ @Override
+ public void handleException(String message, Throwable exception) {
+ handled.add(exception);
+ }
+
+ @Override
+ public void handleException(String message, Exchange exchange,
Throwable exception) {
+ handled.add(exception);
+ }
+ }
+}