From 077abf35e97cd7b63c2aea752df01b0667d99281 Mon Sep 17 00:00:00 2001 From: Thomas Lavocat Date: Tue, 1 Sep 2026 16:08:41 +0200 Subject: [PATCH] ARTEMIS-6219 Support _class discriminator in JSON/YAML broker properties Add reserved _class key support for interface-typed fields in JSON/YAML broker properties. The class name was already exposed by the flat format (e.g. storeConfiguration=org...DatabaseStorageConfiguration.class); this makes the same mechanism available in structured formats where a JSON key cannot simultaneously be a scalar discriminator and an object container. Assisted-by: Claude Opus 4.6 --- .../core/config/impl/ConfigurationImpl.java | 25 ++- .../impl/AbstractConfigurationFullTest.java | 22 +++ .../config/impl/ConfigurationImplTest.java | 185 ++++++++++++++++++ .../test/resources/broker-full-config.json | 14 ++ .../test/resources/broker-full-config.yaml | 13 ++ 5 files changed, 258 insertions(+), 1 deletion(-) diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImpl.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImpl.java index dc840c310e42..6564f9427c4a 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImpl.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImpl.java @@ -177,6 +177,8 @@ public class ConfigurationImpl extends javax.security.auth.login.Configuration i public static final String PROPERTY_CLASS_SUFFIX = ".class"; + public static final String JSON_CLASS_DISCRIMINATOR_KEY = "_class"; + public static final String REDACTED = "**redacted**"; private static final String FILTER_QUERY_PARAMETER_KEY = "filter"; @@ -3940,8 +3942,19 @@ public synchronized boolean loadYaml(ConfigurationImpl configuration, InputStrea @SuppressWarnings("unchecked") private void loadYamlMap(String keySurroundString, String parentKey, Map map) { + // emit _class discriminator first so the interface-typed field gets instantiated + // before any sub-properties attempt to set values on it + Object classDiscriminator = map.get(JSON_CLASS_DISCRIMINATOR_KEY); + if (classDiscriminator != null) { + String discriminatorKey = parentKey.endsWith(".") ? parentKey.substring(0, parentKey.length() - 1) : parentKey; + put(discriminatorKey, String.valueOf(classDiscriminator)); + } + for (Map.Entry entry : map.entrySet()) { String key = entry.getKey(); + if (JSON_CLASS_DISCRIMINATOR_KEY.equals(key)) { + continue; + } key = autoSurroundIfNecessary(key, keySurroundString); String propertyKey = parentKey + key; Object value = entry.getValue(); @@ -3970,10 +3983,20 @@ private String determineSurroundString(ConfigurationImpl configuration, Map { + String jsonKey = jsonEntry.getKey(); + if (JSON_CLASS_DISCRIMINATOR_KEY.equals(jsonKey)) { + return; + } JsonValue jsonValue = jsonEntry.getValue(); JsonValue.ValueType jsonValueType = jsonValue.getValueType(); - String jsonKey = jsonEntry.getKey(); jsonKey = autoSurroundIfNecessary(jsonKey, keySurroundString); String propertyKey = parentKey + jsonKey; switch (jsonValueType) { diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/AbstractConfigurationFullTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/AbstractConfigurationFullTest.java index 84d70a81092b..4c5a66477d50 100644 --- a/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/AbstractConfigurationFullTest.java +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/AbstractConfigurationFullTest.java @@ -42,6 +42,8 @@ import org.apache.activemq.artemis.core.config.JaasAppConfiguration; import org.apache.activemq.artemis.core.config.JaasAppConfigurationEntry; import org.apache.activemq.artemis.core.config.MetricsConfiguration; +import org.apache.activemq.artemis.core.config.StoreConfiguration; +import org.apache.activemq.artemis.core.config.storage.DatabaseStorageConfiguration; import org.apache.activemq.artemis.core.config.WildcardConfiguration; import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPBrokerConnectConfiguration; import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPMirrorBrokerConnectionElement; @@ -482,6 +484,24 @@ public void testHAPolicyConfiguration() { assertTrue(haPolicy instanceof LiveOnlyPolicyConfiguration); } + @Test + public void testStoreConfiguration() { + StoreConfiguration store = configuration.getStoreConfiguration(); + assertNotNull(store, "storeConfiguration via _class discriminator must be populated"); + assertTrue(store instanceof DatabaseStorageConfiguration); + DatabaseStorageConfiguration dbStore = (DatabaseStorageConfiguration) store; + assertEquals("FULL_MESSAGES", dbStore.getMessageTableName()); + assertEquals("FULL_BINDINGS", dbStore.getBindingsTableName()); + assertEquals("FULL_LARGE_MESSAGES", dbStore.getLargeMessageTableName()); + assertEquals("FULL_PAGE_STORE", dbStore.getPageStoreTableName()); + assertEquals("FULL_NODE_MANAGER", dbStore.getNodeManagerStoreTableName()); + assertEquals("jdbc:derby:target/full-test-store;create=true", dbStore.getJdbcConnectionUrl()); + assertEquals("org.apache.derby.jdbc.EmbeddedDriver", dbStore.getJdbcDriverClassName()); + assertEquals(30000, dbStore.getJdbcNetworkTimeout()); + assertEquals(3000, dbStore.getJdbcLockRenewPeriodMillis()); + assertEquals(20000, dbStore.getJdbcLockExpirationMillis()); + } + @Test public void testResourceLimitSettings() { Map limits = configuration.getResourceLimitSettings(); @@ -690,6 +710,8 @@ public void testRoundTripExport() throws Exception { assertEquals(configuration.getAMQPConnections().size(), reloaded.getAMQPConnections().size()); assertNotNull(reloaded.getHAPolicyConfiguration()); assertEquals(configuration.getHAPolicyConfiguration().getType(), reloaded.getHAPolicyConfiguration().getType()); + assertNotNull(reloaded.getStoreConfiguration()); + assertEquals(configuration.getStoreConfiguration().getStoreType(), reloaded.getStoreConfiguration().getStoreType()); assertEquals(configuration.getResourceLimitSettings().size(), reloaded.getResourceLimitSettings().size()); assertEquals(configuration.getJaasConfigs().size(), reloaded.getJaasConfigs().size()); assertEquals(configuration.getFederationDownstreamAuthorization(), reloaded.getFederationDownstreamAuthorization()); diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImplTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImplTest.java index 05864c64670c..ce21b5219fd6 100644 --- a/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImplTest.java +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImplTest.java @@ -61,10 +61,13 @@ import org.apache.activemq.artemis.ArtemisConstants; import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration; +import org.apache.activemq.artemis.api.core.BroadcastGroupConfiguration; +import org.apache.activemq.artemis.api.core.DiscoveryGroupConfiguration; import org.apache.activemq.artemis.api.core.QueueConfiguration; import org.apache.activemq.artemis.api.core.RoutingType; import org.apache.activemq.artemis.api.core.SimpleString; import org.apache.activemq.artemis.api.core.TransportConfiguration; +import org.apache.activemq.artemis.api.core.UDPBroadcastEndpointFactory; import org.apache.activemq.artemis.core.config.Configuration; import org.apache.activemq.artemis.core.config.ConfigurationUtils; import org.apache.activemq.artemis.core.config.CoreAddressConfiguration; @@ -104,6 +107,7 @@ import org.apache.activemq.artemis.core.server.ComponentConfigurationRoutingType; import org.apache.activemq.artemis.core.server.cluster.impl.MessageLoadBalancingType; import org.apache.activemq.artemis.core.server.impl.LegacyLDAPSecuritySettingPlugin; +import org.apache.activemq.artemis.core.server.metrics.plugins.SimpleMetricsPlugin; import org.apache.activemq.artemis.core.server.plugin.impl.ConnectionPeriodicExpiryPlugin; import org.apache.activemq.artemis.core.server.plugin.impl.LoggingActiveMQServerPlugin; import org.apache.activemq.artemis.core.server.routing.KeyType; @@ -2375,6 +2379,187 @@ public void testInvalidStoreConfigurationProps() throws Exception { assertTrue(status.contains("AMQ229249")); } + @Test + public void testDatabaseStoreConfigurationJsonClassDiscriminator() throws Exception { + File tmpFile = File.createTempFile("store-config-json-class-test", ".json", temporaryFolder); + try (FileOutputStream fileOutputStream = new FileOutputStream(tmpFile); + PrintWriter printWriter = new PrintWriter(fileOutputStream)) { + printWriter.write("{\n"); + printWriter.write(" \"storeConfiguration\": {\n"); + printWriter.write(" \"_class\": \"DATABASE\",\n"); + printWriter.write(" \"largeMessageTableName\": \"lmtn\",\n"); + printWriter.write(" \"messageTableName\": \"mtn\",\n"); + printWriter.write(" \"bindingsTableName\": \"btn\",\n"); + printWriter.write(" \"jdbcConnectionUrl\": \"url\",\n"); + printWriter.write(" \"jdbcDriverClassName\": \"dcn\",\n"); + printWriter.write(" \"jdbcUser\": \"user\"\n"); + printWriter.write(" }\n"); + printWriter.write("}\n"); + } + + ConfigurationImpl configuration = new ConfigurationImpl(); + configuration.parseProperties(tmpFile.getAbsolutePath()); + + assertTrue(configuration.getStatus().contains("\"errors\":[]")); + + assertInstanceOf(DatabaseStorageConfiguration.class, configuration.getStoreConfiguration()); + DatabaseStorageConfiguration dsc = (DatabaseStorageConfiguration) configuration.getStoreConfiguration(); + assertEquals("lmtn", dsc.getLargeMessageTableName()); + assertEquals("mtn", dsc.getMessageTableName()); + assertEquals("btn", dsc.getBindingsTableName()); + assertEquals("url", dsc.getJdbcConnectionUrl()); + assertEquals("dcn", dsc.getJdbcDriverClassName()); + assertEquals("user", dsc.getJdbcUser()); + } + + @Test + public void testHAPolicyConfigurationJsonClassDiscriminator() throws Exception { + File tmpFile = File.createTempFile("ha-config-json-class-test", ".json", temporaryFolder); + try (FileOutputStream fileOutputStream = new FileOutputStream(tmpFile); + PrintWriter printWriter = new PrintWriter(fileOutputStream)) { + printWriter.write("{\n"); + printWriter.write(" \"HAPolicyConfiguration\": {\n"); + printWriter.write(" \"_class\": \"SHARED_STORE_PRIMARY\",\n"); + printWriter.write(" \"failoverOnServerShutdown\": true,\n"); + printWriter.write(" \"waitForActivation\": false\n"); + printWriter.write(" }\n"); + printWriter.write("}\n"); + } + + ConfigurationImpl configuration = new ConfigurationImpl(); + configuration.parseProperties(tmpFile.getAbsolutePath()); + + assertTrue(configuration.getStatus().contains("\"errors\":[]")); + + HAPolicyConfiguration haPolicyConfiguration = configuration.getHAPolicyConfiguration(); + assertEquals(SharedStorePrimaryPolicyConfiguration.class, haPolicyConfiguration.getClass()); + + SharedStorePrimaryPolicyConfiguration sharedStorePrimaryPolicyConfiguration = + (SharedStorePrimaryPolicyConfiguration) haPolicyConfiguration; + assertTrue(sharedStorePrimaryPolicyConfiguration.isFailoverOnServerShutdown()); + assertFalse(sharedStorePrimaryPolicyConfiguration.isWaitForActivation()); + } + + @Test + public void testJsonClassDiscriminatorOrderIndependent() throws Exception { + // _class appears AFTER sub-properties — must still work + File tmpFile = File.createTempFile("class-order-test", ".json", temporaryFolder); + try (FileOutputStream fileOutputStream = new FileOutputStream(tmpFile); + PrintWriter printWriter = new PrintWriter(fileOutputStream)) { + printWriter.write("{\n"); + printWriter.write(" \"storeConfiguration\": {\n"); + printWriter.write(" \"largeMessageTableName\": \"lmtn\",\n"); + printWriter.write(" \"jdbcConnectionUrl\": \"url\",\n"); + printWriter.write(" \"_class\": \"DATABASE\"\n"); + printWriter.write(" }\n"); + printWriter.write("}\n"); + } + + ConfigurationImpl configuration = new ConfigurationImpl(); + configuration.parseProperties(tmpFile.getAbsolutePath()); + + assertTrue(configuration.getStatus().contains("\"errors\":[]")); + + assertInstanceOf(DatabaseStorageConfiguration.class, configuration.getStoreConfiguration()); + DatabaseStorageConfiguration dsc = (DatabaseStorageConfiguration) configuration.getStoreConfiguration(); + assertEquals("lmtn", dsc.getLargeMessageTableName()); + assertEquals("url", dsc.getJdbcConnectionUrl()); + } + + @Test + public void testMetricsPluginJsonClassDiscriminator() throws Exception { + File tmpFile = File.createTempFile("metrics-json-class-test", ".json", temporaryFolder); + try (FileOutputStream fileOutputStream = new FileOutputStream(tmpFile); + PrintWriter printWriter = new PrintWriter(fileOutputStream)) { + printWriter.write("{\n"); + printWriter.write(" \"metricsConfiguration\": {\n"); + printWriter.write(" \"plugin\": {\n"); + printWriter.write(" \"_class\": \"org.apache.activemq.artemis.core.server.metrics.plugins.SimpleMetricsPlugin.class\",\n"); + printWriter.write(" \"init\": \"\"\n"); + printWriter.write(" },\n"); + printWriter.write(" \"jvmMemory\": false\n"); + printWriter.write(" }\n"); + printWriter.write("}\n"); + } + + ConfigurationImpl configuration = new ConfigurationImpl(); + configuration.parseProperties(tmpFile.getAbsolutePath()); + + assertTrue(configuration.getStatus().contains("\"errors\":[]"), configuration.getStatus()); + + assertNotNull(configuration.getMetricsConfiguration()); + assertNotNull(configuration.getMetricsConfiguration().getPlugin()); + assertInstanceOf(SimpleMetricsPlugin.class, configuration.getMetricsConfiguration().getPlugin()); + assertFalse(configuration.getMetricsConfiguration().isJvmMemory()); + } + + @Test + public void testBroadcastEndpointFactoryJsonClassDiscriminator() throws Exception { + File tmpFile = File.createTempFile("broadcast-json-class-test", ".json", temporaryFolder); + try (FileOutputStream fileOutputStream = new FileOutputStream(tmpFile); + PrintWriter printWriter = new PrintWriter(fileOutputStream)) { + printWriter.write("{\n"); + printWriter.write(" \"broadcastGroupConfigurations\": {\n"); + printWriter.write(" \"bg1\": {\n"); + printWriter.write(" \"broadcastPeriod\": 1234,\n"); + printWriter.write(" \"endpointFactory\": {\n"); + printWriter.write(" \"_class\": \"org.apache.activemq.artemis.api.core.UDPBroadcastEndpointFactory.class\",\n"); + printWriter.write(" \"groupAddress\": \"231.7.7.7\",\n"); + printWriter.write(" \"groupPort\": 9876\n"); + printWriter.write(" }\n"); + printWriter.write(" }\n"); + printWriter.write(" }\n"); + printWriter.write("}\n"); + } + + ConfigurationImpl configuration = new ConfigurationImpl(); + configuration.parseProperties(tmpFile.getAbsolutePath()); + + assertTrue(configuration.getStatus().contains("\"errors\":[]"), configuration.getStatus()); + + assertEquals(1, configuration.getBroadcastGroupConfigurations().size()); + BroadcastGroupConfiguration bg = configuration.getBroadcastGroupConfigurations().get(0); + assertEquals(1234, bg.getBroadcastPeriod()); + assertInstanceOf(UDPBroadcastEndpointFactory.class, bg.getEndpointFactory()); + UDPBroadcastEndpointFactory udp = (UDPBroadcastEndpointFactory) bg.getEndpointFactory(); + assertEquals("231.7.7.7", udp.getGroupAddress()); + assertEquals(9876, udp.getGroupPort()); + } + + @Test + public void testDiscoveryEndpointFactoryJsonClassDiscriminator() throws Exception { + File tmpFile = File.createTempFile("discovery-json-class-test", ".json", temporaryFolder); + try (FileOutputStream fileOutputStream = new FileOutputStream(tmpFile); + PrintWriter printWriter = new PrintWriter(fileOutputStream)) { + printWriter.write("{\n"); + printWriter.write(" \"discoveryGroupConfigurations\": {\n"); + printWriter.write(" \"dg1\": {\n"); + printWriter.write(" \"refreshTimeout\": 5000,\n"); + printWriter.write(" \"broadcastEndpointFactory\": {\n"); + printWriter.write(" \"_class\": \"org.apache.activemq.artemis.api.core.UDPBroadcastEndpointFactory.class\",\n"); + printWriter.write(" \"groupAddress\": \"231.7.7.7\",\n"); + printWriter.write(" \"groupPort\": 9877\n"); + printWriter.write(" }\n"); + printWriter.write(" }\n"); + printWriter.write(" }\n"); + printWriter.write("}\n"); + } + + ConfigurationImpl configuration = new ConfigurationImpl(); + configuration.parseProperties(tmpFile.getAbsolutePath()); + + assertTrue(configuration.getStatus().contains("\"errors\":[]"), configuration.getStatus()); + + assertEquals(1, configuration.getDiscoveryGroupConfigurations().size()); + DiscoveryGroupConfiguration dg = configuration.getDiscoveryGroupConfigurations().get("dg1"); + assertNotNull(dg); + assertEquals(5000, dg.getRefreshTimeout()); + assertInstanceOf(UDPBroadcastEndpointFactory.class, dg.getBroadcastEndpointFactory()); + UDPBroadcastEndpointFactory udp = (UDPBroadcastEndpointFactory) dg.getBroadcastEndpointFactory(); + assertEquals("231.7.7.7", udp.getGroupAddress()); + assertEquals(9877, udp.getGroupPort()); + } + @Test public void testEnumConversion() throws Exception { ConfigurationImpl configuration = new ConfigurationImpl(); diff --git a/artemis-server/src/test/resources/broker-full-config.json b/artemis-server/src/test/resources/broker-full-config.json index d61138a578db..f9b771e84539 100644 --- a/artemis-server/src/test/resources/broker-full-config.json +++ b/artemis-server/src/test/resources/broker-full-config.json @@ -348,6 +348,20 @@ "HAPolicyConfiguration": "PRIMARY_ONLY", + "storeConfiguration": { + "_class": "DATABASE", + "messageTableName": "FULL_MESSAGES", + "bindingsTableName": "FULL_BINDINGS", + "largeMessageTableName": "FULL_LARGE_MESSAGES", + "pageStoreTableName": "FULL_PAGE_STORE", + "nodeManagerStoreTableName": "FULL_NODE_MANAGER", + "jdbcConnectionUrl": "jdbc:derby:target/full-test-store;create=true", + "jdbcDriverClassName": "org.apache.derby.jdbc.EmbeddedDriver", + "jdbcNetworkTimeout": 30000, + "jdbcLockRenewPeriodMillis": 3000, + "jdbcLockExpirationMillis": 20000 + }, + "resourceLimitSettings": { "user1": { "maxConnections": 100, diff --git a/artemis-server/src/test/resources/broker-full-config.yaml b/artemis-server/src/test/resources/broker-full-config.yaml index 7233d9dde4f2..2df5a5071914 100644 --- a/artemis-server/src/test/resources/broker-full-config.yaml +++ b/artemis-server/src/test/resources/broker-full-config.yaml @@ -324,6 +324,19 @@ AMQPConnections: HAPolicyConfiguration: "PRIMARY_ONLY" +storeConfiguration: + _class: "DATABASE" + messageTableName: "FULL_MESSAGES" + bindingsTableName: "FULL_BINDINGS" + largeMessageTableName: "FULL_LARGE_MESSAGES" + pageStoreTableName: "FULL_PAGE_STORE" + nodeManagerStoreTableName: "FULL_NODE_MANAGER" + jdbcConnectionUrl: "jdbc:derby:target/full-test-store;create=true" + jdbcDriverClassName: "org.apache.derby.jdbc.EmbeddedDriver" + jdbcNetworkTimeout: 30000 + jdbcLockRenewPeriodMillis: 3000 + jdbcLockExpirationMillis: 20000 + resourceLimitSettings: user1: maxConnections: 100