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 7bf4337815dc CAMEL-26171: camel-xchange - convert currency and
currency pair text, and fail an endpoint with a method of another service
(#29001)
7bf4337815dc is described below
commit 7bf4337815dcf6857fde01b015cd51374ccc920d
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Nov 3 13:59:34 2026 +0100
CAMEL-26171: camel-xchange - convert currency and currency pair text, and
fail an endpoint with a method of another service (#29001)
… and fail an endpoint with a method of another service
The currency option failed with no type converter from String to Currency,
and text in the Currency or CurrencyPair header or body was not converted,
so the call failed with "Null currency" or "Null currency pair". Add type
converters from String to Currency and CurrencyPair that only look up known
currencies. currencyPairMetaData with no pair now reports "Null currency
pair". An endpoint with no method, or a method of another service, now fails
when it is created instead of leaving the message unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../apache/camel/catalog/components/xchange.json | 4 +-
.../camel/catalog/docs/xchange-component.adoc | 22 +++
.../component/xchange/XChangeConvertersLoader.java | 68 +++++++++
.../apache/camel/component/xchange/xchange.json | 4 +-
.../services/org/apache/camel/TypeConverterLoader | 2 +
.../src/main/docs/xchange-component.adoc | 22 +++
.../camel/component/xchange/XChangeComponent.java | 13 ++
.../component/xchange/XChangeConfiguration.java | 29 +++-
.../camel/component/xchange/XChangeConverters.java | 56 +++++++
.../camel/component/xchange/XChangeEndpoint.java | 11 +-
.../xchange/XChangeCurrencyConversionTest.java | 170 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 7 +
12 files changed, 390 insertions(+), 18 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/xchange.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/xchange.json
index cc1f03c0412f..7423303ed398 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/xchange.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/xchange.json
@@ -32,8 +32,8 @@
"CurrencyPair": { "index": 1, "kind": "header", "displayName": "",
"group": "producer", "label": "", "required": false, "javaType":
"org.knowm.xchange.currency.CurrencyPair", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "description": "The
target currency pair", "constantName":
"org.apache.camel.component.xchange.XChangeConfiguration#HEADER_CURRENCY_PAIR" }
},
"bodyTypes": {
- "producerIn": { "carriers": [ "any" ], "description": "Read only by ticker
and currencyPairMetaData, as a CurrencyPair, and by currencyMetaData, as a
Currency, when the CurrencyPair or Currency header is not set. Text such as
BTC\/USDT is not converted: use the currencyPair option, or a CurrencyPair
object in the body or the header" },
- "producerOut": { "carriers": [ "pojo", "list", "null", "unchanged" ],
"byOption": [ { "option": "method", "value": "ticker", "carriers": [ "pojo" ],
"javaType": "org.knowm.xchange.dto.marketdata.Ticker" }, { "option": "method",
"value": "currencies", "carriers": [ "list" ], "javaType":
"java.util.List<org.knowm.xchange.currency.Currency>" }, { "option": "method",
"value": "currencyPairs", "carriers": [ "list" ], "javaType":
"java.util.List<org.knowm.xchange.currency.CurrencyPair>" }, [...]
+ "producerIn": { "carriers": [ "any" ], "description": "Read only by ticker
and currencyPairMetaData, as a CurrencyPair, and by currencyMetaData, as a
Currency, when the CurrencyPair or Currency header is not set. Text such as
BTC\/USDT or BTC is converted; a currency XChange does not know fails" },
+ "producerOut": { "carriers": [ "pojo", "list", "null" ], "byOption": [ {
"option": "method", "value": "ticker", "carriers": [ "pojo" ], "javaType":
"org.knowm.xchange.dto.marketdata.Ticker" }, { "option": "method", "value":
"currencies", "carriers": [ "list" ], "javaType":
"java.util.List<org.knowm.xchange.currency.Currency>" }, { "option": "method",
"value": "currencyPairs", "carriers": [ "list" ], "javaType":
"java.util.List<org.knowm.xchange.currency.CurrencyPair>" }, { "option": [...]
},
"properties": {
"name": { "index": 0, "kind": "path", "displayName": "Name", "group":
"producer", "label": "", "required": true, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "configurationClass":
"org.apache.camel.component.xchange.XChangeConfiguration",
"configurationField": "configuration", "description": "The exchange to connect
to" },
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/xchange-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/xchange-component.adoc
index 034f5d10628f..19447778ad20 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/xchange-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/xchange-component.adoc
@@ -65,6 +65,28 @@ apiKey = GuRW0*********
secretKey = nKLki************
----
+=== Services and methods
+
+Each method belongs to one service, and an endpoint with a method of another
service fails when it is created:
+
+[width="100%",cols="1,3",options="header"]
+|===
+| Service | Methods
+| `account` | `balances`, `fundingHistory`, `wallets`
+| `marketdata` | `ticker`
+| `metadata` | `currencies`, `currencyMetaData`, `currencyPairs`,
`currencyPairMetaData`
+|===
+
+=== Currency and currency pair
+
+The `ticker` and `currencyPairMetaData` methods use the currency pair of the
`CurrencyPair` header, else of the
+message body, else of the `currencyPair` option. The `currencyMetaData` method
uses the currency of the `Currency`
+header, else of the message body, else of the `currency` option.
+
+The header, the body and the option can be an XChange `CurrencyPair` or
`Currency`, or text such as `BTC/USDT` and
+`BTC`. Text in the header or the body with a currency that XChange does not
know fails the exchange: Camel does not
+register a new currency in XChange for text that comes with a message.
+
== Examples
In this sample we find the current Bitcoin market price in USDT:
diff --git
a/components/camel-xchange/src/generated/java/org/apache/camel/component/xchange/XChangeConvertersLoader.java
b/components/camel-xchange/src/generated/java/org/apache/camel/component/xchange/XChangeConvertersLoader.java
new file mode 100644
index 000000000000..1593ba694e18
--- /dev/null
+++
b/components/camel-xchange/src/generated/java/org/apache/camel/component/xchange/XChangeConvertersLoader.java
@@ -0,0 +1,68 @@
+/* Generated by camel build tools - do NOT edit this file! */
+package org.apache.camel.component.xchange;
+
+import javax.annotation.processing.Generated;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.CamelContextAware;
+import org.apache.camel.DeferredContextBinding;
+import org.apache.camel.Exchange;
+import org.apache.camel.TypeConversionException;
+import org.apache.camel.TypeConverterLoaderException;
+import org.apache.camel.spi.TypeConverterLoader;
+import org.apache.camel.spi.TypeConverterRegistry;
+import org.apache.camel.support.SimpleTypeConverter;
+import org.apache.camel.support.TypeConverterSupport;
+import org.apache.camel.util.DoubleMap;
+
+/**
+ * Generated by camel build tools - do NOT edit this file!
+ */
+@Generated("org.apache.camel.maven.packaging.TypeConverterLoaderGeneratorMojo")
+@SuppressWarnings("unchecked")
+@DeferredContextBinding
+public final class XChangeConvertersLoader implements TypeConverterLoader,
CamelContextAware {
+
+ private CamelContext camelContext;
+
+ public XChangeConvertersLoader() {
+ }
+
+ @Override
+ public void setCamelContext(CamelContext camelContext) {
+ this.camelContext = camelContext;
+ }
+
+ @Override
+ public CamelContext getCamelContext() {
+ return camelContext;
+ }
+
+ @Override
+ public void load(TypeConverterRegistry registry) throws
TypeConverterLoaderException {
+ registerConverters(registry);
+ }
+
+ private void registerConverters(TypeConverterRegistry registry) {
+ addTypeConverter(registry, org.knowm.xchange.currency.Currency.class,
java.lang.String.class, false,
+ (type, exchange, value) -> {
+ Object answer =
org.apache.camel.component.xchange.XChangeConverters.toCurrency((java.lang.String)
value);
+ if (false && answer == null) {
+ answer = Void.class;
+ }
+ return answer;
+ });
+ addTypeConverter(registry,
org.knowm.xchange.currency.CurrencyPair.class, java.lang.String.class, false,
+ (type, exchange, value) -> {
+ Object answer =
org.apache.camel.component.xchange.XChangeConverters.toCurrencyPair((java.lang.String)
value);
+ if (false && answer == null) {
+ answer = Void.class;
+ }
+ return answer;
+ });
+ }
+
+ private static void addTypeConverter(TypeConverterRegistry registry,
Class<?> toType, Class<?> fromType, boolean allowNull,
SimpleTypeConverter.ConversionMethod method) {
+ registry.addTypeConverter(toType, fromType, new
SimpleTypeConverter(allowNull, method));
+ }
+}
diff --git
a/components/camel-xchange/src/generated/resources/META-INF/org/apache/camel/component/xchange/xchange.json
b/components/camel-xchange/src/generated/resources/META-INF/org/apache/camel/component/xchange/xchange.json
index cc1f03c0412f..7423303ed398 100644
---
a/components/camel-xchange/src/generated/resources/META-INF/org/apache/camel/component/xchange/xchange.json
+++
b/components/camel-xchange/src/generated/resources/META-INF/org/apache/camel/component/xchange/xchange.json
@@ -32,8 +32,8 @@
"CurrencyPair": { "index": 1, "kind": "header", "displayName": "",
"group": "producer", "label": "", "required": false, "javaType":
"org.knowm.xchange.currency.CurrencyPair", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "description": "The
target currency pair", "constantName":
"org.apache.camel.component.xchange.XChangeConfiguration#HEADER_CURRENCY_PAIR" }
},
"bodyTypes": {
- "producerIn": { "carriers": [ "any" ], "description": "Read only by ticker
and currencyPairMetaData, as a CurrencyPair, and by currencyMetaData, as a
Currency, when the CurrencyPair or Currency header is not set. Text such as
BTC\/USDT is not converted: use the currencyPair option, or a CurrencyPair
object in the body or the header" },
- "producerOut": { "carriers": [ "pojo", "list", "null", "unchanged" ],
"byOption": [ { "option": "method", "value": "ticker", "carriers": [ "pojo" ],
"javaType": "org.knowm.xchange.dto.marketdata.Ticker" }, { "option": "method",
"value": "currencies", "carriers": [ "list" ], "javaType":
"java.util.List<org.knowm.xchange.currency.Currency>" }, { "option": "method",
"value": "currencyPairs", "carriers": [ "list" ], "javaType":
"java.util.List<org.knowm.xchange.currency.CurrencyPair>" }, [...]
+ "producerIn": { "carriers": [ "any" ], "description": "Read only by ticker
and currencyPairMetaData, as a CurrencyPair, and by currencyMetaData, as a
Currency, when the CurrencyPair or Currency header is not set. Text such as
BTC\/USDT or BTC is converted; a currency XChange does not know fails" },
+ "producerOut": { "carriers": [ "pojo", "list", "null" ], "byOption": [ {
"option": "method", "value": "ticker", "carriers": [ "pojo" ], "javaType":
"org.knowm.xchange.dto.marketdata.Ticker" }, { "option": "method", "value":
"currencies", "carriers": [ "list" ], "javaType":
"java.util.List<org.knowm.xchange.currency.Currency>" }, { "option": "method",
"value": "currencyPairs", "carriers": [ "list" ], "javaType":
"java.util.List<org.knowm.xchange.currency.CurrencyPair>" }, { "option": [...]
},
"properties": {
"name": { "index": 0, "kind": "path", "displayName": "Name", "group":
"producer", "label": "", "required": true, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "configurationClass":
"org.apache.camel.component.xchange.XChangeConfiguration",
"configurationField": "configuration", "description": "The exchange to connect
to" },
diff --git
a/components/camel-xchange/src/generated/resources/META-INF/services/org/apache/camel/TypeConverterLoader
b/components/camel-xchange/src/generated/resources/META-INF/services/org/apache/camel/TypeConverterLoader
new file mode 100644
index 000000000000..e87b4f7313f7
--- /dev/null
+++
b/components/camel-xchange/src/generated/resources/META-INF/services/org/apache/camel/TypeConverterLoader
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+org.apache.camel.component.xchange.XChangeConvertersLoader
diff --git a/components/camel-xchange/src/main/docs/xchange-component.adoc
b/components/camel-xchange/src/main/docs/xchange-component.adoc
index 034f5d10628f..19447778ad20 100644
--- a/components/camel-xchange/src/main/docs/xchange-component.adoc
+++ b/components/camel-xchange/src/main/docs/xchange-component.adoc
@@ -65,6 +65,28 @@ apiKey = GuRW0*********
secretKey = nKLki************
----
+=== Services and methods
+
+Each method belongs to one service, and an endpoint with a method of another
service fails when it is created:
+
+[width="100%",cols="1,3",options="header"]
+|===
+| Service | Methods
+| `account` | `balances`, `fundingHistory`, `wallets`
+| `marketdata` | `ticker`
+| `metadata` | `currencies`, `currencyMetaData`, `currencyPairs`,
`currencyPairMetaData`
+|===
+
+=== Currency and currency pair
+
+The `ticker` and `currencyPairMetaData` methods use the currency pair of the
`CurrencyPair` header, else of the
+message body, else of the `currencyPair` option. The `currencyMetaData` method
uses the currency of the `Currency`
+header, else of the message body, else of the `currency` option.
+
+The header, the body and the option can be an XChange `CurrencyPair` or
`Currency`, or text such as `BTC/USDT` and
+`BTC`. Text in the header or the body with a currency that XChange does not
know fails the exchange: Camel does not
+register a new currency in XChange for text that comes with a message.
+
== Examples
In this sample we find the current Bitcoin market price in USDT:
diff --git
a/components/camel-xchange/src/main/java/org/apache/camel/component/xchange/XChangeComponent.java
b/components/camel-xchange/src/main/java/org/apache/camel/component/xchange/XChangeComponent.java
index b08e58f2d125..650a9bafb159 100644
---
a/components/camel-xchange/src/main/java/org/apache/camel/component/xchange/XChangeComponent.java
+++
b/components/camel-xchange/src/main/java/org/apache/camel/component/xchange/XChangeComponent.java
@@ -20,6 +20,8 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.camel.Endpoint;
+import org.apache.camel.component.xchange.XChangeConfiguration.XChangeMethod;
+import org.apache.camel.component.xchange.XChangeConfiguration.XChangeService;
import org.apache.camel.spi.annotations.Component;
import org.apache.camel.support.DefaultComponent;
import org.knowm.xchange.Exchange;
@@ -39,6 +41,17 @@ public class XChangeComponent extends DefaultComponent {
XChangeEndpoint endpoint = new XChangeEndpoint(uri, this,
configuration);
setProperties(endpoint, parameters);
+ XChangeService service = configuration.getService();
+ XChangeMethod method = configuration.getMethod();
+ if (service == null || method == null) {
+ throw new IllegalArgumentException("The service and method options
are required");
+ }
+ if (method.getService() != service) {
+ throw new IllegalArgumentException(
+ "The method " + method + " belongs to the " +
method.getService() + " service, not to the " + service
+ + " service");
+ }
+
// after configuring endpoint then get or create xchange instance
XChange xchange = getOrCreateXChange(remaining);
endpoint.setXchange(xchange);
diff --git
a/components/camel-xchange/src/main/java/org/apache/camel/component/xchange/XChangeConfiguration.java
b/components/camel-xchange/src/main/java/org/apache/camel/component/xchange/XChangeConfiguration.java
index d8a0c41b3576..2c751972a219 100644
---
a/components/camel-xchange/src/main/java/org/apache/camel/component/xchange/XChangeConfiguration.java
+++
b/components/camel-xchange/src/main/java/org/apache/camel/component/xchange/XChangeConfiguration.java
@@ -36,16 +36,29 @@ public class XChangeConfiguration {
// Available methods
public enum XChangeMethod {
// Account service methods
- balances,
- fundingHistory,
- wallets,
+ balances(XChangeService.account),
+ fundingHistory(XChangeService.account),
+ wallets(XChangeService.account),
// Metadata service methods
- currencies,
- currencyMetaData,
- currencyPairs,
- currencyPairMetaData,
+ currencies(XChangeService.metadata),
+ currencyMetaData(XChangeService.metadata),
+ currencyPairs(XChangeService.metadata),
+ currencyPairMetaData(XChangeService.metadata),
// Marketdata service methods
- ticker
+ ticker(XChangeService.marketdata);
+
+ private final XChangeService service;
+
+ XChangeMethod(XChangeService service) {
+ this.service = service;
+ }
+
+ /**
+ * The service the method belongs to
+ */
+ public XChangeService getService() {
+ return service;
+ }
}
@Metadata(description = "The target currency", javaType =
"org.knowm.xchange.currency.Currency")
diff --git
a/components/camel-xchange/src/main/java/org/apache/camel/component/xchange/XChangeConverters.java
b/components/camel-xchange/src/main/java/org/apache/camel/component/xchange/XChangeConverters.java
new file mode 100644
index 000000000000..996971b2909f
--- /dev/null
+++
b/components/camel-xchange/src/main/java/org/apache/camel/component/xchange/XChangeConverters.java
@@ -0,0 +1,56 @@
+/*
+ * 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.xchange;
+
+import org.apache.camel.Converter;
+import org.knowm.xchange.currency.Currency;
+import org.knowm.xchange.currency.CurrencyPair;
+
+/**
+ * Converts the text of a currency, such as BTC, and of a currency pair, such
as BTC/USDT, to the XChange
+ * {@link Currency} and {@link CurrencyPair}.
+ * <p>
+ * Only currencies XChange knows are converted: the predefined ones, and the
ones of the exchanges that are loaded.
+ * Unknown text fails rather than registers a new currency, as XChange keeps
the currencies in a JVM wide registry.
+ */
+@Converter(generateLoader = true)
+public final class XChangeConverters {
+
+ private XChangeConverters() {
+ // Utility Class
+ }
+
+ @Converter
+ public static Currency toCurrency(String code) {
+ Currency answer = Currency.getInstanceNoCreate(code.trim());
+ if (answer == null) {
+ throw new IllegalArgumentException("Unknown currency: " + code);
+ }
+ return answer;
+ }
+
+ @Converter
+ public static CurrencyPair toCurrencyPair(String pair) {
+ String text = pair.trim();
+ // the same separators as new CurrencyPair(String)
+ int pos = text.contains("-") ? text.indexOf('-') : text.indexOf('/');
+ if (pos < 1 || pos == text.length() - 1) {
+ throw new IllegalArgumentException("Could not parse currency pair
from: " + pair);
+ }
+ return new CurrencyPair(toCurrency(text.substring(0, pos)),
toCurrency(text.substring(pos + 1)));
+ }
+}
diff --git
a/components/camel-xchange/src/main/java/org/apache/camel/component/xchange/XChangeEndpoint.java
b/components/camel-xchange/src/main/java/org/apache/camel/component/xchange/XChangeEndpoint.java
index 038f182889d1..777db8848b3a 100644
---
a/components/camel-xchange/src/main/java/org/apache/camel/component/xchange/XChangeEndpoint.java
+++
b/components/camel-xchange/src/main/java/org/apache/camel/component/xchange/XChangeEndpoint.java
@@ -61,9 +61,9 @@ import org.knowm.xchange.utils.Assert;
@BodyTypes(producerIn = @BodyType(value = Carrier.ANY,
description = "Read only by ticker and
currencyPairMetaData, as a CurrencyPair, and by"
+ " currencyMetaData, as a
Currency, when the CurrencyPair or Currency header"
- + " is not set. Text such as
BTC/USDT is not converted: use the currencyPair"
- + " option, or a CurrencyPair
object in the body or the header"),
- producerOut = @BodyType(value = { Carrier.POJO, Carrier.LIST,
Carrier.NULL, Carrier.UNCHANGED },
+ + " is not set. Text such as
BTC/USDT or BTC is converted; a currency"
+ + " XChange does not know
fails"),
+ producerOut = @BodyType(value = { Carrier.POJO, Carrier.LIST,
Carrier.NULL },
byOption = {
@BodyType.When(option = "method",
value = "ticker", carriers = Carrier.POJO,
javaType =
"org.knowm.xchange.dto.marketdata.Ticker"),
@@ -83,8 +83,7 @@ import org.knowm.xchange.utils.Assert;
javaType =
"java.util.List<org.knowm.xchange.dto.account.FundingRecord>") },
description = "The XChange object of the
method: a Ticker, a List of Currency,"
+ " CurrencyPair, Balance,
Wallet or FundingRecord, or the CurrencyMetaData or"
- + " InstrumentMetaData, null
for one the exchange does not know. A method of"
- + " another service leaves
the body unchanged"))
+ + " InstrumentMetaData, null
for one the exchange does not know"))
public class XChangeEndpoint extends DefaultEndpoint {
@UriParam
@@ -159,7 +158,7 @@ public class XChangeEndpoint extends DefaultEndpoint {
}
public InstrumentMetaData getCurrencyPairMetaData(CurrencyPair pair) {
- Assert.notNull(pair, "Null currency");
+ Assert.notNull(pair, "Null currency pair");
ExchangeMetaData metaData = xchange.getExchangeMetaData();
return metaData.getInstruments().get(pair);
}
diff --git
a/components/camel-xchange/src/test/java/org/apache/camel/component/xchange/XChangeCurrencyConversionTest.java
b/components/camel-xchange/src/test/java/org/apache/camel/component/xchange/XChangeCurrencyConversionTest.java
new file mode 100644
index 000000000000..b6de840a3964
--- /dev/null
+++
b/components/camel-xchange/src/test/java/org/apache/camel/component/xchange/XChangeCurrencyConversionTest.java
@@ -0,0 +1,170 @@
+/*
+ * 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.xchange;
+
+import java.lang.reflect.Proxy;
+import java.math.BigDecimal;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.ResolveEndpointFailedException;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.knowm.xchange.Exchange;
+import org.knowm.xchange.currency.Currency;
+import org.knowm.xchange.currency.CurrencyPair;
+import org.knowm.xchange.dto.marketdata.Ticker;
+import org.knowm.xchange.dto.meta.CurrencyMetaData;
+import org.knowm.xchange.dto.meta.ExchangeMetaData;
+import org.knowm.xchange.dto.meta.InstrumentMetaData;
+import org.knowm.xchange.instrument.Instrument;
+import org.knowm.xchange.service.marketdata.MarketDataService;
+
+import static
org.apache.camel.component.xchange.XChangeConfiguration.HEADER_CURRENCY;
+import static
org.apache.camel.component.xchange.XChangeConfiguration.HEADER_CURRENCY_PAIR;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Runs the producers against a fake exchange, so the currency and the
currency pair given as text are tested without
+ * calling a real exchange.
+ */
+public class XChangeCurrencyConversionTest extends CamelTestSupport {
+
+ private static final CurrencyMetaData BTC_META = new CurrencyMetaData(8,
BigDecimal.ZERO);
+ private static final CurrencyMetaData USDT_META = new CurrencyMetaData(2,
BigDecimal.ONE);
+ private static final InstrumentMetaData BTC_USDT_META =
InstrumentMetaData.builder().priceScale(2).build();
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ CamelContext context = super.createCamelContext();
+ XChangeComponent component = new XChangeComponent();
+ component.putXChange("fake", new XChange(fakeExchange()));
+ context.addComponent("xchange", component);
+ return context;
+ }
+
+ private static Exchange fakeExchange() {
+ ExchangeMetaData metaData = new ExchangeMetaData(
+ Map.of(CurrencyPair.BTC_USDT, BTC_USDT_META),
+ Map.of(Currency.BTC, BTC_META, Currency.USDT, USDT_META),
null, null, false);
+ MarketDataService marketData = (MarketDataService)
Proxy.newProxyInstance(
+ XChangeCurrencyConversionTest.class.getClassLoader(), new
Class<?>[] { MarketDataService.class },
+ (proxy, method, args) -> {
+ if ("getTicker".equals(method.getName())) {
+ return new Ticker.Builder().instrument((Instrument)
args[0]).last(BigDecimal.TEN).build();
+ }
+ throw new UnsupportedOperationException(method.getName());
+ });
+ return (Exchange) Proxy.newProxyInstance(
+ XChangeCurrencyConversionTest.class.getClassLoader(), new
Class<?>[] { Exchange.class },
+ (proxy, method, args) -> switch (method.getName()) {
+ case "getExchangeMetaData" -> metaData;
+ case "getMarketDataService" -> marketData;
+ default -> throw new
UnsupportedOperationException(method.getName());
+ });
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:ticker")
+ .to("xchange:fake?service=marketdata&method=ticker");
+ from("direct:currencyMetaData")
+
.to("xchange:fake?service=metadata&method=currencyMetaData");
+ from("direct:currencyPairMetaData")
+
.to("xchange:fake?service=metadata&method=currencyPairMetaData");
+ }
+ };
+ }
+
+ @Test
+ void testCurrencyOption() {
+ Object body =
template.requestBody("xchange:fake?service=metadata&method=currencyMetaData¤cy=BTC",
(Object) null);
+ assertSame(BTC_META, body);
+ }
+
+ @Test
+ void testCurrencyHeaderAsText() {
+ Object body = template.requestBodyAndHeader("direct:currencyMetaData",
null, HEADER_CURRENCY, "USDT");
+ assertSame(USDT_META, body);
+ }
+
+ @Test
+ void testCurrencyBodyAsText() {
+ Object body = template.requestBody("direct:currencyMetaData", "btc");
+ assertSame(BTC_META, body);
+ }
+
+ @Test
+ void testUnknownCurrency() {
+ CamelExecutionException e = assertThrows(CamelExecutionException.class,
+ () -> template.requestBodyAndHeader("direct:currencyMetaData",
null, HEADER_CURRENCY, "NOSUCHCOIN"));
+ assertTrue(e.getCause().getMessage().contains("NOSUCHCOIN"),
e.getCause().getMessage());
+ }
+
+ @Test
+ void testCurrencyPairHeaderAsText() {
+ Ticker ticker = template.requestBodyAndHeader("direct:ticker", null,
HEADER_CURRENCY_PAIR, "BTC/USDT", Ticker.class);
+ assertNotNull(ticker);
+ assertEquals(CurrencyPair.BTC_USDT, ticker.getInstrument());
+ }
+
+ @Test
+ void testCurrencyPairBodyAsText() {
+ Object body = template.requestBody("direct:currencyPairMetaData",
"BTC/USDT");
+ assertSame(BTC_USDT_META, body);
+ }
+
+ @Test
+ void testCurrencyPairTextWithoutSeparator() {
+ CamelExecutionException e = assertThrows(CamelExecutionException.class,
+ () -> template.requestBody("direct:currencyPairMetaData",
"BTCUSDT"));
+ assertTrue(e.getCause().getMessage().contains("BTCUSDT"),
e.getCause().getMessage());
+ }
+
+ @Test
+ void testNoCurrencyPair() {
+ CamelExecutionException e = assertThrows(CamelExecutionException.class,
+ () -> template.requestBody("direct:currencyPairMetaData",
(Object) null));
+ assertInstanceOf(IllegalArgumentException.class, e.getCause());
+ assertEquals("Null currency pair", e.getCause().getMessage());
+ }
+
+ @Test
+ void testMethodOfAnotherService() {
+ ResolveEndpointFailedException e =
assertThrows(ResolveEndpointFailedException.class,
+ () ->
context.getEndpoint("xchange:fake?service=marketdata&method=currencies"));
+ assertEquals("The method currencies belongs to the metadata service,
not to the marketdata service",
+ e.getCause().getMessage());
+ }
+
+ @Test
+ void testNoMethod() {
+ ResolveEndpointFailedException e =
assertThrows(ResolveEndpointFailedException.class,
+ () -> context.getEndpoint("xchange:fake?service=metadata"));
+ assertEquals("The service and method options are required",
e.getCause().getMessage());
+ }
+}
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 5a71f0252288..350ed995d9a2 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
@@ -868,6 +868,13 @@ header to `ZERO_RESULTS`. Before, the exchange failed with
an `IllegalArgumentEx
no place (Nominatim answers `{"error":"Unable to geocode"}`) now sets
`ZERO_RESULTS` too, and another error answer of
Nominatim sets `ERROR`. Before, both set `OK` with no other headers.
+=== camel-xchange - an endpoint with a method of another service fails
+
+An `xchange` endpoint now fails when it is created if it has no `service` or
no `method`, or if the method belongs to
+another service, such as `service=marketdata&method=currencies`. Before, its
producer did nothing and left the message
+unchanged. The `Currency` and `CurrencyPair` headers and the body can now be
text such as `BTC` and `BTC/USDT`; text
+with a currency that XChange does not know fails the exchange.
+
=== camel-gson, camel-jsonb - the unmarshal type header needs
allowUnmarshallType
The Gson and JSON-B data formats now use the `CamelGsonUnmarshalType` and
`CamelJsonbUnmarshallType` headers to choose