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&currency=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

Reply via email to