This is an automated email from the ASF dual-hosted git repository. gtully pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/activemq-artemis.git
commit 03ef286cc8d3124f339515cbb644cd4c0156237d Author: Gary Tully <[email protected]> AuthorDate: Mon Oct 3 13:39:43 2022 +0100 ARTEMIS-4025 - trap failure to apply properties as errors and report via the broker status in a configuration/properties/errors status field --- .../apache/activemq/artemis/api/core/JsonUtil.java | 49 +++++ .../activemq/artemis/api/core/JsonUtilTest.java | 85 ++++++++ .../core/config/impl/ConfigurationImpl.java | 217 ++++++++++++++++++++- .../management/impl/ActiveMQServerControlImpl.java | 2 +- .../artemis/core/server/ActiveMQServer.java | 2 + .../core/server/impl/ActiveMQServerImpl.java | 15 ++ .../artemis/core/server/impl/ServerStatus.java | 69 +++++++ .../core/config/impl/ConfigurationImplTest.java | 50 +++++ .../integration/server/ConfigurationTest.java | 6 +- 9 files changed, 483 insertions(+), 12 deletions(-) diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/JsonUtil.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/JsonUtil.java index c98aa9cf97..bfa7bbce31 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/JsonUtil.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/JsonUtil.java @@ -360,6 +360,55 @@ public final class JsonUtil { return result; } + public static JsonObject mergeAndUpdate(JsonObject source, JsonObject update) { + // all immutable so we need to create new merged instance + JsonObjectBuilder jsonObjectBuilder = JsonLoader.createObjectBuilder(); + for (Map.Entry<String, JsonValue> entry : source.entrySet()) { + jsonObjectBuilder.add(entry.getKey(), entry.getValue()); + } + // apply any updates + for (String updateKey : update.keySet()) { + JsonValue updatedValue = update.get(updateKey); + if (updatedValue != null) { + if (!source.containsKey(updateKey)) { + jsonObjectBuilder.add(updateKey, updatedValue); + } else { + // recursively merge into new value + if (updatedValue.getValueType() == JsonValue.ValueType.OBJECT) { + jsonObjectBuilder.add(updateKey, mergeAndUpdate(source.getJsonObject(updateKey), updatedValue.asJsonObject())); + + } else if (updatedValue.getValueType() == JsonValue.ValueType.ARRAY) { + JsonArrayBuilder jsonArrayBuilder = JsonLoader.createArrayBuilder(); + + // update wins + JsonArray updatedArrayValue = update.getJsonArray(updateKey); + JsonArray sourceArrayValue = source.getJsonArray(updateKey); + + for (int i = 0; i < updatedArrayValue.size(); i++) { + if (i < sourceArrayValue.size()) { + JsonValue element = updatedArrayValue.get(i); + if (element.getValueType() == JsonValue.ValueType.OBJECT) { + jsonArrayBuilder.add(mergeAndUpdate(sourceArrayValue.getJsonObject(i), updatedArrayValue.getJsonObject(i))); + } else { + // take the update + jsonArrayBuilder.add(element); + } + } else { + jsonArrayBuilder.add(updatedArrayValue.get(i)); + } + } + jsonObjectBuilder.add(updateKey, jsonArrayBuilder.build()); + } else { + // update wins! + jsonObjectBuilder.add(updateKey, updatedValue); + } + } + } + } + + return jsonObjectBuilder.build(); + } + private static class NullableJsonString implements JsonValue, JsonString { private final String value; diff --git a/artemis-core-client/src/test/java/org/apache/activemq/artemis/api/core/JsonUtilTest.java b/artemis-core-client/src/test/java/org/apache/activemq/artemis/api/core/JsonUtilTest.java index d6d28470cc..7c11ad4534 100644 --- a/artemis-core-client/src/test/java/org/apache/activemq/artemis/api/core/JsonUtilTest.java +++ b/artemis-core-client/src/test/java/org/apache/activemq/artemis/api/core/JsonUtilTest.java @@ -127,4 +127,89 @@ public class JsonUtilTest { Assert.assertEquals(input, notTruncated); } + + @Test + public void testMergeEqual() { + + final byte[] bytesA = {0x0a, 0x0b}; + final byte[] bytesB = {0x1a, 0x1b}; + final byte[] bytesAB = {0x0a, 0x0b, 0x1a, 0x1b}; + + JsonObjectBuilder jsonObjectBuilder = JsonLoader.createObjectBuilder(); + JsonUtil.addToObject("a", "a", jsonObjectBuilder); + JsonUtil.addToObject("byteArray", bytesA, jsonObjectBuilder); + JsonUtil.addToObject("null", null, jsonObjectBuilder); + + JsonObject sourceOne = jsonObjectBuilder.build(); + + JsonObjectBuilder jsonTargetObjectBuilder = JsonLoader.createObjectBuilder(); + JsonUtil.addToObject("a", "a", jsonTargetObjectBuilder); + JsonUtil.addToObject("byteArray", bytesB, jsonTargetObjectBuilder); + JsonUtil.addToObject("null", null, jsonTargetObjectBuilder); + + JsonObject sourceTwo = jsonTargetObjectBuilder.build(); + + JsonObjectBuilder jsonMergedObjectBuilder = JsonLoader.createObjectBuilder(); + JsonUtil.addToObject("a", "a", jsonMergedObjectBuilder); + // update wins + JsonUtil.addToObject("byteArray", bytesB, jsonMergedObjectBuilder); + JsonUtil.addToObject("null", null, jsonMergedObjectBuilder); + + JsonObject mergedExpected = jsonMergedObjectBuilder.build(); + + Assert.assertEquals(mergedExpected, JsonUtil.mergeAndUpdate(sourceOne, sourceTwo)); + } + + @Test + public void testMergeArray() { + + final byte[] bytesA = {0x0a, 0x0b}; + JsonObjectBuilder jsonObjectBuilder = JsonLoader.createObjectBuilder(); + + jsonObjectBuilder = JsonLoader.createObjectBuilder(); + JsonUtil.addToObject("byteArray", bytesA, jsonObjectBuilder); + + JsonObject sourceOne = jsonObjectBuilder.build(); + + JsonObjectBuilder jsonTargetObjectBuilder = JsonLoader.createObjectBuilder(); + JsonUtil.addToObject("byteArray", bytesA, jsonTargetObjectBuilder); + + JsonObject sourceTwo = jsonTargetObjectBuilder.build(); + + JsonObjectBuilder jsonMergedObjectBuilder = JsonLoader.createObjectBuilder(); + JsonUtil.addToObject("byteArray", bytesA, jsonMergedObjectBuilder); + + JsonObject mergedExpected = jsonMergedObjectBuilder.build(); + + Assert.assertEquals(mergedExpected, JsonUtil.mergeAndUpdate(sourceOne, sourceTwo)); + + } + + @Test + public void testMergeDuplicate() { + + // merge duplicate attribute value, two wins + JsonObjectBuilder jsonObjectBuilder = JsonLoader.createObjectBuilder(); + JsonUtil.addToObject("dup", "a", jsonObjectBuilder); + JsonObject sourceOne = jsonObjectBuilder.build(); + + JsonObjectBuilder jsonTargetObjectBuilder = JsonLoader.createObjectBuilder(); + JsonUtil.addToObject("dup", "b", jsonTargetObjectBuilder); + JsonObject sourceTwo = jsonTargetObjectBuilder.build(); + + Assert.assertEquals(sourceTwo, JsonUtil.mergeAndUpdate(sourceOne, sourceTwo)); + } + + @Test + public void testMergeEmpty() { + + // merge into empty + JsonObject sourceOne = JsonLoader.createObjectBuilder().build(); + + JsonObjectBuilder jsonTargetObjectBuilder = JsonLoader.createObjectBuilder(); + JsonUtil.addToObject("dup", "b", jsonTargetObjectBuilder); + JsonObject sourceTwo = jsonTargetObjectBuilder.build(); + + Assert.assertEquals(sourceTwo, JsonUtil.mergeAndUpdate(sourceOne, sourceTwo)); + } } 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 b67f42242b..a7116a1e54 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 @@ -16,6 +16,7 @@ */ package org.apache.activemq.artemis.core.config.impl; +import java.beans.IndexedPropertyDescriptor; import java.beans.PropertyDescriptor; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; @@ -52,6 +53,7 @@ import java.util.concurrent.TimeUnit; 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.JsonUtil; import org.apache.activemq.artemis.api.core.Pair; import org.apache.activemq.artemis.api.core.QueueConfiguration; import org.apache.activemq.artemis.api.core.SimpleString; @@ -99,8 +101,12 @@ import org.apache.activemq.artemis.core.server.plugin.ActiveMQServerResourcePlug import org.apache.activemq.artemis.core.server.plugin.ActiveMQServerSessionPlugin; import org.apache.activemq.artemis.core.settings.impl.AddressSettings; import org.apache.activemq.artemis.core.settings.impl.ResourceLimitSettings; +import org.apache.activemq.artemis.json.JsonArrayBuilder; +import org.apache.activemq.artemis.json.JsonObject; +import org.apache.activemq.artemis.json.JsonObjectBuilder; import org.apache.activemq.artemis.utils.ByteUtil; import org.apache.activemq.artemis.utils.Env; +import org.apache.activemq.artemis.utils.JsonLoader; import org.apache.activemq.artemis.utils.ObjectInputStreamWithClassLoader; import org.apache.activemq.artemis.utils.PasswordMaskingUtil; import org.apache.activemq.artemis.utils.XMLUtil; @@ -109,6 +115,7 @@ import org.apache.activemq.artemis.utils.uri.BeanSupport; import org.apache.commons.beanutils.BeanUtilsBean; import org.apache.commons.beanutils.ConvertUtilsBean; import org.apache.commons.beanutils.Converter; +import org.apache.commons.beanutils.DynaBean; import org.apache.commons.beanutils.MappedPropertyDescriptor; import org.apache.commons.beanutils.MethodUtils; import org.apache.commons.beanutils.PropertyUtilsBean; @@ -116,6 +123,7 @@ import org.apache.commons.beanutils.expression.DefaultResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.invoke.MethodHandles; +import org.apache.commons.beanutils.expression.Resolver; public class ConfigurationImpl implements Configuration, Serializable { @@ -403,7 +411,7 @@ public class ConfigurationImpl implements Configuration, Serializable { * Parent folder for all data folders. */ private File artemisInstance; - private String status; + private transient volatile JsonObject status = JsonLoader.createObjectBuilder().build(); @Override public String getJournalRetentionDirectory() { @@ -538,7 +546,146 @@ public class ConfigurationImpl implements Configuration, Serializable { public void populateWithProperties(Map<String, Object> beanProperties) throws InvocationTargetException, IllegalAccessException { CollectionAutoFillPropertiesUtil autoFillCollections = new CollectionAutoFillPropertiesUtil(); - BeanUtilsBean beanUtils = new BeanUtilsBean(new ConvertUtilsBean(), autoFillCollections); + BeanUtilsBean beanUtils = new BeanUtilsBean(new ConvertUtilsBean(), autoFillCollections) { + // override to treat missing properties as errors, not skip as the default impl does + @Override + public void setProperty(final Object bean, String name, final Object value) throws InvocationTargetException, IllegalAccessException { + { + if (logger.isTraceEnabled()) { + final StringBuilder sb = new StringBuilder(" setProperty("); + sb.append(bean); + sb.append(", "); + sb.append(name); + sb.append(", "); + if (value == null) { + sb.append("<NULL>"); + } else if (value instanceof String) { + sb.append((String) value); + } else if (value instanceof String[]) { + final String[] values = (String[]) value; + sb.append('['); + for (int i = 0; i < values.length; i++) { + if (i > 0) { + sb.append(','); + } + sb.append(values[i]); + } + sb.append(']'); + } else { + sb.append(value.toString()); + } + sb.append(')'); + logger.trace(sb.toString()); + } + + // Resolve any nested expression to get the actual target bean + Object target = bean; + final Resolver resolver = getPropertyUtils().getResolver(); + while (resolver.hasNested(name)) { + try { + target = getPropertyUtils().getProperty(target, resolver.next(name)); + if (target == null) { + throw new InvocationTargetException(null, "Resolved nested property for:" + name + ", on: " + bean + " was null"); + } + name = resolver.remove(name); + } catch (final NoSuchMethodException e) { + throw new InvocationTargetException(e, "No getter for property:" + name + ", on: " + bean); + } + } + if (logger.isTraceEnabled()) { + logger.trace(" Target bean = " + target); + logger.trace(" Target name = " + name); + } + + // Declare local variables we will require + final String propName = resolver.getProperty(name); // Simple name of target property + Class<?> type = null; // Java type of target property + final int index = resolver.getIndex(name); // Indexed subscript value (if any) + final String key = resolver.getKey(name); // Mapped key value (if any) + + // Calculate the property type + if (target instanceof DynaBean) { + throw new InvocationTargetException(null, "Cannot determine DynaBean type to access: " + name + " on: " + target); + } else if (target instanceof Map) { + type = Object.class; + } else if (target != null && target.getClass().isArray() && index >= 0) { + type = Array.get(target, index).getClass(); + } else { + PropertyDescriptor descriptor = null; + try { + descriptor = getPropertyUtils().getPropertyDescriptor(target, name); + if (descriptor == null) { + throw new InvocationTargetException(null, "No accessor method descriptor for: " + name + " on: " + target.getClass()); + } + } catch (final NoSuchMethodException e) { + throw new InvocationTargetException(e, "Failed to get descriptor for: " + name + " on: " + target.getClass()); + } + if (descriptor instanceof MappedPropertyDescriptor) { + if (((MappedPropertyDescriptor) descriptor).getMappedWriteMethod() == null) { + throw new InvocationTargetException(null, "No mapped Write method for: " + name + " on: " + target.getClass()); + } + type = ((MappedPropertyDescriptor) descriptor).getMappedPropertyType(); + } else if (index >= 0 && descriptor instanceof IndexedPropertyDescriptor) { + if (((IndexedPropertyDescriptor) descriptor).getIndexedWriteMethod() == null) { + throw new InvocationTargetException(null, "No indexed Write method for: " + name + " on: " + target.getClass()); + } + type = ((IndexedPropertyDescriptor) descriptor).getIndexedPropertyType(); + } else if (index >= 0 && List.class.isAssignableFrom(descriptor.getPropertyType())) { + type = Object.class; + } else if (key != null) { + if (descriptor.getReadMethod() == null) { + throw new InvocationTargetException(null, "No Read method for: " + name + " on: " + target.getClass()); + } + type = (value == null) ? Object.class : value.getClass(); + } else { + if (descriptor.getWriteMethod() == null) { + throw new InvocationTargetException(null, "No Write method for: " + name + " on: " + target.getClass()); + } + type = descriptor.getPropertyType(); + } + } + + // Convert the specified value to the required type + Object newValue = null; + if (type.isArray() && (index < 0)) { // Scalar value into array + if (value == null) { + final String[] values = new String[1]; + values[0] = null; + newValue = getConvertUtils().convert(values, type); + } else if (value instanceof String) { + newValue = getConvertUtils().convert(value, type); + } else if (value instanceof String[]) { + newValue = getConvertUtils().convert((String[]) value, type); + } else { + newValue = convert(value, type); + } + } else if (type.isArray()) { // Indexed value into array + if (value instanceof String || value == null) { + newValue = getConvertUtils().convert((String) value, type.getComponentType()); + } else if (value instanceof String[]) { + newValue = getConvertUtils().convert(((String[]) value)[0], type.getComponentType()); + } else { + newValue = convert(value, type.getComponentType()); + } + } else { // Value into scalar + if (value instanceof String) { + newValue = getConvertUtils().convert((String) value, type); + } else if (value instanceof String[]) { + newValue = getConvertUtils().convert(((String[]) value)[0], type); + } else { + newValue = convert(value, type); + } + } + + // Invoke the setter method + try { + getPropertyUtils().setProperty(target, name, newValue); + } catch (final NoSuchMethodException e) { + throw new InvocationTargetException(e, "Cannot set: " + propName + " on: " + target.getClass()); + } + } + } + }; autoFillCollections.setBeanUtilsBean(beanUtils); // nested property keys delimited by . and enclosed by '"' if they key's themselves contain dots beanUtils.getPropertyUtils().setResolver(new SurroundResolver(getBrokerPropertiesKeySurround(beanProperties))); @@ -599,7 +746,46 @@ public class ConfigurationImpl implements Configuration, Serializable { BeanSupport.customise(beanUtils); - beanUtils.populate(this, beanProperties); + logger.debug("populate:" + this + ", " + beanProperties); + + HashMap<String, String> errors = new HashMap<>(); + // Loop through the property name/value pairs to be set + for (final Map.Entry<String, ? extends Object> entry : beanProperties.entrySet()) { + // Identify the property name and value(s) to be assigned + final String name = entry.getKey(); + try { + // Perform the assignment for this property + beanUtils.setProperty(this, name, entry.getValue()); + } catch (InvocationTargetException invocationTargetException) { + logger.trace("failed to populate property key: " + name, invocationTargetException); + Throwable toLog = invocationTargetException; + if (invocationTargetException.getCause() != null) { + toLog = invocationTargetException.getCause(); + } + trackError(errors, entry, toLog); + + } catch (Exception oops) { + trackError(errors, entry, oops); + } + } + updateStatus(errors); + } + + private void trackError(HashMap<String, String> errors, Map.Entry<String,?> entry, Throwable oops) { + logger.debug("failed to populate property entry(" + entry.toString() + "), reason: " + oops.getLocalizedMessage(), oops); + errors.put(entry.toString(), oops.getLocalizedMessage()); + } + + private void updateStatus(HashMap<String, String> errors) { + + JsonArrayBuilder errorsObjectArray = JsonLoader.createArrayBuilder(); + for (Map.Entry<String, String> entry : errors.entrySet()) { + errorsObjectArray.add(JsonLoader.createObjectBuilder().add("value", entry.getKey()).add("reason", entry.getValue())); + } + JsonObjectBuilder jsonObjectBuilder = JsonLoader.createObjectBuilder(); + jsonObjectBuilder.add("properties", JsonLoader.createObjectBuilder().add("errors", errorsObjectArray)); + + status = JsonUtil.mergeAndUpdate(status, jsonObjectBuilder.build()); } private String getBrokerPropertiesKeySurround(Map<String, Object> propertiesToApply) { @@ -2852,13 +3038,14 @@ public class ConfigurationImpl implements Configuration, Serializable { } @Override - public String getStatus() { - return status; + public synchronized String getStatus() { + return status.toString(); } @Override - public void setStatus(String status) { - this.status = status; + public synchronized void setStatus(String status) { + JsonObject update = JsonUtil.readJsonObject(status); + this.status = JsonUtil.mergeAndUpdate(this.status, update); } // extend property utils with ability to auto-fill and locate from collections @@ -2904,7 +3091,14 @@ public class ConfigurationImpl implements Configuration, Serializable { } } - Object resolved = getNestedProperty(bean, name); + Object resolved = null; + + try { + resolved = getNestedProperty(bean, name); + } catch (final NoSuchMethodException e) { + // to avoid it being swallowed by caller wrap + throw new InvocationTargetException(e, "Cannot access property with key: " + name); + } return trackCollectionOrMap(name, resolved, bean); } @@ -3015,7 +3209,12 @@ public class ConfigurationImpl implements Configuration, Serializable { // create one and initialise with name try { Object instance = candidate.getParameterTypes()[candidate.getParameterCount() - 1].getDeclaredConstructor().newInstance(); - beanUtilsBean.setProperty(instance, "name", name); + + try { + beanUtilsBean.setProperty(instance, "name", name); + } catch (Throwable ignored) { + // for maps a name attribute is not mandatory + } // this is always going to be a little hacky b/c our config is not natively property friendly if (instance instanceof TransportConfiguration) { diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/ActiveMQServerControlImpl.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/ActiveMQServerControlImpl.java index de0066dcd1..d7edd431c0 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/ActiveMQServerControlImpl.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/ActiveMQServerControlImpl.java @@ -4499,7 +4499,7 @@ public class ActiveMQServerControlImpl extends AbstractControl implements Active } checkStarted(); - return server.getConfiguration().getStatus(); + return server.getStatus(); } @Override diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServer.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServer.java index bbc32eb727..bfc0feea5a 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServer.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServer.java @@ -167,6 +167,8 @@ public interface ActiveMQServer extends ServiceComponent { CriticalAnalyzer getCriticalAnalyzer(); + void updateStatus(String component, String statusJson); + /** * it will release hold a lock for the activation. */ diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ActiveMQServerImpl.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ActiveMQServerImpl.java index ccbb53013e..27b6ba1cc9 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ActiveMQServerImpl.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ActiveMQServerImpl.java @@ -403,6 +403,8 @@ public class ActiveMQServerImpl implements ActiveMQServer { } }; + private final ServerStatus serverStatus; + public ActiveMQServerImpl() { this(null, null, null); } @@ -481,6 +483,7 @@ public class ActiveMQServerImpl implements ActiveMQServer { this.serviceRegistry = serviceRegistry == null ? new ServiceRegistryImpl() : serviceRegistry; + this.serverStatus = new ServerStatus(this); } @Override @@ -608,6 +611,16 @@ public class ActiveMQServerImpl implements ActiveMQServer { propertiesFileUrl = fileUrltoBrokerProperties; } + @Override + public String getStatus() { + return serverStatus.asJson(); + } + + @Override + public void updateStatus(String component, String statusJson) { + serverStatus.update(component, statusJson); + } + private void internalStart() throws Exception { if (state != SERVER_STATE.STOPPED) { logger.debug("Server already started!"); @@ -615,6 +628,7 @@ public class ActiveMQServerImpl implements ActiveMQServer { } configuration.parseProperties(propertiesFileUrl); + updateStatus("configuration", configuration.getStatus()); initializeExecutorServices(); @@ -4388,6 +4402,7 @@ public class ActiveMQServerImpl implements ActiveMQServer { configurationReloadDeployed.set(false); if (isActive()) { configuration.parseProperties(propertiesFileUrl); + updateStatus("configuration", configuration.getStatus()); deployReloadableConfigFromConfiguration(); } } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ServerStatus.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ServerStatus.java new file mode 100644 index 0000000000..50fbe77533 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ServerStatus.java @@ -0,0 +1,69 @@ +/** + * 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 + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * 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.activemq.artemis.core.server.impl; + + +import java.util.HashMap; + +import org.apache.activemq.artemis.api.core.JsonUtil; +import org.apache.activemq.artemis.api.core.SimpleString; +import org.apache.activemq.artemis.json.JsonObject; +import org.apache.activemq.artemis.json.JsonObjectBuilder; +import org.apache.activemq.artemis.utils.JsonLoader; + +public class ServerStatus { + + private final ActiveMQServerImpl server; + private final HashMap<String, String> immutableStateValues = new HashMap<>(); + private JsonObject globalStatus = JsonLoader.createObjectBuilder().build(); + + public ServerStatus(ActiveMQServerImpl activeMQServer) { + this.server = activeMQServer; + immutableStateValues.put("version", server.getVersion().getFullVersion()); + } + + public synchronized String asJson() { + updateServerStatus(); + return globalStatus.toString(); + } + + private synchronized void updateServerStatus() { + HashMap<String, String> snapshotOfServerStatusAttributes = new HashMap<>(); + snapshotOfServerStatusAttributes.putAll(immutableStateValues); + snapshotOfServerStatusAttributes.put("identity", server.getIdentity()); + SimpleString nodeId = server.getNodeID(); + snapshotOfServerStatusAttributes.put("nodeId", nodeId == null ? null : nodeId.toString()); + snapshotOfServerStatusAttributes.put("uptime", server.getUptime()); + snapshotOfServerStatusAttributes.put("state", server.getState().toString()); + + update("server", JsonUtil.toJsonObject(snapshotOfServerStatusAttributes)); + } + + public synchronized void update(String component, String statusJson) { + JsonObjectBuilder jsonObjectBuilder = JsonLoader.createObjectBuilder(); + jsonObjectBuilder.add(component, JsonUtil.readJsonObject(statusJson)); + globalStatus = JsonUtil.mergeAndUpdate(globalStatus, jsonObjectBuilder.build()); + } + + public synchronized void update(String component, JsonObject componentStatus) { + JsonObjectBuilder jsonObjectBuilder = JsonLoader.createObjectBuilder(); + jsonObjectBuilder.add(component, componentStatus); + globalStatus = JsonUtil.mergeAndUpdate(globalStatus, jsonObjectBuilder.build()); + } + +} 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 84e90704c5..fb10c84f66 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 @@ -644,6 +644,7 @@ public class ConfigurationImplTest extends ActiveMQTestBase { } } } + properties.remove("status"); // this is not a simple symmetric property // now parse configuration.parsePrefixedProperties(properties, null); @@ -1289,6 +1290,55 @@ public class ConfigurationImplTest extends ActiveMQTestBase { Assert.assertEquals(SimpleString.toSimpleString("moreImportant"), configuration.getAddressSettings().get("Name.With.Dots").getExpiryAddress()); } + @Test + public void testStatusOnErrorApplyingProperties() throws Exception { + ConfigurationImpl configuration = new ConfigurationImpl(); + + Properties properties = new Properties(); + + properties.put("clusterConfigurations.cc.bonkers", "bla"); + + properties.put("notValid.#.expiryAddress", "sharedExpiry"); + properties.put("addressSettings.#.bla", "bla"); + properties.put("addressSettings.#.expiryAddress", "good"); + + String SHA = "34311"; + // status field json blob gives possibility for two-way interaction + // this value is reflected back but can be augmented + properties.put("status", "{ \"properties\": { \"sha\": \"" + SHA + "\"}}"); + + configuration.parsePrefixedProperties(properties, null); + + String jsonStatus = configuration.getStatus(); + + // errors reported + assertTrue(jsonStatus.contains("notValid")); + assertTrue(jsonStatus.contains("Unknown")); + assertTrue(jsonStatus.contains("bonkers")); + assertTrue(jsonStatus.contains("bla")); + + // input status reflected + assertTrue(jsonStatus.contains(SHA)); + // only errors reported, good property goes unmentioned + assertFalse(jsonStatus.contains("good")); + + // apply again with only good values, new sha.... verify no errors + properties.clear(); + + String UPDATED_SHA = "66666"; + // status field json blob gives possibility for two-way interaction + // this value is reflected back but can be augmented + properties.put("status", "{ \"properties\": { \"sha\": \"" + UPDATED_SHA + "\"}}"); + properties.put("addressSettings.#.expiryAddress", "changed"); + + configuration.parsePrefixedProperties(properties, null); + + jsonStatus = configuration.getStatus(); + + assertTrue(jsonStatus.contains(UPDATED_SHA)); + assertFalse(jsonStatus.contains(SHA)); + } + /** * To test ARTEMIS-926 * @throws Throwable diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/server/ConfigurationTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/server/ConfigurationTest.java index b3457f641a..641c9a4008 100644 --- a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/server/ConfigurationTest.java +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/server/ConfigurationTest.java @@ -95,8 +95,6 @@ public class ConfigurationTest extends ActiveMQTestBase { Assert.assertTrue(propsFile.exists()); - System.out.println("props: " + propsFile.getAbsolutePath()); - ActiveMQServer server = getActiveMQServer("duplicate-queues.xml"); server.setProperties(propsFile.getAbsolutePath()); try { @@ -127,6 +125,10 @@ public class ConfigurationTest extends ActiveMQTestBase { // verify round trip apply Assert.assertTrue(server.getActiveMQServerControl().getStatus().contains("2")); + // verify some server attributes + Assert.assertNotNull(server.getActiveMQServerControl().getStatus().contains("version")); + Assert.assertNotNull(server.getActiveMQServerControl().getStatus().contains("uptime")); + } finally { try { server.stop();
