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