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:35 UTC
(logging-log4j-tools) 01/01: Add annotation processor to generate documentation
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>