You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by pk...@apache.org on 2024/02/02 16:00:34 UTC

(logging-log4j-tools) branch processor created (now 189b5c6)

This is an automated email from the ASF dual-hosted git repository.

pkarwasz pushed a change to branch processor
in repository https://gitbox.apache.org/repos/asf/logging-log4j-tools.git


      at 189b5c6  Add annotation processor to generate documentation

This branch includes the following new commits:

     new 189b5c6  Add annotation processor to generate documentation

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(logging-log4j-tools) 01/01: Add annotation processor to generate documentation

Posted by pk...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

pkarwasz pushed a commit to branch processor
in repository https://gitbox.apache.org/repos/asf/logging-log4j-tools.git

commit 189b5c6324938ecdfad5d72af38564639258ba8e
Author: Piotr P. Karwasz <pi...@karwasz.org>
AuthorDate: Fri Feb 2 16:50:03 2024 +0100

    Add annotation processor to generate documentation
    
    We add a documentation-generating annotation processor.
    
    By default it generates a `plugins.xml` descriptor in the
    `META-INF/log4j` directory of the output folder.
    
    Currently it supports:
    
     * Log4j Plugins 3.x annotations **only**,
     * Both factory methods and builders.
    
    Closes apache/logging-log4j2#1956.
---
 .../log4j/docgen/processor/DocGenProcessor.java    | 446 +++++++++++++++++++++
 .../src/test/it/example/AbstractAppender.java      |  23 ++
 log4j-docgen/src/test/it/example/Appender.java     |  23 ++
 log4j-docgen/src/test/it/example/BaseAppender.java |  23 ++
 log4j-docgen/src/test/it/example/Filter.java       |  23 ++
 log4j-docgen/src/test/it/example/Layout.java       |  23 ++
 log4j-docgen/src/test/it/example/MyAppender.java   | 141 +++++++
 log4j-docgen/src/test/it/example/MyEnum.java       |  39 ++
 log4j-docgen/src/test/it/example/MyOldLayout.java  |  62 +++
 .../docgen/processor/DocGenProcessorTest.java      |  98 +++++
 .../expected/processor/META-INF/log4j/plugins.xml  | 173 ++++++++
 11 files changed, 1074 insertions(+)

diff --git a/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/DocGenProcessor.java b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/DocGenProcessor.java
new file mode 100644
index 0000000..0bade93
--- /dev/null
+++ b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/DocGenProcessor.java
@@ -0,0 +1,446 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.docgen.processor;
+
+import static org.apache.commons.lang3.StringUtils.defaultIfEmpty;
+import static org.apache.commons.lang3.StringUtils.defaultString;
+
+import com.sun.source.doctree.DocCommentTree;
+import com.sun.source.doctree.DocTree;
+import com.sun.source.doctree.ParamTree;
+import com.sun.source.util.DocTrees;
+import java.io.IOException;
+import java.io.Writer;
+import java.lang.annotation.Annotation;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.Messager;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.annotation.processing.RoundEnvironment;
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.annotation.processing.SupportedSourceVersion;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.QualifiedNameable;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.ArrayType;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.PrimitiveType;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.type.TypeVisitor;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.SimpleTypeVisitor8;
+import javax.lang.model.util.Types;
+import javax.tools.Diagnostic;
+import javax.tools.FileObject;
+import javax.tools.StandardLocation;
+import javax.xml.stream.XMLStreamException;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.docgen.AbstractType;
+import org.apache.logging.log4j.docgen.Description;
+import org.apache.logging.log4j.docgen.PluginAttribute;
+import org.apache.logging.log4j.docgen.PluginElement;
+import org.apache.logging.log4j.docgen.PluginSet;
+import org.apache.logging.log4j.docgen.PluginType;
+import org.apache.logging.log4j.docgen.ScalarType;
+import org.apache.logging.log4j.docgen.ScalarValue;
+import org.apache.logging.log4j.docgen.Type;
+import org.apache.logging.log4j.docgen.io.stax.PluginBundleStaxWriter;
+
+@SupportedAnnotationTypes("org.apache.logging.log4j.plugins.*")
+@SupportedSourceVersion(SourceVersion.RELEASE_17)
+public class DocGenProcessor extends AbstractProcessor {
+
+    private static final String MULTIPLICITY_UNBOUNDED = "*";
+    private static final TypeVisitor<String, Void> CLASS_NAME_VISITOR = new ClassNameVisitor();
+    private static final CharSequence[] GETTER_SETTER_PREFIXES = {"get", "is", "set"};
+    private static final List<Class<? extends Annotation>> PROPERTY_ANNOTATION_TYPES = List.of(
+            org.apache.logging.log4j.plugins.PluginBuilderAttribute.class,
+            org.apache.logging.log4j.plugins.PluginAttribute.class,
+            org.apache.logging.log4j.plugins.PluginElement.class);
+    private static final List<Class<? extends Annotation>> FACTORY_ANNOTATION_TYPES = List.of(
+            org.apache.logging.log4j.plugins.Factory.class, org.apache.logging.log4j.plugins.PluginFactory.class);
+
+    // Plugins
+    private final Map<String, PluginType> pluginTypes = new TreeMap<>();
+    // Other interfaces
+    private final Map<String, DeclaredType> abstractTypes = new TreeMap<>();
+    // Scalar types
+    private final Map<String, DeclaredType> scalarTypes = new TreeMap<>();
+
+    private AsciidocConverter converter;
+    private DocTrees docTrees;
+    private Elements elements;
+    private Types types;
+    private Messager messager;
+
+    @Override
+    public synchronized void init(final ProcessingEnvironment processingEnv) {
+        super.init(processingEnv);
+        docTrees = DocTrees.instance(processingEnv);
+        converter = new AsciidocConverter(docTrees);
+        elements = processingEnv.getElementUtils();
+        types = processingEnv.getTypeUtils();
+        messager = processingEnv.getMessager();
+    }
+
+    @Override
+    public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv) {
+        final PluginSet set = new PluginSet();
+        // First step: document plugins
+        roundEnv.getElementsAnnotatedWith(org.apache.logging.log4j.plugins.Plugin.class)
+                .forEach(element -> {
+                    final PluginType pluginType = new PluginType();
+                    final org.apache.logging.log4j.plugins.Plugin pluginAnnotation =
+                            element.getAnnotation(org.apache.logging.log4j.plugins.Plugin.class);
+                    processPlugin((TypeElement) element, pluginAnnotation, pluginType);
+                    pluginTypes.put(pluginType.getClassName(), pluginType);
+                });
+        pluginTypes.values().forEach(set::addPlugin);
+        // Second step: document abstract types
+        abstractTypes.values().forEach(type -> {
+            final AbstractType abstractType = new AbstractType();
+            processAbstractType(type, abstractType);
+            if (!abstractType.getDescription().getText().isEmpty()) {
+                set.addAbstractType(abstractType);
+            }
+        });
+        // Second step: document scalars
+        scalarTypes.values().forEach(type -> {
+            final ScalarType scalarType = new ScalarType();
+            processScalarType(type, scalarType);
+            set.addScalar(scalarType);
+        });
+        // Write the result file
+        if (roundEnv.processingOver()) {
+            try {
+                final FileObject output = processingEnv
+                        .getFiler()
+                        .createResource(StandardLocation.CLASS_OUTPUT, "", "META-INF/log4j/plugins.xml");
+
+                try (final Writer writer = output.openWriter()) {
+                    new PluginBundleStaxWriter().write(writer, set);
+                }
+            } catch (final IOException | XMLStreamException e) {
+                messager.printMessage(
+                        Diagnostic.Kind.ERROR,
+                        "An error occurred while writing to `META-INF/log4j/plugins.xml`: " + e.getMessage());
+            }
+        }
+        return false;
+    }
+
+    private void processType(final DeclaredType type, final Type docgenType) {
+        // Class name
+        docgenType.setClassName(type.accept(CLASS_NAME_VISITOR, null));
+        // Description
+        docgenType.setDescription(createDescription(type.asElement(), null));
+    }
+
+    private void processAbstractType(final DeclaredType type, final AbstractType abstractType) {
+        processType(type, abstractType);
+    }
+
+    private void processScalarType(final DeclaredType type, final ScalarType scalarType) {
+        processType(type, scalarType);
+        final TypeMirror enumType =
+                types.erasure(elements.getTypeElement("java.lang.Enum").asType());
+        if (types.isSubtype(type, enumType)) {
+            for (final Element element : elements.getAllMembers((TypeElement) type.asElement())) {
+                if (element instanceof final VariableElement field
+                        && field.getModifiers().contains(Modifier.STATIC)
+                        && types.isSameType(field.asType(), type)) {
+                    final ScalarValue value = new ScalarValue();
+                    value.setDescription(createDescription(element, null));
+                    value.setName(field.getSimpleName().toString());
+                    scalarType.addValue(value);
+                }
+            }
+        }
+    }
+
+    private Map<String, String> processParameterDescriptions(final Element element) {
+        final Map<String, String> descriptions = new HashMap<>();
+        final DocCommentTree docCommentTree = docTrees.getDocCommentTree(element);
+        if (docCommentTree != null) {
+            for (final DocTree tree : docCommentTree.getBlockTags()) {
+                if (tree.getKind() == DocTree.Kind.PARAM) {
+                    final ParamTree paramTree = (ParamTree) tree;
+                    final String name = paramTree.getName().getName().toString();
+                    descriptions.put(name, defaultString(converter.toAsciiDoc(paramTree)));
+                }
+            }
+        }
+        return descriptions;
+    }
+
+    private void processPlugin(
+            final TypeElement element,
+            final org.apache.logging.log4j.plugins.Plugin pluginAnnotation,
+            final PluginType pluginType) {
+        final DeclaredType type = (DeclaredType) element.asType();
+        processAbstractType(type, pluginType);
+        // Name
+        pluginType.setName(getPluginName(element, pluginAnnotation));
+        // Namespace
+        final org.apache.logging.log4j.plugins.Namespace namespace =
+                element.getAnnotation(org.apache.logging.log4j.plugins.Namespace.class);
+        pluginType.setNamespace(namespace != null ? namespace.value() : "Core");
+        // Supertypes
+        registerSupertypes(type).forEach(pluginType::addSupertype);
+        // Plugin factory
+        for (final Element member : elements.getAllMembers(element)) {
+            if (!findAllAnnotationsOnMember(member, FACTORY_ANNOTATION_TYPES).isEmpty()) {
+                final ExecutableElement executable = (ExecutableElement) member;
+                final Map<String, String> descriptions = processParameterDescriptions(executable);
+                final List<? extends VariableElement> parameters = executable.getParameters();
+                if (parameters.isEmpty()) {
+                    // We have a builder
+                    final TypeMirror returnType = executable.getReturnType();
+                    if (returnType instanceof DeclaredType) {
+                        processProperties(
+                                elements.getAllMembers((TypeElement) ((DeclaredType) returnType).asElement()),
+                                descriptions,
+                                pluginType);
+                    } else {
+                        messager.printMessage(
+                                Diagnostic.Kind.WARNING,
+                                "The return type of a @PluginFactory annotated method should be a concrete class.",
+                                member);
+                    }
+                } else {
+                    // Old style factory method
+                    processProperties(parameters, descriptions, pluginType);
+                }
+            }
+        }
+    }
+
+    private void processProperties(
+            final Iterable<? extends Element> members,
+            final Map<? super String, String> descriptions,
+            final PluginType pluginType) {
+        final Collection<PluginAttribute> pluginAttributes =
+                new TreeSet<>(Comparator.comparing(PluginAttribute::getName));
+        final Collection<PluginElement> pluginElements = new TreeSet<>(Comparator.comparing(PluginElement::getType));
+        // Gather documentation, which can be on any member.
+        for (final Element member : members) {
+            final String name = getMemberName(member);
+            final String asciidoc = converter.toAsciiDoc(member);
+            descriptions.compute(name, (key, value) -> Stream.of(value, asciidoc)
+                    .filter(StringUtils::isNotEmpty)
+                    .collect(Collectors.joining("\n")));
+        }
+        // Creates attributes and elements
+        for (final Element member : members) {
+            final String description = descriptions.get(getMemberName(member));
+            for (final Annotation annotation : findAllAnnotationsOnMember(member, PROPERTY_ANNOTATION_TYPES)) {
+                if (annotation instanceof final org.apache.logging.log4j.plugins.PluginBuilderAttribute attribute) {
+                    pluginAttributes.add(createPluginAttribute(member, description, attribute.value()));
+                }
+                if (annotation instanceof final org.apache.logging.log4j.plugins.PluginAttribute attribute) {
+                    pluginAttributes.add(createPluginAttribute(member, description, attribute.value()));
+                }
+                if (annotation instanceof org.apache.logging.log4j.plugins.PluginElement) {
+                    pluginElements.add(createPluginElement(member, description));
+                }
+            }
+        }
+        pluginAttributes.forEach(pluginType::addAttribute);
+        pluginElements.forEach(pluginType::addElement);
+    }
+
+    private Description createDescription(final String asciidoc) {
+        final Description description = new Description();
+        description.setText(StringUtils.stripToEmpty(asciidoc));
+        return description;
+    }
+
+    private Description createDescription(final Element element, final String fallback) {
+        return createDescription(defaultIfEmpty(converter.toAsciiDoc(element), defaultString(fallback)));
+    }
+
+    private PluginAttribute createPluginAttribute(
+            final Element element, final String description, final String specifiedName) {
+        final PluginAttribute attribute = new PluginAttribute();
+        // Name
+        attribute.setName(specifiedName.isEmpty() ? getMemberName(element) : specifiedName);
+        // Type
+        final TypeMirror type = getMemberType(element);
+        final String className = type.accept(CLASS_NAME_VISITOR, null);
+        if (type instanceof DeclaredType && !"java.lang.String".equals(type.accept(CLASS_NAME_VISITOR, null))) {
+            scalarTypes.putIfAbsent(className, (DeclaredType) type);
+        }
+        attribute.setType(className);
+        // Description
+        attribute.setDescription(createDescription(element, description));
+        // Required
+        if (element.getAnnotation(org.apache.logging.log4j.plugins.validation.constraints.Required.class) != null) {
+            attribute.setRequired(true);
+        }
+        // Default value
+        final Object defaultValue = element instanceof final VariableElement field ? field.getConstantValue() : null;
+        if (defaultValue != null) {
+            attribute.setDefaultValue(elements.getConstantExpression(defaultValue));
+        }
+        // TODO: add the value of the property used, when we add it to the annotation.
+        return attribute;
+    }
+
+    private PluginElement createPluginElement(final Element element, final String description) {
+        final PluginElement pluginElement = new PluginElement();
+        // Type and multiplicity
+        final TypeMirror elementType = getMemberType(element);
+        if (elementType instanceof final ArrayType arrayType) {
+            pluginElement.setType(arrayType.getComponentType().accept(CLASS_NAME_VISITOR, null));
+            pluginElement.setMultiplicity(MULTIPLICITY_UNBOUNDED);
+        } else {
+            pluginElement.setType(elementType.accept(CLASS_NAME_VISITOR, null));
+        }
+        // Required
+        if (element.getAnnotation(org.apache.logging.log4j.plugins.validation.constraints.Required.class) != null) {
+            pluginElement.setRequired(true);
+        }
+        // Description
+        pluginElement.setDescription(createDescription(element, description));
+        return pluginElement;
+    }
+
+    /**
+     * Register all the supertypes of the given type for doc processing.
+     * @param type a plugin type,
+     * @return the set of FQCN of all supertypes.
+     */
+    private Set<String> registerSupertypes(final TypeMirror type) {
+        return registerSupertypes(type, new TreeSet<>());
+    }
+
+    private Set<String> registerSupertypes(final TypeMirror type, final Set<String> visited) {
+        for (final TypeMirror supertype : types.directSupertypes(type)) {
+            final String className = supertype.accept(CLASS_NAME_VISITOR, null);
+            if (visited.add(className)) {
+                abstractTypes.putIfAbsent(className, (DeclaredType) supertype);
+                registerSupertypes(supertype, visited);
+            }
+        }
+        return visited;
+    }
+
+    private String getPluginName(final Element element, final org.apache.logging.log4j.plugins.Plugin annotation) {
+        final String value = annotation.value();
+        return value.isEmpty() ? element.getSimpleName().toString() : value;
+    }
+
+    private Collection<? extends Annotation> findAllAnnotationsOnMember(
+            final Element element, final Collection<Class<? extends Annotation>> annotationTypes) {
+        final Collection<Annotation> annotations = new HashSet<>();
+        findAllAnnotationsOnMember(element, annotationTypes, annotations);
+        return annotations;
+    }
+
+    private void findAllAnnotationsOnMember(
+            final Element element,
+            final Collection<Class<? extends Annotation>> annotationTypes,
+            final Collection<? super Annotation> annotations) {
+        for (final Class<? extends Annotation> annotationType : annotationTypes) {
+            final Annotation annotation = element.getAnnotation(annotationType);
+            if (annotation != null) {
+                annotations.add(annotation);
+            }
+        }
+        // Annotated method parameter
+        if (element.getKind() == ElementKind.METHOD) {
+            final String name = element.getSimpleName().toString();
+            final List<? extends VariableElement> parameters = ((ExecutableElement) element).getParameters();
+            if (StringUtils.startsWithAny(name, GETTER_SETTER_PREFIXES) && parameters.size() == 1) {
+                findAllAnnotationsOnMember(parameters.get(0), annotationTypes, annotations);
+            }
+        }
+    }
+
+    private TypeMirror getMemberType(final Element element) {
+        switch (element.getKind()) {
+            case FIELD:
+            case PARAMETER:
+                return element.asType();
+            case METHOD:
+                final ExecutableElement executable = (ExecutableElement) element;
+                final TypeMirror returnType = executable.getReturnType();
+                final List<? extends VariableElement> parameters = executable.getParameters();
+                if (returnType.getKind() == TypeKind.VOID && parameters.size() == 1) {
+                    return parameters.get(0).asType();
+                }
+                if (parameters.isEmpty()) {
+                    return returnType;
+                }
+                // fall through
+            default:
+                throw new IllegalArgumentException();
+        }
+    }
+
+    private String getMemberName(final Element element) {
+        String name = element.getSimpleName().toString();
+        if (element instanceof ExecutableElement && StringUtils.startsWithAny(name, GETTER_SETTER_PREFIXES)) {
+            name = name.substring(name.startsWith("is") ? 2 : 3);
+            if (!name.isEmpty()) {
+                final char first = name.charAt(0);
+                name = Character.toLowerCase(first) + name.substring(1);
+            }
+        }
+        return name;
+    }
+
+    private static class ClassNameVisitor extends SimpleTypeVisitor8<String, Void> {
+
+        @Override
+        public String visitDeclared(final DeclaredType t, final Void unused) {
+            final QualifiedNameable typeElement = (QualifiedNameable) t.asElement();
+            return typeElement.getQualifiedName().toString();
+        }
+
+        @Override
+        public String visitPrimitive(final PrimitiveType t, final Void unused) {
+            return switch (t.getKind()) {
+                case BOOLEAN -> "boolean";
+                case BYTE -> "byte";
+                case SHORT -> "short";
+                case INT -> "int";
+                case LONG -> "long";
+                case CHAR -> "char";
+                case FLOAT -> "float";
+                case DOUBLE -> "double";
+                default -> throw new IllegalArgumentException();
+            };
+        }
+    }
+}
diff --git a/log4j-docgen/src/test/it/example/AbstractAppender.java b/log4j-docgen/src/test/it/example/AbstractAppender.java
new file mode 100644
index 0000000..61f1122
--- /dev/null
+++ b/log4j-docgen/src/test/it/example/AbstractAppender.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example;
+
+/**
+ * An example of base abstract class.
+ */
+public abstract class AbstractAppender implements BaseAppender {
+}
diff --git a/log4j-docgen/src/test/it/example/Appender.java b/log4j-docgen/src/test/it/example/Appender.java
new file mode 100644
index 0000000..a2045ae
--- /dev/null
+++ b/log4j-docgen/src/test/it/example/Appender.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example;
+
+/**
+ * Extended interface that also allows to do {@code baz}.
+ */
+public interface Appender extends BaseAppender {
+}
diff --git a/log4j-docgen/src/test/it/example/BaseAppender.java b/log4j-docgen/src/test/it/example/BaseAppender.java
new file mode 100644
index 0000000..e405fec
--- /dev/null
+++ b/log4j-docgen/src/test/it/example/BaseAppender.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example;
+
+/**
+ * Base interface for appenders.
+ */
+public interface BaseAppender {
+}
diff --git a/log4j-docgen/src/test/it/example/Filter.java b/log4j-docgen/src/test/it/example/Filter.java
new file mode 100644
index 0000000..6250ce2
--- /dev/null
+++ b/log4j-docgen/src/test/it/example/Filter.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example;
+
+/**
+ * Filters messages.
+ */
+public interface Filter {
+}
diff --git a/log4j-docgen/src/test/it/example/Layout.java b/log4j-docgen/src/test/it/example/Layout.java
new file mode 100644
index 0000000..5884c6d
--- /dev/null
+++ b/log4j-docgen/src/test/it/example/Layout.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example;
+
+/**
+ * Formats messages.
+ */
+public interface Layout {
+}
diff --git a/log4j-docgen/src/test/it/example/MyAppender.java b/log4j-docgen/src/test/it/example/MyAppender.java
new file mode 100644
index 0000000..45bcbb2
--- /dev/null
+++ b/log4j-docgen/src/test/it/example/MyAppender.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example;
+
+import org.apache.logging.log4j.plugins.Namespace;
+import org.apache.logging.log4j.plugins.Plugin;
+import org.apache.logging.log4j.plugins.PluginBuilderAttribute;
+import org.apache.logging.log4j.plugins.PluginElement;
+import org.apache.logging.log4j.plugins.PluginFactory;
+
+/**
+ * Example plugin
+ * <p>
+ *     This is an example plugin. It has the following characteristics:
+ * </p>
+ * <ol>
+ *     <li>Plugin name: {@code MyPlugin},</li>
+ *     <li>Namespace: default (i.e. {@code Core}).</li>
+ * </ol>
+ * <p>
+ *     It also implements:
+ * </p>
+ * <ul>
+ *     <li>{@link Appender},</li>
+ *     <li>{@link BaseAppender}</li>
+ * </ul>
+ */
+@Plugin
+@Namespace("namespace")
+public final class MyAppender extends AbstractAppender implements Appender {
+
+    public static final class Builder implements org.apache.logging.log4j.plugins.util.Builder<MyAppender> {
+
+        /**
+         * A {@code char} attribute.
+         */
+        @PluginBuilderAttribute
+        private char charAtt = 'L';
+
+        /**
+         * A {@code short} attribute annotated on type.
+         */
+        private @PluginBuilderAttribute short shortAtt = 42;
+
+        /**
+         * An {@code int} attribute.
+         */
+        @PluginBuilderAttribute
+        private int intAtt = 4242;
+
+        /**
+         * A {@code long} attribute annotated on type.
+         */
+        private @PluginBuilderAttribute long longAtt = 424242L;
+
+        /**
+         * A {@code String} attribute.
+         */
+        @PluginBuilderAttribute
+        private String stringAtt;
+
+        /**
+         * An attribute whose name differs from the field name.
+         */
+        @PluginBuilderAttribute("anotherName")
+        private String origName;
+
+        /**
+         * An attribute that is an enumeration annotated on type.
+         */
+        private @PluginBuilderAttribute MyEnum enumAtt;
+
+        /**
+         * An attribute of type {@code float}.
+         */
+        private @PluginBuilderAttribute float floatAtt;
+
+        /**
+         * An attribute of type {@code double}.
+         */
+        private @PluginBuilderAttribute double aDouble;
+
+        /**
+         * An element with multiplicity 1.
+         */
+        @PluginElement
+        private Layout layout;
+
+        private Object notAnAttribute;
+
+        /**
+         * A {@code boolean} attribute with annotated type.
+         */
+        public void setBooleanAtt(final @PluginBuilderAttribute boolean booleanAtt) {}
+
+        /**
+         * A {@code byte} attribute with annotated parameter.
+         */
+        public void setByteAtt(@PluginBuilderAttribute final byte byteAtt) {}
+
+        /**
+         * An element with multiplicity n with annotated setter.
+         */
+        @PluginElement
+        public void setFilters(final Filter[] filters) {}
+
+        /**
+         * An element that is not an interface with annotated parameter.
+         */
+        public void setAbstractElement(@PluginElement final AbstractAppender abstractAppender) {}
+
+        /**
+         * An element with an annotated type.
+         */
+        public void setNestedAppender(final @PluginElement Appender nestedAppender) {}
+
+        @Override
+        public MyAppender build() {
+            return null;
+        }
+    }
+
+    @PluginFactory
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+}
diff --git a/log4j-docgen/src/test/it/example/MyEnum.java b/log4j-docgen/src/test/it/example/MyEnum.java
new file mode 100644
index 0000000..4d7afa8
--- /dev/null
+++ b/log4j-docgen/src/test/it/example/MyEnum.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example;
+
+/**
+ * A very important enum.
+ */
+public enum MyEnum {
+    /**
+     * Makes things go boom!
+     */
+    A,
+    /**
+     * A second choice.
+     */
+    B,
+    /**
+     * Value C.
+     */
+    C,
+    /**
+     * Value D.
+     */
+    D;
+}
diff --git a/log4j-docgen/src/test/it/example/MyOldLayout.java b/log4j-docgen/src/test/it/example/MyOldLayout.java
new file mode 100644
index 0000000..9a6b7c3
--- /dev/null
+++ b/log4j-docgen/src/test/it/example/MyOldLayout.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example;
+
+import org.apache.logging.log4j.plugins.Factory;
+import org.apache.logging.log4j.plugins.Plugin;
+import org.apache.logging.log4j.plugins.PluginAttribute;
+import org.apache.logging.log4j.plugins.PluginElement;
+
+/**
+ * Example plugin without a builder.
+ */
+@Plugin
+public final class MyOldLayout implements Layout {
+
+    /**
+     * @param boolAttr A {@code boolean} attribute.
+     * @param byteAttr A {@code byte} attribute.
+     * @param charAttr A {@code char} attribute.
+     * @param doubleAttr A {@code double} attribute.
+     * @param floatAttr A {@code float} attribute.
+     * @param intAttr An {@code int} attribute.
+     * @param longAttr A {@code long} attribute.
+     * @param shortAttr A {@code short} attribute.
+     * @param stringAttr A {@link String} attribute.
+     * @param origName An attribute with overwritten name.
+     * @param enumAttr An {@code enum} attribute.
+     * @param nestedLayout An element with multiplicity {@code 1}.
+     * @param filters An element with multiplicity {@code n}.
+     */
+    @Factory
+    public static MyOldLayout newLayout(
+            final @PluginAttribute(defaultBoolean = false) boolean boolAttr,
+            final @PluginAttribute(defaultByte = 'L') byte byteAttr,
+            final @PluginAttribute(defaultChar = 'L') char charAttr,
+            final @PluginAttribute(defaultDouble = 42.0) double doubleAttr,
+            final @PluginAttribute(defaultFloat = 42.0f) float floatAttr,
+            final @PluginAttribute(defaultInt = 424242) int intAttr,
+            final @PluginAttribute(defaultLong = 42424242L) long longAttr,
+            final @PluginAttribute(defaultShort = 4242) short shortAttr,
+            final @PluginAttribute String stringAttr,
+            final @PluginAttribute("otherName") String origName,
+            final @PluginAttribute MyEnum enumAttr,
+            final @PluginElement Layout nestedLayout,
+            final @PluginElement Filter[] filters) {
+        return null;
+    }
+}
diff --git a/log4j-docgen/src/test/java/org/apache/logging/log4j/docgen/processor/DocGenProcessorTest.java b/log4j-docgen/src/test/java/org/apache/logging/log4j/docgen/processor/DocGenProcessorTest.java
new file mode 100644
index 0000000..61f0ae2
--- /dev/null
+++ b/log4j-docgen/src/test/java/org/apache/logging/log4j/docgen/processor/DocGenProcessorTest.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.docgen.processor;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+import java.io.IOException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.stream.Stream;
+import javax.tools.Diagnostic;
+import javax.tools.DiagnosticCollector;
+import javax.tools.JavaCompiler;
+import javax.tools.JavaCompiler.CompilationTask;
+import javax.tools.JavaFileObject;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.StandardLocation;
+import javax.tools.ToolProvider;
+import org.apache.logging.log4j.docgen.xsd.SchemaGenerator;
+import org.junit.jupiter.api.Test;
+import org.xmlunit.assertj3.XmlAssert;
+
+public class DocGenProcessorTest {
+
+    @Test
+    void descriptorGenerationSucceeds() {
+        final Path basePath = Paths.get(System.getProperty("basedir", "."));
+        final Path schema = basePath.resolve("target/generated-site/resources/xsd/plugins-0.1.0.xsd");
+        final URL expected = SchemaGenerator.class.getResource("/expected/processor/META-INF/log4j/plugins.xml");
+        final Path actual = assertDoesNotThrow(DocGenProcessorTest::generateDescriptor);
+        XmlAssert.assertThat(actual)
+                .isValidAgainst(schema)
+                .and(expected)
+                .ignoreComments()
+                .ignoreWhitespace()
+                .areIdentical();
+    }
+
+    private static Path generateDescriptor() throws IOException {
+        final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+        final DiagnosticCollector<JavaFileObject> ds = new DiagnosticCollector<>();
+        final StandardJavaFileManager fileManager =
+                compiler.getStandardFileManager(null, Locale.ROOT, StandardCharsets.UTF_8);
+
+        final Path basePath = Paths.get(System.getProperty("basedir", "."));
+        final Path sourcePath = basePath.resolve("src/test/it");
+        final Iterable<? extends JavaFileObject> sources;
+        try (final Stream<Path> files = Files.walk(sourcePath)) {
+            sources = fileManager.getJavaFileObjectsFromPaths(
+                    files.filter(Files::isRegularFile).toList());
+        }
+
+        final Path destPath = basePath.resolve("target/test-site/processor");
+        Files.createDirectories(destPath);
+        fileManager.setLocationFromPaths(StandardLocation.CLASS_OUTPUT, Set.of(destPath));
+
+        final CompilationTask task = compiler.getTask(
+                null,
+                fileManager,
+                ds,
+                Arrays.asList("-proc:only", "-processor", DocGenProcessor.class.getName()),
+                null,
+                sources);
+        task.call();
+
+        final List<String> warnings = ds.getDiagnostics().stream()
+                .filter(d -> d.getKind() != Diagnostic.Kind.NOTE)
+                .map(d -> d.getMessage(Locale.ROOT))
+                .toList();
+        assertThat(warnings).isEmpty();
+
+        final Path descriptor = destPath.resolve("META-INF/log4j/plugins.xml");
+        assertThat(descriptor).isNotEmptyFile();
+        return descriptor;
+    }
+}
diff --git a/log4j-docgen/src/test/resources/expected/processor/META-INF/log4j/plugins.xml b/log4j-docgen/src/test/resources/expected/processor/META-INF/log4j/plugins.xml
new file mode 100644
index 0000000..7b4578f
--- /dev/null
+++ b/log4j-docgen/src/test/resources/expected/processor/META-INF/log4j/plugins.xml
@@ -0,0 +1,173 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to you under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License.  You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<pluginSet xmlns="https://logging.apache.org/xml/ns/plugins" version="0.1.0">
+    <scalars>
+        <scalar className="example.MyEnum">
+            <values>
+                <value name="A">
+                    <description>Makes things go boom!</description>
+                </value>
+                <value name="B">
+                    <description>A second choice.</description>
+                </value>
+                <value name="C">
+                    <description>Value C.</description>
+                </value>
+                <value name="D">
+                    <description>Value D.</description>
+                </value>
+            </values>
+            <description>A very important enum.</description>
+        </scalar>
+    </scalars>
+    <plugins>
+        <plugin name="MyAppender" namespace="namespace" className="example.MyAppender">
+            <supertypes>
+                <supertype>example.AbstractAppender</supertype>
+                <supertype>example.Appender</supertype>
+                <supertype>example.BaseAppender</supertype>
+                <supertype>java.lang.Object</supertype>
+            </supertypes>
+            <attributes>
+                <attribute name="aDouble" type="double">
+                    <description>An attribute of type `double`.</description>
+                </attribute>
+                <attribute name="anotherName">
+                    <description>An attribute whose name differs from the field name.</description>
+                </attribute>
+                <attribute name="booleanAtt" type="boolean">
+                    <description>A `boolean` attribute with annotated type.</description>
+                </attribute>
+                <attribute name="byteAtt" type="byte">
+                    <description>A `byte` attribute with annotated parameter.</description>
+                </attribute>
+                <attribute name="charAtt" type="char">
+                    <description>A `char` attribute.</description>
+                </attribute>
+                <attribute name="enumAtt" type="example.MyEnum">
+                    <description>An attribute that is an enumeration annotated on type.</description>
+                </attribute>
+                <attribute name="floatAtt" type="float">
+                    <description>An attribute of type `float`.</description>
+                </attribute>
+                <attribute name="intAtt" type="int">
+                    <description>An `int` attribute.</description>
+                </attribute>
+                <attribute name="longAtt" type="long">
+                    <description>A `long` attribute annotated on type.</description>
+                </attribute>
+                <attribute name="shortAtt" type="short">
+                    <description>A `short` attribute annotated on type.</description>
+                </attribute>
+                <attribute name="stringAtt">
+                    <description>A `String` attribute.</description>
+                </attribute>
+            </attributes>
+            <elements>
+                <element type="example.AbstractAppender">
+                    <description>An element that is not an interface with annotated parameter.</description>
+                </element>
+                <element type="example.Appender">
+                    <description>An element with an annotated type.</description>
+                </element>
+                <element multiplicity="*" type="example.Filter">
+                    <description>An element with multiplicity n with annotated setter.</description>
+                </element>
+                <element type="example.Layout">
+                    <description>An element with multiplicity 1.</description>
+                </element>
+            </elements>
+            <description>Example plugin
+
+This is an example plugin.
+It has the following characteristics:
+
+. Plugin name: `MyPlugin`,
+. Namespace: default (i.e. `Core`).
+
+It also implements:
+
+* xref:Appender.adoc[],
+* xref:BaseAppender.adoc[]</description>
+        </plugin>
+        <plugin name="MyOldLayout" className="example.MyOldLayout">
+            <supertypes>
+                <supertype>example.Layout</supertype>
+                <supertype>java.lang.Object</supertype>
+            </supertypes>
+            <attributes>
+                <attribute name="boolAttr" type="boolean">
+                    <description>A `boolean` attribute.</description>
+                </attribute>
+                <attribute name="byteAttr" type="byte">
+                    <description>A `byte` attribute.</description>
+                </attribute>
+                <attribute name="charAttr" type="char">
+                    <description>A `char` attribute.</description>
+                </attribute>
+                <attribute name="doubleAttr" type="double">
+                    <description>A `double` attribute.</description>
+                </attribute>
+                <attribute name="enumAttr" type="example.MyEnum">
+                    <description>An `enum` attribute.</description>
+                </attribute>
+                <attribute name="floatAttr" type="float">
+                    <description>A `float` attribute.</description>
+                </attribute>
+                <attribute name="intAttr" type="int">
+                    <description>An `int` attribute.</description>
+                </attribute>
+                <attribute name="longAttr" type="long">
+                    <description>A `long` attribute.</description>
+                </attribute>
+                <attribute name="otherName">
+                    <description>An attribute with overwritten name.</description>
+                </attribute>
+                <attribute name="shortAttr" type="short">
+                    <description>A `short` attribute.</description>
+                </attribute>
+                <attribute name="stringAttr">
+                    <description>A xref:String.adoc[] attribute.</description>
+                </attribute>
+            </attributes>
+            <elements>
+                <element multiplicity="*" type="example.Filter">
+                    <description>An element with multiplicity `n`.</description>
+                </element>
+                <element type="example.Layout">
+                    <description>An element with multiplicity `1`.</description>
+                </element>
+            </elements>
+            <description>Example plugin without a builder.</description>
+        </plugin>
+    </plugins>
+    <abstractTypes>
+        <abstractType className="example.AbstractAppender">
+            <description>An example of base abstract class.</description>
+        </abstractType>
+        <abstractType className="example.Appender">
+            <description>Extended interface that also allows to do `baz`.</description>
+        </abstractType>
+        <abstractType className="example.BaseAppender">
+            <description>Base interface for appenders.</description>
+        </abstractType>
+        <abstractType className="example.Layout">
+            <description>Formats messages.</description>
+        </abstractType>
+    </abstractTypes>
+</pluginSet>