This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/master by this push:
new 190659d [CAMEL-15188] Add proxy type query parameter in
camel-telegram, add support for SOCKS proxy (#3915)
190659d is described below
commit 190659d8a03aaa837d2c81a86f699c0c5eb37c3b
Author: iliya-gr <[email protected]>
AuthorDate: Mon Jun 15 10:44:50 2020 +0300
[CAMEL-15188] Add proxy type query parameter in camel-telegram, add support
for SOCKS proxy (#3915)
* [CAMEL-15188] Add proxy type query parameter in camel-telegram, add
support for SOCKS4 and SOCKS5 proxy
* [CAMEL-15188] Add license header, remove javadoc
---
.../telegram/TelegramEndpointConfigurer.java | 5 ++
.../apache/camel/component/telegram/telegram.json | 1 +
.../src/main/docs/telegram-component.adoc | 3 +-
.../component/telegram/TelegramConfiguration.java | 11 +++
.../camel/component/telegram/TelegramEndpoint.java | 23 +++++-
.../component/telegram/TelegramProxyType.java | 21 +++++
.../telegram/TelegramComponentParametersTest.java | 12 +++
.../telegram/TelegramConfigurationTest.java | 3 +-
.../integration/TelegramServiceProxyTest.java | 61 ++++++++++++++
.../dsl/TelegramEndpointBuilderFactory.java | 96 ++++++++++++++++++++++
10 files changed, 232 insertions(+), 4 deletions(-)
diff --git
a/components/camel-telegram/src/generated/java/org/apache/camel/component/telegram/TelegramEndpointConfigurer.java
b/components/camel-telegram/src/generated/java/org/apache/camel/component/telegram/TelegramEndpointConfigurer.java
index 87f8aeb..f44327a 100644
---
a/components/camel-telegram/src/generated/java/org/apache/camel/component/telegram/TelegramEndpointConfigurer.java
+++
b/components/camel-telegram/src/generated/java/org/apache/camel/component/telegram/TelegramEndpointConfigurer.java
@@ -56,6 +56,8 @@ public class TelegramEndpointConfigurer extends
PropertyConfigurerSupport implem
case "proxyHost":
target.getConfiguration().setProxyHost(property(camelContext,
java.lang.String.class, value)); return true;
case "proxyport":
case "proxyPort":
target.getConfiguration().setProxyPort(property(camelContext,
java.lang.Integer.class, value)); return true;
+ case "proxytype":
+ case "proxyType":
target.getConfiguration().setProxyType(property(camelContext,
org.apache.camel.component.telegram.TelegramProxyType.class, value)); return
true;
case "repeatcount":
case "repeatCount": target.setRepeatCount(property(camelContext,
long.class, value)); return true;
case "runlogginglevel":
@@ -102,6 +104,7 @@ public class TelegramEndpointConfigurer extends
PropertyConfigurerSupport implem
answer.put("pollStrategy",
org.apache.camel.spi.PollingConsumerPollStrategy.class);
answer.put("proxyHost", java.lang.String.class);
answer.put("proxyPort", java.lang.Integer.class);
+ answer.put("proxyType",
org.apache.camel.component.telegram.TelegramProxyType.class);
answer.put("repeatCount", long.class);
answer.put("runLoggingLevel", org.apache.camel.LoggingLevel.class);
answer.put("scheduledExecutorService",
java.util.concurrent.ScheduledExecutorService.class);
@@ -157,6 +160,8 @@ public class TelegramEndpointConfigurer extends
PropertyConfigurerSupport implem
case "proxyHost": return target.getConfiguration().getProxyHost();
case "proxyport":
case "proxyPort": return target.getConfiguration().getProxyPort();
+ case "proxytype":
+ case "proxyType": return target.getConfiguration().getProxyType();
case "repeatcount":
case "repeatCount": return target.getRepeatCount();
case "runlogginglevel":
diff --git
a/components/camel-telegram/src/generated/resources/org/apache/camel/component/telegram/telegram.json
b/components/camel-telegram/src/generated/resources/org/apache/camel/component/telegram/telegram.json
index 7f653e3..857dfca 100644
---
a/components/camel-telegram/src/generated/resources/org/apache/camel/component/telegram/telegram.json
+++
b/components/camel-telegram/src/generated/resources/org/apache/camel/component/telegram/telegram.json
@@ -47,6 +47,7 @@
"synchronous": { "kind": "parameter", "displayName": "Synchronous",
"group": "advanced", "label": "advanced", "required": false, "type": "boolean",
"javaType": "boolean", "deprecated": false, "secret": false, "defaultValue":
"false", "description": "Sets whether synchronous processing should be strictly
used, or Camel is allowed to use asynchronous processing (if supported)." },
"proxyHost": { "kind": "parameter", "displayName": "Proxy Host", "group":
"proxy", "label": "proxy", "required": false, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "secret": false, "configurationClass":
"org.apache.camel.component.telegram.TelegramConfiguration",
"configurationField": "configuration", "description": "HTTP proxy host which
could be used when sending out the message." },
"proxyPort": { "kind": "parameter", "displayName": "Proxy Port", "group":
"proxy", "label": "proxy", "required": false, "type": "integer", "javaType":
"java.lang.Integer", "deprecated": false, "secret": false,
"configurationClass":
"org.apache.camel.component.telegram.TelegramConfiguration",
"configurationField": "configuration", "description": "HTTP proxy port which
could be used when sending out the message." },
+ "proxyType": { "kind": "parameter", "displayName": "Proxy Type", "group":
"proxy", "label": "proxy", "required": false, "type": "object", "javaType":
"org.apache.camel.component.telegram.TelegramProxyType", "enum": [ "HTTP",
"SOCKS4", "SOCKS5" ], "deprecated": false, "secret": false, "defaultValue":
"HTTP", "configurationClass":
"org.apache.camel.component.telegram.TelegramConfiguration",
"configurationField": "configuration", "description": "HTTP proxy type which
could be used when [...]
"backoffErrorThreshold": { "kind": "parameter", "displayName": "Backoff
Error Threshold", "group": "scheduler", "label": "consumer,scheduler",
"required": false, "type": "integer", "javaType": "int", "deprecated": false,
"secret": false, "description": "The number of subsequent error polls (failed
due some error) that should happen before the backoffMultipler should kick-in."
},
"backoffIdleThreshold": { "kind": "parameter", "displayName": "Backoff
Idle Threshold", "group": "scheduler", "label": "consumer,scheduler",
"required": false, "type": "integer", "javaType": "int", "deprecated": false,
"secret": false, "description": "The number of subsequent idle polls that
should happen before the backoffMultipler should kick-in." },
"backoffMultiplier": { "kind": "parameter", "displayName": "Backoff
Multiplier", "group": "scheduler", "label": "consumer,scheduler", "required":
false, "type": "integer", "javaType": "int", "deprecated": false, "secret":
false, "description": "To let the scheduled polling consumer backoff if there
has been a number of subsequent idles\/errors in a row. The multiplier is then
the number of polls that will be skipped before the next actual attempt is
happening again. When this option [...]
diff --git a/components/camel-telegram/src/main/docs/telegram-component.adoc
b/components/camel-telegram/src/main/docs/telegram-component.adoc
index 851a01c..0230e95 100644
--- a/components/camel-telegram/src/main/docs/telegram-component.adoc
+++ b/components/camel-telegram/src/main/docs/telegram-component.adoc
@@ -93,7 +93,7 @@ with the following path and query parameters:
|===
-=== Query Parameters (31 parameters):
+=== Query Parameters (32 parameters):
[width="100%",cols="2,5,^1,2",options="header"]
@@ -115,6 +115,7 @@ with the following path and query parameters:
| *synchronous* (advanced) | Sets whether synchronous processing should be
strictly used, or Camel is allowed to use asynchronous processing (if
supported). | false | boolean
| *proxyHost* (proxy) | HTTP proxy host which could be used when sending out
the message. | | String
| *proxyPort* (proxy) | HTTP proxy port which could be used when sending out
the message. | | Integer
+| *proxyType* (proxy) | HTTP proxy type which could be used when sending out
the message. The value can be one of: HTTP, SOCKS4, SOCKS5 | HTTP |
TelegramProxyType
| *backoffErrorThreshold* (scheduler) | The number of subsequent error polls
(failed due some error) that should happen before the backoffMultipler should
kick-in. | | int
| *backoffIdleThreshold* (scheduler) | The number of subsequent idle polls
that should happen before the backoffMultipler should kick-in. | | int
| *backoffMultiplier* (scheduler) | To let the scheduled polling consumer
backoff if there has been a number of subsequent idles/errors in a row. The
multiplier is then the number of polls that will be skipped before the next
actual attempt is happening again. When this option is in use then
backoffIdleThreshold and/or backoffErrorThreshold must also be configured. | |
int
diff --git
a/components/camel-telegram/src/main/java/org/apache/camel/component/telegram/TelegramConfiguration.java
b/components/camel-telegram/src/main/java/org/apache/camel/component/telegram/TelegramConfiguration.java
index 1cc6695..5d70998 100644
---
a/components/camel-telegram/src/main/java/org/apache/camel/component/telegram/TelegramConfiguration.java
+++
b/components/camel-telegram/src/main/java/org/apache/camel/component/telegram/TelegramConfiguration.java
@@ -43,6 +43,9 @@ public class TelegramConfiguration {
@UriParam(description = "HTTP proxy port which could be used when sending
out the message.", label = "proxy")
private Integer proxyPort;
+ @UriParam(description = "HTTP proxy type which could be used when sending
out the message.", label = "proxy", defaultValue = "HTTP")
+ private TelegramProxyType proxyType = TelegramProxyType.HTTP;
+
@UriParam(description = "The identifier of the chat that will receive the
produced messages. Chat ids can be first obtained from incoming messages "
+ "(eg. when a telegram user starts a conversation with a bot, its
client sends automatically a '/start' message containing the chat id). "
+ "It is an optional parameter, as the chat id can be set
dynamically for each outgoing message (using body or headers).", label =
"producer")
@@ -94,6 +97,14 @@ public class TelegramConfiguration {
this.proxyPort = proxyPort;
}
+ public TelegramProxyType getProxyType() {
+ return proxyType;
+ }
+
+ public void setProxyType(TelegramProxyType proxyType) {
+ this.proxyType = proxyType;
+ }
+
public String getChatId() {
return chatId;
}
diff --git
a/components/camel-telegram/src/main/java/org/apache/camel/component/telegram/TelegramEndpoint.java
b/components/camel-telegram/src/main/java/org/apache/camel/component/telegram/TelegramEndpoint.java
index 389ff47..57d25c5 100644
---
a/components/camel-telegram/src/main/java/org/apache/camel/component/telegram/TelegramEndpoint.java
+++
b/components/camel-telegram/src/main/java/org/apache/camel/component/telegram/TelegramEndpoint.java
@@ -39,6 +39,7 @@ import org.asynchttpclient.AsyncHttpClientConfig;
import org.asynchttpclient.DefaultAsyncHttpClient;
import org.asynchttpclient.DefaultAsyncHttpClientConfig;
import org.asynchttpclient.proxy.ProxyServer;
+import org.asynchttpclient.proxy.ProxyType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -85,10 +86,13 @@ public class TelegramEndpoint extends ScheduledPollEndpoint
implements WebhookCa
if (configuration != null &&
ObjectHelper.isNotEmpty(configuration.getProxyHost())
&& ObjectHelper.isNotEmpty(configuration.getProxyPort())) {
- LOG.debug("Setup http proxy host:{} port:{} for
TelegramService", configuration.getProxyHost(),
+ LOG.debug("Setup {} proxy host:{} port:{} for TelegramService",
+ configuration.getProxyType(),
+ configuration.getProxyHost(),
configuration.getProxyPort());
builder.setProxyServer(
- new ProxyServer.Builder(configuration.getProxyHost(),
configuration.getProxyPort()).build());
+ new ProxyServer.Builder(configuration.getProxyHost(),
configuration.getProxyPort())
+
.setProxyType(getProxyType(configuration.getProxyType())).build());
}
final AsyncHttpClientConfig config = builder.build();
client = new DefaultAsyncHttpClient(config);
@@ -208,4 +212,19 @@ public class TelegramEndpoint extends
ScheduledPollEndpoint implements WebhookCa
this.bufferSize = bufferSize;
}
+ private ProxyType getProxyType(TelegramProxyType type) {
+ if (type == null) {
+ return ProxyType.HTTP;
+ }
+
+ switch (type) {
+ case HTTP:
+ return ProxyType.HTTP;
+ case SOCKS4:
+ return ProxyType.SOCKS_V4;
+ case SOCKS5:
+ return ProxyType.SOCKS_V5;
+ }
+ throw new IllegalArgumentException("Unknown proxy type: " + type);
+ }
}
diff --git
a/components/camel-telegram/src/main/java/org/apache/camel/component/telegram/TelegramProxyType.java
b/components/camel-telegram/src/main/java/org/apache/camel/component/telegram/TelegramProxyType.java
new file mode 100644
index 0000000..e9473e0
--- /dev/null
+++
b/components/camel-telegram/src/main/java/org/apache/camel/component/telegram/TelegramProxyType.java
@@ -0,0 +1,21 @@
+/*
+ * 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.telegram;
+
+public enum TelegramProxyType {
+ HTTP, SOCKS4, SOCKS5
+}
diff --git
a/components/camel-telegram/src/test/java/org/apache/camel/component/telegram/TelegramComponentParametersTest.java
b/components/camel-telegram/src/test/java/org/apache/camel/component/telegram/TelegramComponentParametersTest.java
index 05defa8..27ce893 100644
---
a/components/camel-telegram/src/test/java/org/apache/camel/component/telegram/TelegramComponentParametersTest.java
+++
b/components/camel-telegram/src/test/java/org/apache/camel/component/telegram/TelegramComponentParametersTest.java
@@ -16,6 +16,8 @@
*/
package org.apache.camel.component.telegram;
+import org.apache.camel.PropertyBindingException;
+import org.apache.camel.TypeConversionException;
import org.apache.camel.component.telegram.util.TelegramTestSupport;
import org.junit.jupiter.api.Test;
@@ -33,6 +35,7 @@ public class TelegramComponentParametersTest extends
TelegramTestSupport {
TelegramEndpoint ep1 = (TelegramEndpoint)
component.createEndpoint("telegram:bots");
assertEquals("DEFAULT",
ep1.getConfiguration().getAuthorizationToken());
+ assertEquals(TelegramProxyType.HTTP,
ep1.getConfiguration().getProxyType());
TelegramEndpoint ep2 = (TelegramEndpoint)
component.createEndpoint("telegram:bots?authorizationToken=CUSTOM");
assertEquals("CUSTOM", ep2.getConfiguration().getAuthorizationToken());
@@ -68,4 +71,13 @@ public class TelegramComponentParametersTest extends
TelegramTestSupport {
});
}
+ @Test
+ public void testWrongURI3() {
+ assertThrows(PropertyBindingException.class, () -> {
+ TelegramComponent component =
(TelegramComponent)context().getComponent("telegram");
+ component.setAuthorizationToken("ANY");
+ component.createEndpoint("telegram:bots?proxyType=ANY");
+ });
+ }
+
}
diff --git
a/components/camel-telegram/src/test/java/org/apache/camel/component/telegram/TelegramConfigurationTest.java
b/components/camel-telegram/src/test/java/org/apache/camel/component/telegram/TelegramConfigurationTest.java
index 1abb14c..9ba9ebf 100644
---
a/components/camel-telegram/src/test/java/org/apache/camel/component/telegram/TelegramConfigurationTest.java
+++
b/components/camel-telegram/src/test/java/org/apache/camel/component/telegram/TelegramConfigurationTest.java
@@ -41,6 +41,7 @@ public class TelegramConfigurationTest extends
TelegramTestSupport {
assertEquals(Integer.valueOf(60), config.getLimit());
assertEquals("127.0.0.1", config.getProxyHost());
assertEquals(Integer.valueOf(1234), config.getProxyPort());
+ assertEquals(TelegramProxyType.SOCKS5, config.getProxyType());
}
@@ -51,7 +52,7 @@ public class TelegramConfigurationTest extends
TelegramTestSupport {
public void configure() throws Exception {
from("direct:telegram")
-
.to("telegram:bots/?authorizationToken=mock-token&chatId=12345&delay=2000&timeout=10&limit=60&proxyHost=127.0.0.1&proxyPort=1234");
+
.to("telegram:bots/?authorizationToken=mock-token&chatId=12345&delay=2000&timeout=10&limit=60&proxyHost=127.0.0.1&proxyPort=1234&proxyType=SOCKS5");
}
};
}
diff --git
a/components/camel-telegram/src/test/java/org/apache/camel/component/telegram/integration/TelegramServiceProxyTest.java
b/components/camel-telegram/src/test/java/org/apache/camel/component/telegram/integration/TelegramServiceProxyTest.java
new file mode 100644
index 0000000..6c33924
--- /dev/null
+++
b/components/camel-telegram/src/test/java/org/apache/camel/component/telegram/integration/TelegramServiceProxyTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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.telegram.integration;
+
+import org.apache.camel.component.telegram.model.IncomingMessage;
+import org.apache.camel.component.telegram.model.OutgoingTextMessage;
+import org.apache.camel.component.telegram.util.TelegramApiConfig;
+import org.apache.camel.component.telegram.util.TelegramTestSupport;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+public class TelegramServiceProxyTest extends TelegramTestSupport {
+
+ private static String proxyHost;
+ private static String proxyPort;
+ private static String proxyType;
+
+ @BeforeAll
+ public static void configureProxyFromEnv() {
+ proxyHost = System.getenv("TELEGRAM_PROXY_HOST");
+ proxyPort = System.getenv("TELEGRAM_PROXY_PORT");
+ proxyType = System.getenv("TELEGRAM_PROXY_TYPE");
+ }
+
+ protected TelegramApiConfig getTelegramApiConfig() {
+ return TelegramApiConfig.fromEnv();
+ }
+
+ @Test
+ public void testGetUpdates() {
+ IncomingMessage res = consumer.receiveBody(
+
String.format("telegram://bots?proxyHost=%s&proxyPort=%s&proxyType=%s",
proxyHost, proxyPort, proxyType), 5000, IncomingMessage.class);
+ assertNotNull(res);
+ }
+
+ @Test
+ public void testSendMessage() {
+ OutgoingTextMessage msg = new OutgoingTextMessage();
+ msg.setChatId(chatId);
+ msg.setText("This is an auto-generated message from the Bot");
+ template.requestBody(
+
String.format("telegram://bots?chatId=%s&proxyHost=%s&proxyPort=%s&proxyType=%s",
chatId, proxyHost, proxyPort, proxyType), msg);
+ }
+
+}
diff --git
a/core/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/TelegramEndpointBuilderFactory.java
b/core/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/TelegramEndpointBuilderFactory.java
index 31047b3..0269def 100644
---
a/core/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/TelegramEndpointBuilderFactory.java
+++
b/core/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/TelegramEndpointBuilderFactory.java
@@ -203,6 +203,35 @@ public interface TelegramEndpointBuilderFactory {
return this;
}
/**
+ * HTTP proxy type which could be used when sending out the message.
+ *
+ * The option is a:
+ * <code>org.apache.camel.component.telegram.TelegramProxyType</code>
+ * type.
+ *
+ * Default: HTTP
+ * Group: proxy
+ */
+ default TelegramEndpointConsumerBuilder proxyType(
+ TelegramProxyType proxyType) {
+ doSetProperty("proxyType", proxyType);
+ return this;
+ }
+ /**
+ * HTTP proxy type which could be used when sending out the message.
+ *
+ * The option will be converted to a
+ * <code>org.apache.camel.component.telegram.TelegramProxyType</code>
+ * type.
+ *
+ * Default: HTTP
+ * Group: proxy
+ */
+ default TelegramEndpointConsumerBuilder proxyType(String proxyType) {
+ doSetProperty("proxyType", proxyType);
+ return this;
+ }
+ /**
* The number of subsequent error polls (failed due some error) that
* should happen before the backoffMultipler should kick-in.
*
@@ -916,6 +945,35 @@ public interface TelegramEndpointBuilderFactory {
return this;
}
/**
+ * HTTP proxy type which could be used when sending out the message.
+ *
+ * The option is a:
+ * <code>org.apache.camel.component.telegram.TelegramProxyType</code>
+ * type.
+ *
+ * Default: HTTP
+ * Group: proxy
+ */
+ default TelegramEndpointProducerBuilder proxyType(
+ TelegramProxyType proxyType) {
+ doSetProperty("proxyType", proxyType);
+ return this;
+ }
+ /**
+ * HTTP proxy type which could be used when sending out the message.
+ *
+ * The option will be converted to a
+ * <code>org.apache.camel.component.telegram.TelegramProxyType</code>
+ * type.
+ *
+ * Default: HTTP
+ * Group: proxy
+ */
+ default TelegramEndpointProducerBuilder proxyType(String proxyType) {
+ doSetProperty("proxyType", proxyType);
+ return this;
+ }
+ /**
* The authorization token for using the bot (ask the BotFather).
*
* The option is a: <code>java.lang.String</code> type.
@@ -1110,6 +1168,34 @@ public interface TelegramEndpointBuilderFactory {
return this;
}
/**
+ * HTTP proxy type which could be used when sending out the message.
+ *
+ * The option is a:
+ * <code>org.apache.camel.component.telegram.TelegramProxyType</code>
+ * type.
+ *
+ * Default: HTTP
+ * Group: proxy
+ */
+ default TelegramEndpointBuilder proxyType(TelegramProxyType proxyType)
{
+ doSetProperty("proxyType", proxyType);
+ return this;
+ }
+ /**
+ * HTTP proxy type which could be used when sending out the message.
+ *
+ * The option will be converted to a
+ * <code>org.apache.camel.component.telegram.TelegramProxyType</code>
+ * type.
+ *
+ * Default: HTTP
+ * Group: proxy
+ */
+ default TelegramEndpointBuilder proxyType(String proxyType) {
+ doSetProperty("proxyType", proxyType);
+ return this;
+ }
+ /**
* The authorization token for using the bot (ask the BotFather).
*
* The option is a: <code>java.lang.String</code> type.
@@ -1254,6 +1340,16 @@ public interface TelegramEndpointBuilderFactory {
}
}
+ /**
+ * Proxy enum for
+ * <code>org.apache.camel.component.telegram.TelegramProxyType</code> enum.
+ */
+ enum TelegramProxyType {
+ HTTP,
+ SOCKS4,
+ SOCKS5;
+ }
+
public interface TelegramBuilders {
/**
* Telegram (camel-telegram)