You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ap...@apache.org on 2026/10/05 11:46:54 UTC

(camel) branch main updated (e008baa436e5 -> f89928713ba7)

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

apupier pushed a change to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


    from e008baa436e5 CAMEL-25336: the YAML validator reports ${...} in a constant, which is used as written (#27369)
     new 6d66f0852a08 CAMEL-25305: camel-netty-http - write a String body in the charset that the Content-Type declares
     new f89928713ba7 CAMEL-25305: camel-netty-http - an empty charset parameter is no charset

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../netty/http/DefaultNettyHttpBinding.java        |  36 +++++-
 .../component/netty/http/NettyHttpConverter.java   |   3 +-
 .../component/netty/http/NettyHttpHelper.java      |  22 ++++
 .../http/handlers/HttpServerChannelHandler.java    |   3 +-
 .../netty/http/NettyHttpStringBodyCharsetTest.java | 136 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |  11 ++
 6 files changed, 204 insertions(+), 7 deletions(-)
 create mode 100644 components/camel-netty-http/src/test/java/org/apache/camel/component/netty/http/NettyHttpStringBodyCharsetTest.java


(camel) 01/02: CAMEL-25305: camel-netty-http - write a String body in the charset that the Content-Type declares

Posted by ap...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

apupier pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 6d66f0852a081d1391ad3ec3837f40c3ec93cf31
Author: smjain <49...@users.noreply.github.com>
AuthorDate: Sat Oct 3 10:48:35 2026 +0530

    CAMEL-25305: camel-netty-http - write a String body in the charset that the Content-Type declares
    
    The consumer (response) and the producer (request) converted a String body
    with the charset of the exchange (UTF-8 by default), also when the
    Content-Type declared another charset, so for example a response with
    text/plain; charset=ISO-8859-1 carried UTF-8 bytes. A String body is now
    written in the declared charset.
    
    The charset of a request Content-Type was found with the deprecated
    HttpUtil.getCharsetFromContentType, which only matches the parameter name in
    lower case; the parameter name is case-insensitive (RFC 9110, section
    5.6.6).
    
    Co-Authored-By: Claude Opus 5.5 <no...@anthropic.com>
---
 .../netty/http/DefaultNettyHttpBinding.java        |  36 +++++-
 .../component/netty/http/NettyHttpConverter.java   |   3 +-
 .../component/netty/http/NettyHttpHelper.java      |  20 ++++
 .../http/handlers/HttpServerChannelHandler.java    |   3 +-
 .../netty/http/NettyHttpStringBodyCharsetTest.java | 121 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |  10 ++
 6 files changed, 186 insertions(+), 7 deletions(-)

diff --git a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/DefaultNettyHttpBinding.java b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/DefaultNettyHttpBinding.java
index 67e8d54704a8..dd73344dc665 100644
--- a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/DefaultNettyHttpBinding.java
+++ b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/DefaultNettyHttpBinding.java
@@ -280,6 +280,26 @@ public class DefaultNettyHttpBinding implements NettyHttpBinding, Cloneable {
 
     }
 
+    /**
+     * Converts a String body with the charset that the content type of the message declares, so the bytes match the
+     * Content-Type header. Returns <tt>null</tt> when the body is not a String or no supported charset is declared, and
+     * the body is then converted with the type converter (the charset of the exchange).
+     */
+    private static ByteBuf toByteBufWithContentTypeCharset(Message message, Object body) {
+        if (body instanceof String str) {
+            String charset = NettyHttpHelper.getCharsetFromContentType(MessageHelper.getContentType(message));
+            if (charset != null) {
+                try {
+                    return NettyConverter.toByteBuffer(str.getBytes(Charset.forName(charset)));
+                } catch (IllegalArgumentException e) {
+                    // unsupported or illegal charset name: keep the conversion with the exchange charset
+                    LOG.debug("Cannot use charset {} of the content type: {}", charset, e.getMessage());
+                }
+            }
+        }
+        return null;
+    }
+
     private static String stripPath(NettyHttpConfiguration configuration, URI uri) {
         String path = uri.getRawPath();
         if (configuration.getPath() != null) {
@@ -493,8 +513,12 @@ public class DefaultNettyHttpBinding implements NettyHttpBinding, Cloneable {
             if (body instanceof ByteBuf byteBuf) {
                 buffer = byteBuf;
             } else {
-                // try to convert to buffer first
-                buffer = message.getBody(ByteBuf.class);
+                // a String body is written in the charset of the content type
+                buffer = toByteBufWithContentTypeCharset(message, body);
+                if (buffer == null) {
+                    // try to convert to buffer first
+                    buffer = message.getBody(ByteBuf.class);
+                }
                 if (buffer == null) {
                     // fallback to byte array as last resort
                     byte[] data = message.getBody(byte[].class);
@@ -652,8 +676,12 @@ public class DefaultNettyHttpBinding implements NettyHttpBinding, Cloneable {
                 if (body instanceof ByteBuf byteBuf) {
                     buffer = byteBuf;
                 } else {
-                    // try to convert to buffer first
-                    buffer = message.getBody(ByteBuf.class);
+                    // a String body is written in the charset of the content type
+                    buffer = toByteBufWithContentTypeCharset(message, body);
+                    if (buffer == null) {
+                        // try to convert to buffer first
+                        buffer = message.getBody(ByteBuf.class);
+                    }
                     if (buffer == null) {
                         // fallback to byte array as last resort
                         byte[] data = message.getMandatoryBody(byte[].class);
diff --git a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/NettyHttpConverter.java b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/NettyHttpConverter.java
index 7ccd8cc8c278..f7afabf4bbdb 100644
--- a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/NettyHttpConverter.java
+++ b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/NettyHttpConverter.java
@@ -30,7 +30,6 @@ import org.apache.camel.Exchange;
 import org.apache.camel.ExchangePropertyKey;
 import org.apache.camel.component.netty.NettyConverter;
 import org.apache.camel.spi.TypeConverterRegistry;
-import org.apache.camel.support.http.HttpUtil;
 
 @Converter(generateLoader = true)
 public final class NettyHttpConverter {
@@ -88,7 +87,7 @@ public final class NettyHttpConverter {
     @Converter
     public static String toString(FullHttpResponse response, Exchange exchange) {
         String contentType = response.headers().get(NettyHttpConstants.CONTENT_TYPE);
-        String charset = HttpUtil.getCharsetFromContentType(contentType);
+        String charset = NettyHttpHelper.getCharsetFromContentType(contentType);
         if (charset == null && exchange != null) {
             charset = exchange.getProperty(ExchangePropertyKey.CHARSET_NAME, String.class);
         }
diff --git a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/NettyHttpHelper.java b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/NettyHttpHelper.java
index 4ce470be55bd..9454acc599d4 100644
--- a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/NettyHttpHelper.java
+++ b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/NettyHttpHelper.java
@@ -56,6 +56,26 @@ public final class NettyHttpHelper {
         CollectionHelper.appendEntry(headers, key, value);
     }
 
+    /**
+     * Gets the charset parameter of the content type. The parameter name is case-insensitive (RFC 9110, section 5.6.6).
+     *
+     * @param  contentType the content type, may be <tt>null</tt>
+     * @return             the charset name, or <tt>null</tt> if the content type has no charset parameter
+     */
+    public static String getCharsetFromContentType(String contentType) {
+        if (contentType == null) {
+            return null;
+        }
+        String[] parts = contentType.split(";");
+        for (int i = 1; i < parts.length; i++) {
+            String part = parts[i].trim();
+            if (part.regionMatches(true, 0, "charset=", 0, 8)) {
+                return IOHelper.normalizeCharset(part.substring(8));
+            }
+        }
+        return null;
+    }
+
     /**
      * Creates the {@link HttpMethod} to use to call the remote server, often either its GET or POST.
      *
diff --git a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/handlers/HttpServerChannelHandler.java b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/handlers/HttpServerChannelHandler.java
index 790ff8cc4903..9720f9ab2521 100644
--- a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/handlers/HttpServerChannelHandler.java
+++ b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/handlers/HttpServerChannelHandler.java
@@ -47,6 +47,7 @@ import org.apache.camel.component.netty.http.InboundStreamHttpRequest;
 import org.apache.camel.component.netty.http.NettyHttpConfiguration;
 import org.apache.camel.component.netty.http.NettyHttpConstants;
 import org.apache.camel.component.netty.http.NettyHttpConsumer;
+import org.apache.camel.component.netty.http.NettyHttpHelper;
 import org.apache.camel.component.netty.http.NettyHttpSecurityConfiguration;
 import org.apache.camel.component.netty.http.SecurityAuthenticator;
 import org.apache.camel.http.base.OAuthHttpSecuritySupport;
@@ -377,7 +378,7 @@ public class HttpServerChannelHandler extends ServerChannelHandler {
 
         // honor the character encoding
         String contentType = in.getHeader(NettyHttpConstants.CONTENT_TYPE, String.class);
-        String charset = org.apache.camel.support.http.HttpUtil.getCharsetFromContentType(contentType);
+        String charset = NettyHttpHelper.getCharsetFromContentType(contentType);
         if (charset != null) {
             exchange.setProperty(ExchangePropertyKey.CHARSET_NAME, charset);
             in.setHeader(NettyHttpConstants.HTTP_CHARACTER_ENCODING, charset);
diff --git a/components/camel-netty-http/src/test/java/org/apache/camel/component/netty/http/NettyHttpStringBodyCharsetTest.java b/components/camel-netty-http/src/test/java/org/apache/camel/component/netty/http/NettyHttpStringBodyCharsetTest.java
new file mode 100644
index 000000000000..727b17ab75c5
--- /dev/null
+++ b/components/camel-netty-http/src/test/java/org/apache/camel/component/netty/http/NettyHttpStringBodyCharsetTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.netty.http;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * A String body is written in the charset that the Content-Type declares, and the charset parameter is recognized
+ * whatever its case.
+ */
+public class NettyHttpStringBodyCharsetTest extends BaseNettyTestSupport {
+
+    private static final String TEXT = "Grüße aus Köln";
+
+    @Test
+    public void testResponseInDeclaredCharset() throws Exception {
+        HttpResponse<byte[]> response = send("/response", "text/plain", TEXT.getBytes(StandardCharsets.UTF_8));
+
+        assertEquals("text/plain; charset=ISO-8859-1", response.headers().firstValue("Content-Type").orElse(null));
+        assertArrayEquals(TEXT.getBytes(StandardCharsets.ISO_8859_1), response.body());
+    }
+
+    @Test
+    public void testResponseWithoutCharsetIsUtf8() throws Exception {
+        HttpResponse<byte[]> response = send("/plain", "text/plain", TEXT.getBytes(StandardCharsets.UTF_8));
+
+        assertArrayEquals(TEXT.getBytes(StandardCharsets.UTF_8), response.body());
+    }
+
+    @Test
+    public void testRequestCharsetParameterIgnoresCase() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:text");
+        mock.expectedBodiesReceived(TEXT);
+
+        send("/text", "text/plain; Charset=ISO-8859-1", TEXT.getBytes(StandardCharsets.ISO_8859_1));
+
+        mock.assertIsSatisfied();
+    }
+
+    @Test
+    public void testProducerRequestInDeclaredCharset() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:bytes");
+        mock.expectedMessageCount(1);
+
+        template.sendBodyAndHeader("netty-http:http://localhost:{{port}}/bytes", TEXT, Exchange.CONTENT_TYPE,
+                "text/plain; charset=ISO-8859-1");
+
+        mock.assertIsSatisfied();
+        assertArrayEquals(TEXT.getBytes(StandardCharsets.ISO_8859_1),
+                mock.getReceivedExchanges().get(0).getMessage().getBody(byte[].class));
+    }
+
+    @Test
+    public void testCharsetFromContentType() {
+        assertEquals("ISO-8859-1", NettyHttpHelper.getCharsetFromContentType("text/plain; Charset=ISO-8859-1"));
+        assertEquals("UTF-8", NettyHttpHelper.getCharsetFromContentType("text/plain;charset=\"UTF-8\""));
+        assertEquals("utf-16", NettyHttpHelper.getCharsetFromContentType("text/plain; format=flowed; CHARSET=utf-16"));
+        // no charset parameter: no charset (not a UTF-8 default), so the exchange charset is used as before
+        assertNull(NettyHttpHelper.getCharsetFromContentType("text/plain"));
+        assertNull(NettyHttpHelper.getCharsetFromContentType("multipart/form-data; boundary=charset"));
+        assertNull(NettyHttpHelper.getCharsetFromContentType(null));
+    }
+
+    private HttpResponse<byte[]> send(String path, String contentType, byte[] body) throws Exception {
+        HttpRequest request = HttpRequest.newBuilder(URI.create("http://localhost:" + getPort() + path))
+                .header("Content-Type", contentType)
+                .POST(HttpRequest.BodyPublishers.ofByteArray(body))
+                .build();
+        return HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofByteArray());
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            public void configure() {
+                from("netty-http:http://localhost:{{port}}/response")
+                        .setHeader(Exchange.CONTENT_TYPE, constant("text/plain; charset=ISO-8859-1"))
+                        .setBody(constant(TEXT));
+
+                from("netty-http:http://localhost:{{port}}/plain")
+                        .setHeader(Exchange.CONTENT_TYPE, constant("text/plain"))
+                        .setBody(constant(TEXT));
+
+                from("netty-http:http://localhost:{{port}}/text")
+                        .convertBodyTo(String.class)
+                        .to("mock:text");
+
+                from("netty-http:http://localhost:{{port}}/bytes")
+                        .convertBodyTo(byte[].class)
+                        .to("mock:bytes");
+            }
+        };
+    }
+}
diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index d65aa7a14049..4746ecf2b297 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -803,6 +803,16 @@ The `charset` parameter of a `Content-Type` was only recognized in lower case. A
 `String` body with such a `Content-Type` was written as UTF-8. The parameter name is now matched case-insensitively
 (RFC 9110), as in the other HTTP components, on the consumer (request and response) and on the producer.
 
+=== camel-netty-http - String bodies in the charset of the Content-Type
+
+The Netty HTTP consumer (response) and producer (request) wrote a `String` body in the charset of the exchange
+(`CamelCharsetName`, UTF-8 by default), even when the `Content-Type` declared another charset (for example
+`text/plain; charset=ISO-8859-1` set by the route). A `String` body is now written in the charset that the
+`Content-Type` declares. Bodies that are not a `String`, and messages whose `Content-Type` declares no charset, are
+sent as before. Also, the `charset` parameter of a received `Content-Type` (a request on the consumer) is now
+recognized whatever its case (`Charset=ISO-8859-1`). A peer that ignored the declared charset and read such a message as UTF-8 must now use the
+declared charset, and characters that the declared charset cannot represent are written as `?`.
+
 === camel-snmp - GET_NEXT (walk)
 
 A `GET_NEXT` walk now ends at the end of the agent's MIB view (`endOfMibView`, or `noSuchName` for SNMPv1) and at


(camel) 02/02: CAMEL-25305: camel-netty-http - an empty charset parameter is no charset

Posted by ap...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

apupier pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git

commit f89928713ba7e0eeaf07633d821d3b8158d05311
Author: smjain <49...@users.noreply.github.com>
AuthorDate: Mon Oct 5 11:40:40 2026 +0530

    CAMEL-25305: camel-netty-http - an empty charset parameter is no charset
    
    A Content-Type with an empty charset parameter (text/plain; charset=) gave
    an empty charset name, which the consumer set as CamelCharsetName, so a later
    conversion failed in Charset.forName(""). Treat it as no charset, as the
    Undertow helper does, and re-wrap the upgrade guide entry.
    
    Co-Authored-By: Claude Opus 5.5 <no...@anthropic.com>
---
 .../camel/component/netty/http/NettyHttpHelper.java       |  6 ++++--
 .../netty/http/NettyHttpStringBodyCharsetTest.java        | 15 +++++++++++++++
 .../modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc   |  5 +++--
 3 files changed, 22 insertions(+), 4 deletions(-)

diff --git a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/NettyHttpHelper.java b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/NettyHttpHelper.java
index 9454acc599d4..f8e4e14dfe32 100644
--- a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/NettyHttpHelper.java
+++ b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/NettyHttpHelper.java
@@ -60,7 +60,8 @@ public final class NettyHttpHelper {
      * Gets the charset parameter of the content type. The parameter name is case-insensitive (RFC 9110, section 5.6.6).
      *
      * @param  contentType the content type, may be <tt>null</tt>
-     * @return             the charset name, or <tt>null</tt> if the content type has no charset parameter
+     * @return             the charset name, or <tt>null</tt> if the content type has no charset parameter, or an empty
+     *                     one
      */
     public static String getCharsetFromContentType(String contentType) {
         if (contentType == null) {
@@ -70,7 +71,8 @@ public final class NettyHttpHelper {
         for (int i = 1; i < parts.length; i++) {
             String part = parts[i].trim();
             if (part.regionMatches(true, 0, "charset=", 0, 8)) {
-                return IOHelper.normalizeCharset(part.substring(8));
+                String name = IOHelper.normalizeCharset(part.substring(8));
+                return ObjectHelper.isEmpty(name) ? null : name;
             }
         }
         return null;
diff --git a/components/camel-netty-http/src/test/java/org/apache/camel/component/netty/http/NettyHttpStringBodyCharsetTest.java b/components/camel-netty-http/src/test/java/org/apache/camel/component/netty/http/NettyHttpStringBodyCharsetTest.java
index 727b17ab75c5..276abcb4ae85 100644
--- a/components/camel-netty-http/src/test/java/org/apache/camel/component/netty/http/NettyHttpStringBodyCharsetTest.java
+++ b/components/camel-netty-http/src/test/java/org/apache/camel/component/netty/http/NettyHttpStringBodyCharsetTest.java
@@ -86,6 +86,21 @@ public class NettyHttpStringBodyCharsetTest extends BaseNettyTestSupport {
         assertNull(NettyHttpHelper.getCharsetFromContentType("text/plain"));
         assertNull(NettyHttpHelper.getCharsetFromContentType("multipart/form-data; boundary=charset"));
         assertNull(NettyHttpHelper.getCharsetFromContentType(null));
+        // an empty charset parameter is no charset
+        assertNull(NettyHttpHelper.getCharsetFromContentType("text/plain; charset="));
+        assertNull(NettyHttpHelper.getCharsetFromContentType("text/plain; charset=\"\""));
+    }
+
+    @Test
+    public void testRequestWithEmptyCharsetParameter() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:text");
+        mock.expectedBodiesReceived(TEXT);
+
+        // an empty charset is ignored: no CamelCharsetName is set, and the body is read as before
+        send("/text", "text/plain; charset=", TEXT.getBytes(StandardCharsets.UTF_8));
+
+        mock.assertIsSatisfied();
+        assertNull(mock.getReceivedExchanges().get(0).getProperty(Exchange.CHARSET_NAME));
     }
 
     private HttpResponse<byte[]> send(String path, String contentType, byte[] body) throws Exception {
diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 4746ecf2b297..df61b679b487 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -810,8 +810,9 @@ The Netty HTTP consumer (response) and producer (request) wrote a `String` body
 `text/plain; charset=ISO-8859-1` set by the route). A `String` body is now written in the charset that the
 `Content-Type` declares. Bodies that are not a `String`, and messages whose `Content-Type` declares no charset, are
 sent as before. Also, the `charset` parameter of a received `Content-Type` (a request on the consumer) is now
-recognized whatever its case (`Charset=ISO-8859-1`). A peer that ignored the declared charset and read such a message as UTF-8 must now use the
-declared charset, and characters that the declared charset cannot represent are written as `?`.
+recognized whatever its case (`Charset=ISO-8859-1`). A peer that ignored the declared charset and read such a
+message as UTF-8 must now use the declared charset, and characters that the declared charset cannot represent are
+written as `?`.
 
 === camel-snmp - GET_NEXT (walk)