gtully commented on code in PR #5533:
URL: https://github.com/apache/activemq-artemis/pull/5533#discussion_r1973858996


##########
artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImpl.java:
##########
@@ -945,6 +950,166 @@ public <T> T convert(Class<T> type, Object value) {
       updateApplyStatus(propsId, errors);
    }
 
+   @Override
+   public void exportAsProperties(File file) throws Exception {
+      try (FileWriter writer = new FileWriter(file, StandardCharsets.UTF_8)) {
+         writeProperties(writer);
+      }
+   }
+
+   private void writeProperties(FileWriter writer) throws Exception {
+      final BeanUtilsBean beanUtilsBean = BeanUtilsBean.getInstance();
+      beanUtilsBean.getPropertyUtils().addBeanIntrospector(new 
FluentPropertyBeanIntrospectorWithIgnores());
+
+      try (BufferedWriter bufferedWriter = new BufferedWriter(writer)) {
+         export(beanUtilsBean, new Stack<String>(), bufferedWriter, this);
+      }
+   }
+
+   final Set<String> ignored = Set.of(
+      // we cannot import a map<string,set<string>> property and this feature 
is only applied by the xml parser
+      "securityRoleNameMappings",
+      // another xml ism using a deprecated config object
+      "queueConfigs");
+   private void export(BeanUtilsBean beanUtils, Stack<String> nested, 
BufferedWriter bufferedWriter, Object value) {
+
+      if (value instanceof Collection collection) {
+         if (!collection.isEmpty()) {
+            // collection of strings, as a comma list
+            if (collection.stream().findFirst().orElseThrow() instanceof 
String) {
+               exportKeyValue(nested, bufferedWriter, (String) 
collection.stream().collect(Collectors.joining(",")));
+            } else if (collection instanceof EnumSet enumSet) {
+               exportKeyValue(nested, bufferedWriter, (String) 
enumSet.stream().map(Object::toString).collect(Collectors.joining(",")));
+            } else {
+               // nested by name
+               collection.stream().forEach((Consumer<Object>) o -> {
+                  nested.push(extractName(o));
+                  export(beanUtils, nested, bufferedWriter, o);
+                  nested.pop();
+               });
+            }
+         }
+      } else if (value instanceof Map map) {
+         if (!map.isEmpty()) {
+            map.entrySet().forEach((Consumer<Map.Entry<?, ?>>) entry -> {
+               // nested by name
+               nested.push(entry.getKey().toString());
+               export(beanUtils, nested, bufferedWriter, entry.getValue());
+               nested.pop();
+            });
+         }
+      } else if (isComplexConfigObject(value)) {
+
+         // these need constructor properties or .class values as first entry
+         if (value instanceof HAPolicyConfiguration haPolicyConfiguration) {
+            exportKeyValue(nested, bufferedWriter, 
haPolicyConfiguration.getType().toString());
+         } else if (value instanceof StoreConfiguration storeConfiguration) {
+            exportKeyValue(nested, bufferedWriter, 
storeConfiguration.getStoreType().toString());
+         } else if (value instanceof NamedPropertyConfiguration 
namedPropertyConfiguration) {
+            exportKeyValue(nested, bufferedWriter, 
namedPropertyConfiguration.getName());
+         } else if (value instanceof BroadcastEndpointFactory 
broadcastEndpointFactory) {
+            exportKeyValue(nested, bufferedWriter, 
broadcastEndpointFactory.getClass().getCanonicalName() + ".class");
+         } else if (value instanceof ActiveMQMetricsPlugin plugin) {
+            exportKeyValue(nested, bufferedWriter, 
plugin.getClass().getCanonicalName() + ".class");
+            nested.push("init");
+            exportKeyValue(nested, bufferedWriter, "");
+            nested.pop();
+         }
+         // recursive export via accessors
+         
Arrays.stream(beanUtils.getPropertyUtils().getPropertyDescriptors(value)).filter(propertyDescriptor
 -> {
+
+            if (ignored.contains(propertyDescriptor.getName())) {
+               return false;
+            }
+            final Method descriptorReadMethod = 
propertyDescriptor.getReadMethod();
+            if (descriptorReadMethod == null) {
+               return false;
+            }
+            Method descriptorWriteMethod = propertyDescriptor.getWriteMethod();
+            if (descriptorWriteMethod == null) {
+               // we can write to a returned simple map ok
+               if 
(!propertyDescriptor.getPropertyType().isAssignableFrom(Map.class)) {
+                  return false;
+               }
+            }
+            return true;
+         }).sorted((a, b) -> 
String.CASE_INSENSITIVE_ORDER.compare(a.getName(), 
b.getName())).forEach(propertyDescriptor -> {
+            Object attributeValue = null;
+            try {
+               attributeValue = 
propertyDescriptor.getReadMethod().invoke(value, null);
+            } catch (Exception e) {
+               throw new RuntimeException("accessing: " + 
propertyDescriptor.getName()  + "@" + nested, e);
+            }
+            if (attributeValue != null) {
+               nested.push(propertyDescriptor.getName());
+               export(beanUtils, nested, bufferedWriter, attributeValue);
+               nested.pop();
+            }
+         });
+      } else {
+         // string form works ok otherwise
+         exportKeyValue(nested, bufferedWriter, value.toString());
+      }
+   }
+
+   private void exportKeyValue(Stack<String> nested, BufferedWriter 
bufferedWriter, String value) {
+      String key = writeKeyEquals(nested, bufferedWriter);
+
+      try {
+         if (shouldRedact(key)) {
+            bufferedWriter.write(REDACTED);
+         } else {
+            bufferedWriter.write(value);
+         }
+         bufferedWriter.newLine();
+      } catch (IOException e) {
+         throw new RuntimeException("error accessing: " + nested, e);
+      }
+   }
+
+   private boolean isComplexConfigObject(Object value) {
+      return !(value instanceof SimpleString || value instanceof Enum<?>) && 
value.getClass().getPackage().getName().contains("artemis");
+   }
+
+   private boolean shouldRedact(String name) {
+      return name.toUpperCase(Locale.ENGLISH).contains("PASSWORD");

Review Comment:
   I don't like the idea of returning the entire content, the intent is to 
provide a view of the effective config, and present the properties format that 
is tricky to grok. It is a transform the would be best done inplace, the 
closest writable place is the broker tmp dir.
   if the whole file is encrypted, the contents will be less useful.
   redacting the passwords is a miminum, but not exporting the config from the 
broker is the best way to keep the config secure. 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: gitbox-unsubscr...@activemq.apache.org

For queries about this service, please contact Infrastructure at:
us...@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: gitbox-unsubscr...@activemq.apache.org
For additional commands, e-mail: gitbox-h...@activemq.apache.org
For further information, visit: https://activemq.apache.org/contact


Reply via email to