All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.debezium.operator.docs.AbstractDocsProcessor Maven / Gradle / Ivy

There is a newer version: 3.0.2.Final
Show newest version
/*
 * Copyright Debezium Authors.
 *
 * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
 */
package io.debezium.operator.docs;

import static io.debezium.operator.docs.Processing.annotation;
import static io.debezium.operator.docs.Processing.asDeclared;
import static io.debezium.operator.docs.Processing.asElement;
import static io.debezium.operator.docs.Processing.asEnum;
import static io.debezium.operator.docs.Processing.enclosedElements;
import static io.debezium.operator.docs.Processing.isAnnotated;
import static io.debezium.operator.docs.Processing.typeArguments;
import static java.util.function.Predicate.not;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.StringJoiner;
import java.util.stream.Stream;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.tools.StandardLocation;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;

import io.debezium.operator.docs.annotations.Documented;
import io.debezium.operator.docs.model.Documentation;
import io.debezium.operator.docs.model.Documentation.TypeDescription;
import io.debezium.operator.docs.model.Documentation.TypeDescriptionBuilder;
import io.debezium.operator.docs.output.DocumentationFormatter;

public abstract class AbstractDocsProcessor extends AbstractProcessor {
    private final Documentation documentation;
    private final String file;
    private final Set knownTypes;

    public AbstractDocsProcessor(String file, String title) {
        this.documentation = new Documentation(title);
        this.knownTypes = new HashSet<>();
        this.file = file;
    }

    @Override
    public Set getSupportedAnnotationTypes() {
        return Set.of(Documented.class.getCanonicalName());
    }

    @Override
    public boolean process(Set annotations, RoundEnvironment roundEnv) {
        for (TypeElement annotation : annotations) {
            var annotated = roundEnv.getElementsAnnotatedWith(annotation);
            var types = ElementFilter.typesIn(annotated);

            setKnownTypes(types);
            documentTypes(types);
            writeDocFile(file);
        }
        return false;
    }

    protected void writeDocFile(String fileName) {
        try {
            var content = formatter()
                    .formatted(documentation);
            var resource = processingEnv
                    .getFiler()
                    .createResource(StandardLocation.SOURCE_OUTPUT, "docs", fileName);

            try (PrintWriter out = new PrintWriter(resource.openWriter())) {
                out.println(content);
            }
        }
        catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    protected void documentTypes(Collection types) {
        types
                .stream()
                .filter(this::isNotHidden)
                .map(this::documentType)
                .forEach(documentation::addTypeDescription);
    }

    protected TypeDescription documentType(TypeElement element) {
        var name = name(element);
        var description = typeDescriptionBuilder(name);

        presentFields(element)
                .stream()
                .map(this::createFieldDocs)
                .forEach(description::addFieldDescription);

        additionalFields(element)
                .stream()
                .map(this::createFieldDocs)
                .forEach(description::addFieldDescription);

        return description.build();
    }

    protected List additionalFields(TypeElement element) {
        return documentedTypeInfo(element)
                .stream()
                .map(Documented::fields)
                .flatMap(Stream::of)
                .toList();
    }

    protected List presentFields(TypeElement element) {
        return enclosedElements(element, ElementKind.FIELD, VariableElement.class)
                .filter(this::isDocumentedField)
                .toList();
    }

    private void setKnownTypes(Collection types) {
        types.stream()
                .filter(this::isNotHidden)
                .map(this::name)
                .forEach(knownTypes::add);
    }

    /**
     * @param type type given as {@link TypeElement} instance
     * @return name of the type
     */
    protected String name(TypeElement type) {
        var fullName = String.valueOf(type.getSimpleName());
        return simpleName(fullName);
    }

    /**
     * @return name of the element
     */
    protected String name(Element element) {
        return String.valueOf(element.getSimpleName());
    }

    /**
     * For a fully qualified name, this method returns the part after the last dot
     *
     * @param fullName fully qualified name
     * @return simple name
     */
    protected String simpleName(String fullName) {
        var dot = fullName.lastIndexOf('.') + 1;
        return fullName.substring(dot);
    }

    protected Optional documentedFieldInfo(Element element) {
        return annotation(element, Documented.Field.class);
    }

    protected Optional jsonPropertyDescription(Element element) {
        return annotation(element, JsonPropertyDescription.class);
    }

    protected Optional jsonProperty(Element element) {
        return annotation(element, JsonProperty.class);
    }

    protected Optional documentedTypeInfo(TypeMirror type) {
        return asElement(type).flatMap(e -> annotation(e, Documented.class));
    }

    protected Optional documentedTypeInfo(TypeElement type) {
        return annotation(type, Documented.class);
    }

    protected boolean isDocumentedField(Element field) {
        return isAnnotated(field, Documented.Field.class)
                || isAnnotated(field, JsonProperty.class)
                || isAnnotated(field, JsonPropertyDescription.class);
    }

    protected boolean isNotHidden(TypeElement element) {
        return documentedTypeInfo(element)
                .map(Documented::hidden)
                .map(hidden -> !hidden)
                .orElse(true);
    }

    protected boolean isKnownType(String name) {
        return knownTypes.contains(name);
    }

    protected String fieldDefaultValue(Element element) {
        return Optional. empty()
                .or(() -> documentedFieldInfo(element)
                        .map(Documented.Field::defaultValue)
                        .filter(not(String::isEmpty)))
                .or(() -> jsonProperty(element)
                        .map(JsonProperty::defaultValue)
                        .filter(not(String::isEmpty)))
                .orElse("");
    }

    /**
     * Gets descriptions from  {@link JsonPropertyDescription}
     *
     * @param element annotated element
     * @return field description
     */
    protected String fieldDescription(Element element) {
        return Optional. empty()
                .or(() -> documentedFieldInfo(element)
                        .map(Documented.Field::description)
                        .filter(not(String::isEmpty)))
                .or(() -> jsonPropertyDescription(element)
                        .map(JsonPropertyDescription::value)
                        .filter(not(String::isEmpty)))
                .orElse("");
    }

    /**
     * @param field variable field
     * @return type with generic arguments
     */
    protected String fieldType(VariableElement field) {
        var type = field.asType();
        return Optional. empty()
                .or(() -> explicitFieldTypeName(field))
                .or(() -> explicitTypeName(type))
                .or(() -> enumTypeName(type))
                .or(() -> declaredTypeName(type))
                .orElseGet(() -> typeName(type));
    }

    /**
     * Extracts type documented by {@link Documented.Field}
     *
     * @param field scanned field
     * @return documented type or empty
     */
    protected Optional explicitFieldTypeName(VariableElement field) {
        return documentedFieldInfo(field)
                .map(Documented.Field::type)
                .filter(not(String::isEmpty));
    }

    protected Optional explicitTypeName(TypeMirror type) {
        return documentedTypeInfo(type)
                .map(Documented::name)
                .filter(not(String::isEmpty));
    }

    protected Optional enumTypeName(TypeMirror type) {
        return asEnum(type)
                .map(this::enumConstantNames)
                .map(names -> String.join(",", names));
    }

    protected Optional declaredTypeName(TypeMirror mirror) {
        return asDeclared(mirror).map(this::genericTypeName);
    }

    protected String typeName(TypeMirror type) {
        var fullName = type.toString();
        return simpleName(fullName);
    }

    protected String genericTypeName(DeclaredType type) {
        var erasure = processingEnv.getTypeUtils().erasure(type);
        var typeArgs = new StringJoiner(", ", "<", ">").setEmptyValue("");

        typeArguments(type)
                .map(this::typeName)
                .forEach(typeArgs::add);

        return typeName(erasure) + typeArgs;
    }

    protected List enumConstantNames(TypeElement type) {
        return enclosedElements(type, ElementKind.ENUM_CONSTANT)
                .map(this::name)
                .map(String::toLowerCase)
                .toList();
    }

    /**
     * Extracts a type reference from a field's type.

     * @param field variable element
     * @return name of documented type
     */
    protected String fieldTypeReference(VariableElement field) {
        var type = field.asType();

        return Optional. empty()
                .or(() -> typeErasureReference(type))
                .or(() -> typeArgumentReference(type))
                .orElse(null);
    }

    /**
     * Extracts external type reference URL  from {@link Documented.Field} present on a field
     *
     * @return reference url
     */
    protected String fieldExternalTypeReference(VariableElement field) {
        return documentedFieldInfo(field)
                .map(this::fieldExternalTypeReference)
                .orElse(null);
    }

    protected String fieldExternalTypeReference(Documented.Field field) {
        return Optional. empty()
                .or(() -> k8TypeReference(field.k8Ref()))
                .orElse(null);
    }

    protected Optional k8TypeReference(String slug) {
        if (slug.isEmpty()) {
            return Optional.empty();
        }
        return Optional.of(Documented.K8_API_DOCS_ADDR + "#" + slug);
    }

    protected Optional typeErasureReference(TypeMirror type) {
        var erasure = processingEnv.getTypeUtils().erasure(type);

        return asDeclared(erasure)
                .map(this::typeName)
                .filter(this::isKnownType);
    }

    protected Optional typeArgumentReference(TypeMirror type) {
        return asDeclared(type)
                .map(t -> typeArguments(t, this::typeName))
                .map(s -> s.filter(this::isKnownType))
                .flatMap(Stream::findFirst);
    }

    /**
     * Creates an appropriate instance of type description builder
     *
     * @param name type name
     * @return type description builder
     */
    protected TypeDescriptionBuilder typeDescriptionBuilder(String name) {
        return new TypeDescriptionBuilder(name);
    }

    /**
     * Called for each field documented by {@link Documented.Field}
     *
     * @param field field as {@link Documented.Field}
     * @return field description
     */
    protected Documentation.FieldDescription createFieldDocs(Documented.Field field) {
        return new Documentation.FieldDescription(
                field.name(),
                field.type(),
                field.type(),
                fieldExternalTypeReference(field),
                field.defaultValue(),
                field.description());
    }

    /**
     * Called for each field documented by {@link JsonPropertyDescription}
     *
     * @param field field as {@link VariableElement}
     * @return filed description
     */
    protected Documentation.FieldDescription createFieldDocs(VariableElement field) {
        return new Documentation.FieldDescription(
                name(field),
                fieldType(field),
                fieldTypeReference(field),
                fieldExternalTypeReference(field),
                fieldDefaultValue(field),
                fieldDescription(field));
    }

    /**
     * Creates an appropriate instance of documentation formatter
     * @return documentation formatter
     */
    protected abstract DocumentationFormatter formatter();

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy