gtully commented on code in PR #5533: URL: https://github.com/apache/activemq-artemis/pull/5533#discussion_r1974020125
########## 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: this is a good point, while the broker does the ENC in specific cases, broker properties will allow any value to use ENC, in that case, if it is read as ENC, it should be redacted. These will need to be tracked. -- 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