|
| 1 | +package com.github.joschi.jadconfig.documentation; |
| 2 | + |
| 3 | +import com.github.joschi.jadconfig.Parameter; |
| 4 | +import com.github.joschi.jadconfig.ReflectionUtils; |
| 5 | +import com.github.joschi.jadconfig.documentation.printers.ConfigFileDocsPrinter; |
| 6 | +import com.github.joschi.jadconfig.documentation.printers.ConfigurationSection; |
| 7 | +import com.github.joschi.jadconfig.documentation.printers.CsvDocsPrinter; |
| 8 | +import com.github.joschi.jadconfig.documentation.printers.DocsPrinter; |
| 9 | +import jakarta.annotation.Nonnull; |
| 10 | +import org.apache.commons.lang3.ClassUtils; |
| 11 | + |
| 12 | +import java.io.FileWriter; |
| 13 | +import java.io.IOException; |
| 14 | +import java.lang.reflect.Field; |
| 15 | +import java.nio.charset.StandardCharsets; |
| 16 | +import java.util.*; |
| 17 | +import java.util.function.Supplier; |
| 18 | +import java.util.stream.Collectors; |
| 19 | + |
| 20 | +public class ConfigurationDocsGenerator { |
| 21 | + /** |
| 22 | + * This class is linked from the datanode pom.xml and generates conf.example and csv documentation. |
| 23 | + */ |
| 24 | + static void main(String[] args) throws IOException { |
| 25 | + final ConfigurationDocsGenerator generator = new ConfigurationDocsGenerator(); |
| 26 | + generator.generateDocumentation(parseDocumentationFormat(args), ConfigurationBeansSPI::loadConfigurationBeans); |
| 27 | + } |
| 28 | + |
| 29 | + @Nonnull |
| 30 | + private static DocumentationFormat parseDocumentationFormat(String[] args) { |
| 31 | + if (args.length != 2) { |
| 32 | + throw new IllegalArgumentException("This command needs two arguments - a format and file path. For example" + "csv ${project.build.directory}/configuration-documentation.csv"); |
| 33 | + } |
| 34 | + final String format = args[0].toLowerCase(Locale.ROOT).trim(); |
| 35 | + final String file = args[1]; |
| 36 | + return new DocumentationFormat(format, file); |
| 37 | + } |
| 38 | + |
| 39 | + public void generateDocumentation(DocumentationFormat format, Supplier<List<Object>> configurationBeans) throws IOException { |
| 40 | + final List<ConfigurationSection> sections = detectConfigurationSections(configurationBeans); |
| 41 | + try (final DocsPrinter writer = createWriter(format)) { |
| 42 | + writer.write(sections); |
| 43 | + } |
| 44 | + } |
| 45 | + |
| 46 | + private DocsPrinter createWriter(DocumentationFormat format) throws IOException { |
| 47 | + final FileWriter fileWriter = new FileWriter(format.outputFile(), StandardCharsets.UTF_8); |
| 48 | + // TODO: if the format list expands, introduce DI and factories for printers |
| 49 | + return switch (format.format()) { |
| 50 | + case "csv" -> new CsvDocsPrinter(fileWriter); |
| 51 | + case "conf" -> new ConfigFileDocsPrinter(fileWriter); |
| 52 | + default -> throw new IllegalArgumentException("Unsupported format " + format.format()); |
| 53 | + }; |
| 54 | + } |
| 55 | + |
| 56 | + /** |
| 57 | + * Collects all configuration options from all available configuration beans. |
| 58 | + */ |
| 59 | + private List<ConfigurationSection> detectConfigurationSections(Supplier<List<Object>> configurationBeans) { |
| 60 | + return configurationBeans.get() |
| 61 | + .stream() |
| 62 | + .map(ConfigurationDocsGenerator::beanToConfigSections) |
| 63 | + .sorted(Comparator.comparing(ConfigurationSection::hasPriority).reversed()) |
| 64 | + .toList(); |
| 65 | + } |
| 66 | + |
| 67 | + private static ConfigurationSection beanToConfigSections(Object configurationBean) { |
| 68 | + |
| 69 | + String sectionHeading = null; |
| 70 | + String sectionDescription = null; |
| 71 | + if (configurationBean.getClass().isAnnotationPresent(DocumentationSection.class)) { |
| 72 | + final DocumentationSection documentationSection = configurationBean.getClass().getAnnotation(DocumentationSection.class); |
| 73 | + sectionHeading = documentationSection.heading(); |
| 74 | + sectionDescription = documentationSection.description(); |
| 75 | + } |
| 76 | + |
| 77 | + final List<ConfigurationEntryWithSection> entries = Arrays.stream(configurationBean.getClass().getDeclaredFields()) |
| 78 | + .filter(f -> f.isAnnotationPresent(Parameter.class)) |
| 79 | + .filter(ConfigurationDocsGenerator::isPublicFacing) |
| 80 | + .map(f -> toConfigurationEntry(f, configurationBean)) |
| 81 | + .toList(); |
| 82 | + |
| 83 | + final List<ConfigurationEntry> entriesWithoutSection = getEntriesWithoutSection(entries); |
| 84 | + final List<ConfigurationSection> sortedSections = sectionsFromEntries(entries); |
| 85 | + |
| 86 | + return new ConfigurationSection(sectionHeading, sectionDescription, sortedSections, entriesWithoutSection); |
| 87 | + } |
| 88 | + |
| 89 | + @Nonnull |
| 90 | + private static List<ConfigurationSection> sectionsFromEntries(List<ConfigurationEntryWithSection> entries) { |
| 91 | + final Collection<ConfigurationSection> sections = entries.stream() |
| 92 | + .filter(ConfigurationEntryWithSection::hasSection) |
| 93 | + .collect(Collectors.groupingBy(ConfigurationEntryWithSection::sectionHeading, Collectors.collectingAndThen(Collectors.toList(), list -> new ConfigurationSection(list.iterator().next().sectionHeading(), list.iterator().next().sectionDescription(), Collections.emptyList(), list.stream().map(ConfigurationEntryWithSection::entry).collect(Collectors.toList()))))).values(); |
| 94 | + return sections.stream() |
| 95 | + .sorted(Comparator.comparing(ConfigurationSection::hasPriority, Comparator.reverseOrder())).toList(); |
| 96 | + } |
| 97 | + |
| 98 | + @Nonnull |
| 99 | + private static List<ConfigurationEntry> getEntriesWithoutSection(List<ConfigurationEntryWithSection> entries) { |
| 100 | + return entries.stream() |
| 101 | + .filter(e -> !e.hasSection()) |
| 102 | + .map(ConfigurationEntryWithSection::entry) |
| 103 | + .sorted(Comparator.comparing(ConfigurationEntry::hasPriority, Comparator.reverseOrder())) |
| 104 | + .collect(Collectors.toList()); |
| 105 | + } |
| 106 | + |
| 107 | + /** |
| 108 | + * There are some configuration options not intended for general usage, mainly just for system packages configuration. |
| 109 | + * |
| 110 | + * @see Documentation#visible() |
| 111 | + */ |
| 112 | + private static boolean isPublicFacing(Field f) { |
| 113 | + return !f.isAnnotationPresent(Documentation.class) || f.getAnnotation(Documentation.class).visible(); |
| 114 | + } |
| 115 | + |
| 116 | + private static ConfigurationEntryWithSection toConfigurationEntry(Field f, Object instance) { |
| 117 | + final String documentation = Optional.ofNullable(f.getAnnotation(Documentation.class)).map(Documentation::value).orElse(null); |
| 118 | + final Parameter parameter = f.getAnnotation(Parameter.class); |
| 119 | + final String propertyName = parameter.value(); |
| 120 | + final Object defaultValue = getDefaultValue(f, instance); |
| 121 | + final String type = getType(f); |
| 122 | + final boolean required = parameter.required(); |
| 123 | + |
| 124 | + final DocumentationSection documentationSection = f.getAnnotation(DocumentationSection.class); |
| 125 | + String sectionHeading = null; |
| 126 | + String sectionDescription = null; |
| 127 | + if (documentationSection != null) { |
| 128 | + sectionHeading = documentationSection.heading(); |
| 129 | + sectionDescription = documentationSection.description(); |
| 130 | + } |
| 131 | + final ConfigurationEntry entry = new ConfigurationEntry(instance.getClass(), f.getName(), type, propertyName, defaultValue, required, documentation); |
| 132 | + return new ConfigurationEntryWithSection(entry, sectionHeading, sectionDescription); |
| 133 | + } |
| 134 | + |
| 135 | + private static Object getDefaultValue(Field f, Object instance) { |
| 136 | + try { |
| 137 | + return ReflectionUtils.getFieldValue(instance, f); |
| 138 | + } catch (IllegalAccessException e) { |
| 139 | + throw new RuntimeException(e); |
| 140 | + } |
| 141 | + } |
| 142 | + |
| 143 | + private static String getType(Field f) { |
| 144 | + if (f.getType().isPrimitive()) { // unify primitive types and wrappers, e.g. int -> Integer |
| 145 | + return ClassUtils.primitiveToWrapper(f.getType()).getSimpleName(); |
| 146 | + } else { |
| 147 | + return f.getType().getSimpleName(); |
| 148 | + } |
| 149 | + } |
| 150 | +} |
0 commit comments