brusdev commented on code in PR #5533: URL: https://github.com/apache/activemq-artemis/pull/5533#discussion_r1973751419
########## 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 was wondering if only passwords should be redact. Maybe for other users there are other sensitive settings to redact. This leads me to idea that rather than redact a few settings the full export should be encrypted. If the full export is encrypted it may be returned instead of storing it in a local file. -- 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