This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-26171-xchange-currency in repository https://gitbox.apache.org/repos/asf/camel.git
commit 264e8cdcc7b2dab8311c5b023ecff572b4ff4e47 Author: Claus Ibsen <[email protected]> AuthorDate: Mon Nov 2 23:32:12 2026 +0100 CAMEL-26171: camel-xchange - convert currency and currency pair text, 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 10f5a47c1b89..c8e675182aa7 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 @@ -764,6 +764,13 @@ onto the configuration of the endpoint. Before, they were, so every later query `location` already did the geolocation when its producer was created, so its `CamelWeatherLocation` header was never used. The `lat` and `lon` options of an endpoint are still used instead of the location. +=== 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
