You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@activemq.apache.org by gt...@apache.org on 2022/10/13 17:23:15 UTC

[activemq-artemis] branch main updated (b900a1e4bd -> 3b981b3920)

This is an automated email from the ASF dual-hosted git repository.

gtully pushed a change to branch main
in repository https://gitbox.apache.org/repos/asf/activemq-artemis.git


    from b900a1e4bd ARTEMIS-4020 Standardize the naming of Logger types for consistency
     new 03ef286cc8 ARTEMIS-4025 - trap failure to apply properties as errors and report via the broker status in a configuration/properties/errors status field
     new 3b981b3920 ARTEMIS-4028 - add alder32 checksum to the status of properties files read by the broker

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../FluentPropertyBeanIntrospectorWithIgnores.java |   2 +-
 .../apache/activemq/artemis/api/core/JsonUtil.java |  64 ++++++
 .../activemq/artemis/api/core/JsonUtilTest.java    | 104 ++++++++++
 .../core/config/impl/ConfigurationImpl.java        | 228 +++++++++++++++++++--
 .../management/impl/ActiveMQServerControlImpl.java |   2 +-
 .../artemis/core/server/ActiveMQServer.java        |   2 +
 .../core/server/impl/ActiveMQServerImpl.java       |  15 ++
 .../artemis/core/server/impl/ServerStatus.java     |  85 ++++++++
 .../core/security/jaas/ReloadableProperties.java   |  34 ++-
 .../core/config/impl/ConfigurationImplTest.java    |  51 +++++
 .../security/jaas/PropertiesLoginModuleTest.java   |  28 +++
 .../integration/server/ConfigurationTest.java      |   6 +-
 12 files changed, 600 insertions(+), 21 deletions(-)
 create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ServerStatus.java


[activemq-artemis] 01/02: ARTEMIS-4025 - trap failure to apply properties as errors and report via the broker status in a configuration/properties/errors status field

Posted by gt...@apache.org.
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 <ga...@gmail.com>
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();


[activemq-artemis] 02/02: ARTEMIS-4028 - add alder32 checksum to the status of properties files read by the broker

Posted by gt...@apache.org.
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 3b981b3920dc2214e943d9de81bd991fb7182eb6
Author: Gary Tully <ga...@gmail.com>
AuthorDate: Tue Oct 4 14:04:41 2022 +0100

    ARTEMIS-4028 - add alder32 checksum to the status of properties files read by the broker
---
 .../FluentPropertyBeanIntrospectorWithIgnores.java |  2 +-
 .../apache/activemq/artemis/api/core/JsonUtil.java | 15 ++++
 .../activemq/artemis/api/core/JsonUtilTest.java    | 19 +++++
 .../core/config/impl/ConfigurationImpl.java        | 93 +++++++++++-----------
 .../core/server/impl/ActiveMQServerImpl.java       |  6 +-
 .../artemis/core/server/impl/ServerStatus.java     | 58 +++++++++-----
 .../core/security/jaas/ReloadableProperties.java   | 34 +++++++-
 .../core/config/impl/ConfigurationImplTest.java    |  1 +
 .../security/jaas/PropertiesLoginModuleTest.java   | 28 +++++++
 9 files changed, 182 insertions(+), 74 deletions(-)

diff --git a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/uri/FluentPropertyBeanIntrospectorWithIgnores.java b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/uri/FluentPropertyBeanIntrospectorWithIgnores.java
index ca0b99b0a9..e21dddcf09 100644
--- a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/uri/FluentPropertyBeanIntrospectorWithIgnores.java
+++ b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/uri/FluentPropertyBeanIntrospectorWithIgnores.java
@@ -63,7 +63,7 @@ public class FluentPropertyBeanIntrospectorWithIgnores extends FluentPropertyBea
                   pd.setWriteMethod(m);
                }
             } catch (IntrospectionException e) {
-               logger.debug(e.getMessage(), e);
+               logger.trace("error for property named {}", propertyName, e);
             }
          }
       }
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 bfa7bbce31..8d193412d1 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
@@ -409,6 +409,21 @@ public final class JsonUtil {
       return jsonObjectBuilder.build();
    }
 
+   // component path, may contain x/y/z as a pointer to a nested object
+   public static JsonObjectBuilder objectBuilderWithValueAtPath(String componentPath, JsonValue componentStatus) {
+      JsonObjectBuilder jsonObjectBuilder = JsonLoader.createObjectBuilder();
+      String[] nestedComponents = componentPath.split("/", 0);
+      // may need to nest this status in objects
+      for (int i = nestedComponents.length - 1; i > 0; i--) {
+         JsonObjectBuilder nestedBuilder = JsonLoader.createObjectBuilder();
+         nestedBuilder.add(nestedComponents[i], componentStatus);
+         componentStatus = nestedBuilder.build();
+      }
+      jsonObjectBuilder.add(nestedComponents[0], componentStatus);
+      return jsonObjectBuilder;
+   }
+
+
    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 7c11ad4534..b2ff4394e4 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
@@ -212,4 +212,23 @@ public class JsonUtilTest {
 
       Assert.assertEquals(sourceTwo, JsonUtil.mergeAndUpdate(sourceOne, sourceTwo));
    }
+
+   @Test
+   public void testBuilderWithValueAtPath() {
+
+      JsonObjectBuilder jsonObjectBuilder = JsonLoader.createObjectBuilder();
+      JsonUtil.addToObject("x", "y", jsonObjectBuilder);
+      JsonObject target = jsonObjectBuilder.build();
+
+      JsonObjectBuilder nested = JsonUtil.objectBuilderWithValueAtPath("a/b/c", target);
+      JsonObject inserted = nested.build();
+      Assert.assertTrue(inserted.containsKey("a"));
+
+      Assert.assertEquals(target, inserted.getJsonObject("a").getJsonObject("b").getJsonObject("c"));
+
+      nested = JsonUtil.objectBuilderWithValueAtPath("c", target);
+      inserted = nested.build();
+      Assert.assertTrue(inserted.containsKey("c"));
+      Assert.assertEquals(target, inserted.getJsonObject("c"));
+   }
 }
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 a7116a1e54..c9d349ed1b 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
@@ -34,6 +34,7 @@ import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.net.URI;
 import java.net.URL;
+import java.nio.charset.StandardCharsets;
 import java.security.AccessController;
 import java.security.PrivilegedExceptionAction;
 import java.util.ArrayList;
@@ -123,6 +124,9 @@ import org.apache.commons.beanutils.expression.DefaultResolver;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import java.lang.invoke.MethodHandles;
+import java.util.zip.Adler32;
+import java.util.zip.Checksum;
+
 import org.apache.commons.beanutils.expression.Resolver;
 
 public class ConfigurationImpl implements Configuration, Serializable {
@@ -411,7 +415,8 @@ public class ConfigurationImpl implements Configuration, Serializable {
     * Parent folder for all data folders.
     */
    private File artemisInstance;
-   private transient volatile JsonObject status = JsonLoader.createObjectBuilder().build();
+   private transient JsonObject status = JsonLoader.createObjectBuilder().build();
+   private final transient Checksum checksum = new Adler32();
 
    @Override
    public String getJournalRetentionDirectory() {
@@ -501,15 +506,16 @@ public class ConfigurationImpl implements Configuration, Serializable {
                         try (FileInputStream fileInputStream = new FileInputStream(new File(dir, fileName)); BufferedInputStream reader = new BufferedInputStream(fileInputStream)) {
                            brokerProperties.clear();
                            brokerProperties.load(reader);
-                           parsePrefixedProperties(brokerProperties, null);
+                           parsePrefixedProperties(fileName, brokerProperties, null);
                         }
                      }
                   }
                }
             } else {
-               try (FileInputStream fileInputStream = new FileInputStream(fileUrl); BufferedInputStream reader = new BufferedInputStream(fileInputStream)) {
+               File file = new File(fileUrl);
+               try (FileInputStream fileInputStream = new FileInputStream(file); BufferedInputStream reader = new BufferedInputStream(fileInputStream)) {
                   brokerProperties.load(reader);
-                  parsePrefixedProperties(brokerProperties, null);
+                  parsePrefixedProperties(file.getName(), brokerProperties, null);
                }
             }
          }
@@ -519,9 +525,14 @@ public class ConfigurationImpl implements Configuration, Serializable {
    }
 
    public void parsePrefixedProperties(Properties properties, String prefix) throws Exception {
-      Map<String, Object> beanProperties = new LinkedHashMap<>();
+      parsePrefixedProperties("system", properties, prefix);
+   }
 
+   public void parsePrefixedProperties(String name, Properties properties, String prefix) throws Exception {
+      Map<String, Object> beanProperties = new LinkedHashMap<>();
+      long alder32Hash = 0;
       synchronized (properties) {
+         checksum.reset();
          String key = null;
          for (Map.Entry<Object, Object> entry : properties.entrySet()) {
             key = entry.getKey().toString();
@@ -531,53 +542,37 @@ public class ConfigurationImpl implements Configuration, Serializable {
                }
                key = entry.getKey().toString().substring(prefix.length());
             }
-            String value = XMLUtil.replaceSystemPropsInString(entry.getValue().toString());
+            String value = entry.getValue().toString();
+
+            checksum.update(key.getBytes(StandardCharsets.UTF_8));
+            checksum.update('=');
+            checksum.update(value.getBytes(StandardCharsets.UTF_8));
+
+            value = XMLUtil.replaceSystemPropsInString(value);
             value = PasswordMaskingUtil.resolveMask(isMaskPassword(), value, getPasswordCodec());
             key = XMLUtil.replaceSystemPropsInString(key);
             logger.debug("Property config, {}={}", key, value);
             beanProperties.put(key, value);
          }
+         alder32Hash = checksum.getValue();
       }
+      updateReadPropertiesStatus(name, alder32Hash);
 
       if (!beanProperties.isEmpty()) {
-         populateWithProperties(beanProperties);
+         populateWithProperties(name, beanProperties);
       }
    }
 
-   public void populateWithProperties(Map<String, Object> beanProperties) throws InvocationTargetException, IllegalAccessException {
+   public void populateWithProperties(final String propsId, Map<String, Object> beanProperties) throws InvocationTargetException, IllegalAccessException {
       CollectionAutoFillPropertiesUtil autoFillCollections = new CollectionAutoFillPropertiesUtil();
       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());
+               if (logger.isDebugEnabled()) {
+                  logger.debug("setProperty on {}, name: {}, value: {}", bean.getClass(), name, value);
                }
-
                // Resolve any nested expression to get the actual target bean
                Object target = bean;
                final Resolver resolver = getPropertyUtils().getResolver();
@@ -592,10 +587,7 @@ public class ConfigurationImpl implements Configuration, Serializable {
                      throw new InvocationTargetException(e, "No getter for property:" + name + ", on: " + bean);
                   }
                }
-               if (logger.isTraceEnabled()) {
-                  logger.trace("    Target bean = " + target);
-                  logger.trace("    Target name = " + name);
-               }
+               logger.trace("resolved target, bean: {}, name: {}", target.getClass(), name);
 
                // Declare local variables we will require
                final String propName = resolver.getProperty(name); // Simple name of target property
@@ -746,7 +738,7 @@ public class ConfigurationImpl implements Configuration, Serializable {
 
       BeanSupport.customise(beanUtils);
 
-      logger.debug("populate:" + this + ", " + beanProperties);
+      logger.trace("populate: bean: {} with {}", this, beanProperties);
 
       HashMap<String, String> errors = new HashMap<>();
       // Loop through the property name/value pairs to be set
@@ -757,7 +749,7 @@ public class ConfigurationImpl implements Configuration, Serializable {
             // Perform the assignment for this property
             beanUtils.setProperty(this, name, entry.getValue());
          } catch (InvocationTargetException invocationTargetException) {
-            logger.trace("failed to populate property key: " + name, invocationTargetException);
+            logger.trace("failed to populate property with key: {}", name, invocationTargetException);
             Throwable toLog = invocationTargetException;
             if (invocationTargetException.getCause() != null) {
                toLog = invocationTargetException.getCause();
@@ -768,23 +760,28 @@ public class ConfigurationImpl implements Configuration, Serializable {
             trackError(errors, entry, oops);
          }
       }
-      updateStatus(errors);
+      updateApplyStatus(propsId, 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);
+      logger.debug("failed to populate property entry({}), reason: {}", entry, oops);
       errors.put(entry.toString(), oops.getLocalizedMessage());
    }
 
-   private void updateStatus(HashMap<String, String> errors) {
-
-      JsonArrayBuilder errorsObjectArray = JsonLoader.createArrayBuilder();
+   private synchronized void updateApplyStatus(String propsId, HashMap<String, String> errors) {
+      JsonArrayBuilder errorsObjectArrayBuilder = JsonLoader.createArrayBuilder();
       for (Map.Entry<String, String> entry : errors.entrySet()) {
-         errorsObjectArray.add(JsonLoader.createObjectBuilder().add("value", entry.getKey()).add("reason", entry.getValue()));
+         errorsObjectArrayBuilder.add(JsonLoader.createObjectBuilder().add("value", entry.getKey()).add("reason", entry.getValue()));
       }
-      JsonObjectBuilder jsonObjectBuilder = JsonLoader.createObjectBuilder();
-      jsonObjectBuilder.add("properties",  JsonLoader.createObjectBuilder().add("errors", errorsObjectArray));
+      JsonObjectBuilder jsonObjectBuilder =
+         JsonUtil.objectBuilderWithValueAtPath("properties/" + propsId + "/errors", errorsObjectArrayBuilder.build());
+      status = JsonUtil.mergeAndUpdate(status, jsonObjectBuilder.build());
+   }
 
+   private synchronized void updateReadPropertiesStatus(String propsId, long alder32Hash) {
+      JsonObjectBuilder propertiesReadStatusBuilder = JsonLoader.createObjectBuilder();
+      propertiesReadStatusBuilder.add("alder32", String.valueOf(alder32Hash));
+      JsonObjectBuilder jsonObjectBuilder = JsonUtil.objectBuilderWithValueAtPath("properties/" + propsId, propertiesReadStatusBuilder.build());
       status = JsonUtil.mergeAndUpdate(status, jsonObjectBuilder.build());
    }
 
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 27b6ba1cc9..a0e499ca13 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
@@ -483,7 +483,7 @@ public class ActiveMQServerImpl implements ActiveMQServer {
 
       this.serviceRegistry = serviceRegistry == null ? new ServiceRegistryImpl() : serviceRegistry;
 
-      this.serverStatus = new ServerStatus(this);
+      this.serverStatus = ServerStatus.getInstanceFor(this);
    }
 
    @Override
@@ -628,7 +628,7 @@ public class ActiveMQServerImpl implements ActiveMQServer {
       }
 
       configuration.parseProperties(propertiesFileUrl);
-      updateStatus("configuration", configuration.getStatus());
+      updateStatus(ServerStatus.CONFIGURATION_COMPONENT, configuration.getStatus());
 
       initializeExecutorServices();
 
@@ -4402,7 +4402,7 @@ public class ActiveMQServerImpl implements ActiveMQServer {
       configurationReloadDeployed.set(false);
       if (isActive()) {
          configuration.parseProperties(propertiesFileUrl);
-         updateStatus("configuration", configuration.getStatus());
+         updateStatus(ServerStatus.CONFIGURATION_COMPONENT, 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
index 50fbe77533..8d9c927002 100644
--- 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
@@ -28,41 +28,57 @@ 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 static final String SERVER_COMPONENT = "server";
+   public static final String CONFIGURATION_COMPONENT = "configuration";
+   public static final String JAAS_COMPONENT = SERVER_COMPONENT + "/jaas";
+
+   private static final ServerStatus instance = new ServerStatus();
 
-   public ServerStatus(ActiveMQServerImpl activeMQServer) {
-      this.server = activeMQServer;
-      immutableStateValues.put("version", server.getVersion().getFullVersion());
+   public static synchronized ServerStatus getInstanceFor(ActiveMQServerImpl activeMQServer) {
+      if (instance.server == null) {
+         instance.server = activeMQServer;
+         instance.immutableStateValues.put("version", instance.server.getVersion().getFullVersion());
+      }
+      return instance;
    }
 
+   public static synchronized ServerStatus getInstance() {
+      return instance;
+   }
+
+   private ActiveMQServerImpl server;
+   private final HashMap<String, String> immutableStateValues = new HashMap<>();
+   private JsonObject globalStatus = JsonLoader.createObjectBuilder().build();
+
    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));
+      if (instance.server != null) {
+         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_COMPONENT, 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());
+      update(component, JsonUtil.readJsonObject(statusJson));
+   }
+
+   public synchronized void update(String component, HashMap<String, String> statusAttributes) {
+      update(component, JsonUtil.toJsonObject(statusAttributes));
    }
 
-   public synchronized void update(String component, JsonObject componentStatus) {
-      JsonObjectBuilder jsonObjectBuilder = JsonLoader.createObjectBuilder();
-      jsonObjectBuilder.add(component, componentStatus);
+   public synchronized void update(String componentPath, JsonObject componentStatus) {
+      JsonObjectBuilder jsonObjectBuilder = JsonUtil.objectBuilderWithValueAtPath(componentPath, componentStatus);
       globalStatus = JsonUtil.mergeAndUpdate(globalStatus, jsonObjectBuilder.build());
    }
 
diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/ReloadableProperties.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/ReloadableProperties.java
index 396340402c..a9864429e8 100644
--- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/ReloadableProperties.java
+++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/ReloadableProperties.java
@@ -19,6 +19,7 @@ package org.apache.activemq.artemis.spi.core.security.jaas;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
@@ -30,9 +31,14 @@ import java.util.regex.Pattern;
 import java.util.regex.PatternSyntaxException;
 
 import org.apache.activemq.artemis.core.server.ActiveMQServerLogger;
+import org.apache.activemq.artemis.core.server.impl.ServerStatus;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import java.lang.invoke.MethodHandles;
+import java.util.zip.Adler32;
+import java.util.zip.Checksum;
+
+import static org.apache.activemq.artemis.core.server.impl.ServerStatus.JAAS_COMPONENT;
 
 public class ReloadableProperties {
 
@@ -47,6 +53,8 @@ public class ReloadableProperties {
    private Map<String, Pattern> regexpProps;
    private long reloadTime = -1;
    private final PropertiesLoader.FileNameKey key;
+   private final Checksum adler32 = new Adler32();
+   private long checksum = 0;
 
    public ReloadableProperties(PropertiesLoader.FileNameKey key) {
       this.key = key;
@@ -58,9 +66,25 @@ public class ReloadableProperties {
 
    public synchronized ReloadableProperties obtained() {
       if (reloadTime < 0 || (key.isReload() && hasModificationAfter(reloadTime))) {
-         props = new Properties();
+
+         adler32.reset();
+         props = new Properties() {
+            // want to checksum in read order
+            @Override
+            public synchronized Object put(Object key, Object value) {
+               String sKey = key instanceof String ? (String)key : null;
+               String sValue = value instanceof String ? (String)value : null;
+               if (sKey != null && sValue != null) {
+                  adler32.update(sKey.getBytes(StandardCharsets.UTF_8));
+                  adler32.update('=');
+                  adler32.update(sValue.getBytes(StandardCharsets.UTF_8));
+               }
+               return super.put(key, value);
+            }
+         };
          try {
             load(key.file(), props);
+            checksum = adler32.getValue();
             invertedProps = null;
             invertedValueProps = null;
             regexpProps = null;
@@ -74,10 +98,18 @@ public class ReloadableProperties {
             }
          }
          reloadTime = System.currentTimeMillis();
+         updateStatus();
       }
       return this;
    }
 
+   private void updateStatus() {
+      HashMap<String, String> statusAttributes = new HashMap<>();
+      statusAttributes.put("Alder32", String.valueOf(checksum));
+      statusAttributes.put("reloadTime", String.valueOf(reloadTime));
+      ServerStatus.getInstance().update(JAAS_COMPONENT + "/properties/" + key.file.getName(),  statusAttributes);
+   }
+
    public synchronized Map<String, String> invertedPropertiesMap() {
       if (invertedProps == null) {
          invertedProps = new HashMap<>(props.size());
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 fb10c84f66..1c35aa9eee 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
@@ -1337,6 +1337,7 @@ public class ConfigurationImplTest extends ActiveMQTestBase {
 
       assertTrue(jsonStatus.contains(UPDATED_SHA));
       assertFalse(jsonStatus.contains(SHA));
+      assertTrue(jsonStatus.contains("alder32"));
    }
 
    /**
diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/PropertiesLoginModuleTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/PropertiesLoginModuleTest.java
index 540c722183..1f5274388d 100644
--- a/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/PropertiesLoginModuleTest.java
+++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/PropertiesLoginModuleTest.java
@@ -25,19 +25,25 @@ import javax.security.auth.callback.UnsupportedCallbackException;
 import javax.security.auth.login.FailedLoginException;
 import javax.security.auth.login.LoginContext;
 import javax.security.auth.login.LoginException;
+import java.io.BufferedReader;
 import java.io.File;
+import java.io.FileReader;
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.net.URL;
 import java.net.URLDecoder;
 import java.nio.charset.StandardCharsets;
 
+import org.apache.activemq.artemis.core.server.impl.ServerStatus;
 import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal;
 import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal;
 import org.apache.commons.io.FileUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import java.lang.invoke.MethodHandles;
+import java.util.zip.Adler32;
+import java.util.zip.Checksum;
+
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -104,6 +110,9 @@ public class PropertiesLoginModuleTest extends Assert {
       assertEquals("Should have one user principal", 1, subject.getPrincipals(UserPrincipal.class).size());
       assertEquals("Should have two group principals", 2, subject.getPrincipals(RolePrincipal.class).size());
 
+      String hashOriginal = genHash(usersFile);
+      assertTrue("Contains hash", ServerStatus.getInstance().asJson().contains(hashOriginal));
+
       context.logout();
 
       assertEquals("Should have zero principals", 0, subject.getPrincipals().size());
@@ -127,9 +136,28 @@ public class PropertiesLoginModuleTest extends Assert {
 
       context.logout();
 
+      String hashUpdated = genHash(usersFile);
+      assertTrue("Contains hashUpdated", ServerStatus.getInstance().asJson().contains(hashUpdated));
+      assertNotEquals(hashOriginal, hashUpdated);
+
       assertEquals("Should have zero principals", 0, subject.getPrincipals().size());
    }
 
+   private String genHash(File usersFile) {
+      Checksum hash = new Adler32();
+
+      try (FileReader fileReader = new FileReader(usersFile); BufferedReader bufferedReader = new BufferedReader(fileReader)) {
+         String line = null;
+         while ((line = bufferedReader.readLine()) != null) {
+            if (!line.startsWith("#") && !line.isBlank()) {
+               hash.update(line.getBytes(StandardCharsets.UTF_8));
+            }
+         }
+      } catch (Exception expectedEof) {
+      }
+      return String.valueOf(hash.getValue());
+   }
+
    @Test
    public void testBadUseridLogin() throws Exception {
       LoginContext context = new LoginContext("PropertiesLogin", new UserPassHandler("BAD", "secret"));