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

org.testifyproject.bytebuddy.implementation.attribute.AnnotationAppender Maven / Gradle / Ivy

The newest version!
package org.testifyproject.bytebuddy.implementation.attribute;

import lombok.EqualsAndHashCode;
import org.testifyproject.bytebuddy.description.annotation.AnnotationDescription;
import org.testifyproject.bytebuddy.description.enumeration.EnumerationDescription;
import org.testifyproject.bytebuddy.description.method.MethodDescription;
import org.testifyproject.bytebuddy.description.type.TypeDescription;
import org.testifyproject.bytebuddy.description.type.TypeList;
import org.testifyproject.bytebuddy.jar.asm.*;

import java.lang.reflect.Array;
import java.util.List;

/**
 * Annotation appenders are capable of writing annotations to a specified target.
 */
public interface AnnotationAppender {

    /**
     * A constant for informing ASM over ignoring a given name.
     */
    String NO_NAME = null;

    /**
     * Writes the given annotation to the target that this appender represents.
     *
     * @param annotationDescription The annotation to be written.
     * @param annotationValueFilter The annotation value filter to use.
     * @return Usually {@code this} or any other annotation appender capable of writing another annotation to the specified target.
     */
    AnnotationAppender append(AnnotationDescription annotationDescription, AnnotationValueFilter annotationValueFilter);

    /**
     * Writes the given type annotation to the target that this appender represents.
     *
     * @param annotationDescription The annotation to be written.
     * @param annotationValueFilter The annotation value filter to use.
     * @param typeReference         The type variable's type reference.
     * @param typePath              The type variable's type path.
     * @return Usually {@code this} or any other annotation appender capable of writing another annotation to the specified target.
     */
    AnnotationAppender append(AnnotationDescription annotationDescription, AnnotationValueFilter annotationValueFilter, int typeReference, String typePath);

    /**
     * Represents a target for an annotation writing process.
     */
    interface Target {

        /**
         * Creates an annotation visitor for writing the specified annotation.
         *
         * @param annotationTypeDescriptor The type descriptor for the annotation to be written.
         * @param visible                  {@code true} if the annotation is to be visible at runtime.
         * @return An annotation visitor for consuming the specified annotation.
         */
        AnnotationVisitor visit(String annotationTypeDescriptor, boolean visible);

        /**
         * Creates an annotation visitor for writing the specified type annotation.
         *
         * @param annotationTypeDescriptor The type descriptor for the annotation to be written.
         * @param visible                  {@code true} if the annotation is to be visible at runtime.
         * @param typeReference            The type annotation's type reference.
         * @param typePath                 The type annotation's type path.
         * @return An annotation visitor for consuming the specified annotation.
         */
        AnnotationVisitor visit(String annotationTypeDescriptor, boolean visible, int typeReference, String typePath);

        /**
         * Target for an annotation that is written to a Java type.
         */
        @EqualsAndHashCode
        class OnType implements Target {

            /**
             * The class visitor to write the annotation to.
             */
            private final ClassVisitor classVisitor;

            /**
             * Creates a new wrapper for a Java type.
             *
             * @param classVisitor The ASM class visitor to which the annotations are appended to.
             */
            public OnType(ClassVisitor classVisitor) {
                this.classVisitor = classVisitor;
            }

            @Override
            public AnnotationVisitor visit(String annotationTypeDescriptor, boolean visible) {
                return classVisitor.visitAnnotation(annotationTypeDescriptor, visible);
            }

            @Override
            public AnnotationVisitor visit(String annotationTypeDescriptor, boolean visible, int typeReference, String typePath) {
                return classVisitor.visitTypeAnnotation(typeReference, TypePath.fromString(typePath), annotationTypeDescriptor, visible);
            }
        }

        /**
         * Target for an annotation that is written to a Java method or constructor.
         */
        @EqualsAndHashCode
        class OnMethod implements Target {

            /**
             * The method visitor to write the annotation to.
             */
            private final MethodVisitor methodVisitor;

            /**
             * Creates a new wrapper for a Java method or constructor.
             *
             * @param methodVisitor The ASM method visitor to which the annotations are appended to.
             */
            public OnMethod(MethodVisitor methodVisitor) {
                this.methodVisitor = methodVisitor;
            }

            @Override
            public AnnotationVisitor visit(String annotationTypeDescriptor, boolean visible) {
                return methodVisitor.visitAnnotation(annotationTypeDescriptor, visible);
            }

            @Override
            public AnnotationVisitor visit(String annotationTypeDescriptor, boolean visible, int typeReference, String typePath) {
                return methodVisitor.visitTypeAnnotation(typeReference, TypePath.fromString(typePath), annotationTypeDescriptor, visible);
            }
        }

        /**
         * Target for an annotation that is written to a Java method or constructor parameter.
         */
        @EqualsAndHashCode
        class OnMethodParameter implements Target {

            /**
             * The method visitor to write the annotation to.
             */
            private final MethodVisitor methodVisitor;

            /**
             * The method parameter index to write the annotation to.
             */
            private final int parameterIndex;

            /**
             * Creates a new wrapper for a Java method or constructor.
             *
             * @param methodVisitor  The ASM method visitor to which the annotations are appended to.
             * @param parameterIndex The index of the method parameter.
             */
            public OnMethodParameter(MethodVisitor methodVisitor, int parameterIndex) {
                this.methodVisitor = methodVisitor;
                this.parameterIndex = parameterIndex;
            }

            @Override
            public AnnotationVisitor visit(String annotationTypeDescriptor, boolean visible) {
                return methodVisitor.visitParameterAnnotation(parameterIndex, annotationTypeDescriptor, visible);
            }

            @Override
            public AnnotationVisitor visit(String annotationTypeDescriptor, boolean visible, int typeReference, String typePath) {
                return methodVisitor.visitTypeAnnotation(typeReference, TypePath.fromString(typePath), annotationTypeDescriptor, visible);
            }
        }

        /**
         * Target for an annotation that is written to a Java field.
         */
        @EqualsAndHashCode
        class OnField implements Target {

            /**
             * The field visitor to write the annotation to.
             */
            private final FieldVisitor fieldVisitor;

            /**
             * Creates a new wrapper for a Java field.
             *
             * @param fieldVisitor The ASM field visitor to which the annotations are appended to.
             */
            public OnField(FieldVisitor fieldVisitor) {
                this.fieldVisitor = fieldVisitor;
            }

            @Override
            public AnnotationVisitor visit(String annotationTypeDescriptor, boolean visible) {
                return fieldVisitor.visitAnnotation(annotationTypeDescriptor, visible);
            }

            @Override
            public AnnotationVisitor visit(String annotationTypeDescriptor, boolean visible, int typeReference, String typePath) {
                return fieldVisitor.visitTypeAnnotation(typeReference, TypePath.fromString(typePath), annotationTypeDescriptor, visible);
            }
        }
    }

    /**
     * A default implementation for an annotation appender that writes annotations to a given byte consumer
     * represented by an ASM {@link org.testifyproject.bytebuddy.jar.asm.AnnotationVisitor}.
     */
    @EqualsAndHashCode
    class Default implements AnnotationAppender {

        /**
         * The target onto which an annotation write process is to be applied.
         */
        private final Target target;

        /**
         * Creates a default annotation appender.
         *
         * @param target The target to which annotations are written to.
         */
        public Default(Target target) {
            this.target = target;
        }

        /**
         * Handles the writing of a single annotation to an annotation visitor.
         *
         * @param annotationVisitor     The annotation visitor the write process is to be applied on.
         * @param annotation            The annotation to be written.
         * @param annotationValueFilter The value filter to apply for discovering which values of an annotation should be written.
         */
        private static void handle(AnnotationVisitor annotationVisitor, AnnotationDescription annotation, AnnotationValueFilter annotationValueFilter) {
            for (MethodDescription.InDefinedShape methodDescription : annotation.getAnnotationType().getDeclaredMethods()) {
                if (annotationValueFilter.isRelevant(annotation, methodDescription)) {
                    apply(annotationVisitor, methodDescription.getReturnType().asErasure(), methodDescription.getName(), annotation.getValue(methodDescription).resolve());
                }
            }
            annotationVisitor.visitEnd();
        }

        /**
         * Performs the writing of a given annotation value to an annotation visitor.
         *
         * @param annotationVisitor The annotation visitor the write process is to be applied on.
         * @param valueType         The type of the annotation value.
         * @param name              The name of the annotation type.
         * @param value             The annotation's value.
         */
        public static void apply(AnnotationVisitor annotationVisitor, TypeDescription valueType, String name, Object value) {
            if (valueType.isArray()) { // The Android emulator reads annotation arrays as annotation types. Therefore, this check needs to come first.
                AnnotationVisitor arrayVisitor = annotationVisitor.visitArray(name);
                int length = Array.getLength(value);
                TypeDescription componentType = valueType.getComponentType();
                for (int index = 0; index < length; index++) {
                    apply(arrayVisitor, componentType, NO_NAME, Array.get(value, index));
                }
                arrayVisitor.visitEnd();
            } else if (valueType.isAnnotation()) {
                handle(annotationVisitor.visitAnnotation(name, valueType.getDescriptor()), (AnnotationDescription) value, AnnotationValueFilter.Default.APPEND_DEFAULTS);
            } else if (valueType.isEnum()) {
                annotationVisitor.visitEnum(name, valueType.getDescriptor(), ((EnumerationDescription) value).getValue());
            } else if (valueType.represents(Class.class)) {
                annotationVisitor.visit(name, Type.getType(((TypeDescription) value).getDescriptor()));
            } else {
                annotationVisitor.visit(name, value);
            }
        }

        @Override
        public AnnotationAppender append(AnnotationDescription annotationDescription, AnnotationValueFilter annotationValueFilter) {
            switch (annotationDescription.getRetention()) {
                case RUNTIME:
                    doAppend(annotationDescription, true, annotationValueFilter);
                    break;
                case CLASS:
                    doAppend(annotationDescription, false, annotationValueFilter);
                    break;
                case SOURCE:
                    break;
                default:
                    throw new IllegalStateException("Unexpected retention policy: " + annotationDescription.getRetention());
            }
            return this;
        }

        /**
         * Tries to append a given annotation by reflectively reading an annotation.
         *
         * @param annotation            The annotation to be written.
         * @param visible               {@code true} if this annotation should be treated as visible at runtime.
         * @param annotationValueFilter The annotation value filter to apply.
         */
        private void doAppend(AnnotationDescription annotation, boolean visible, AnnotationValueFilter annotationValueFilter) {
            handle(target.visit(annotation.getAnnotationType().getDescriptor(), visible), annotation, annotationValueFilter);
        }

        @Override
        public AnnotationAppender append(AnnotationDescription annotationDescription, AnnotationValueFilter annotationValueFilter, int typeReference, String typePath) {
            switch (annotationDescription.getRetention()) {
                case RUNTIME:
                    doAppend(annotationDescription, true, annotationValueFilter, typeReference, typePath);
                    break;
                case CLASS:
                    doAppend(annotationDescription, false, annotationValueFilter, typeReference, typePath);
                    break;
                case SOURCE:
                    break;
                default:
                    throw new IllegalStateException("Unexpected retention policy: " + annotationDescription.getRetention());
            }
            return this;
        }

        /**
         * Tries to append a given annotation by reflectively reading an annotation.
         *
         * @param annotation            The annotation to be written.
         * @param visible               {@code true} if this annotation should be treated as visible at runtime.
         * @param annotationValueFilter The annotation value filter to apply.
         * @param typeReference         The type annotation's type reference.
         * @param typePath              The type annotation's type path.
         */
        private void doAppend(AnnotationDescription annotation,
                              boolean visible,
                              AnnotationValueFilter annotationValueFilter,
                              int typeReference,
                              String typePath) {
            handle(target.visit(annotation.getAnnotationType().getDescriptor(), visible, typeReference, typePath), annotation, annotationValueFilter);
        }
    }

    /**
     * A type visitor that visits all type annotations of a generic type and writes any discovered annotation to a
     * supplied {@link AnnotationAppender}.
     */
    @EqualsAndHashCode
    class ForTypeAnnotations implements TypeDescription.Generic.Visitor {

        /**
         * Indicates that type variables type annotations are written on a Java type.
         */
        public static final boolean VARIABLE_ON_TYPE = true;

        /**
         * Indicates that type variables type annotations are written on a Java method or constructor.
         */
        public static final boolean VARIABLE_ON_INVOKEABLE = false;

        /**
         * Represents an empty type path.
         */
        private static final String EMPTY_TYPE_PATH = "";

        /**
         * Represents a step to a component type within a type path.
         */
        private static final char COMPONENT_TYPE_PATH = '[';

        /**
         * Represents a wildcard type step within a type path.
         */
        private static final char WILDCARD_TYPE_PATH = '*';

        /**
         * Represents a (reversed) type step to an inner class within a type path.
         */
        private static final char INNER_CLASS_PATH = '.';

        /**
         * Represents an index tzpe delimiter within a type path.
         */
        private static final char INDEXED_TYPE_DELIMITER = ';';

        /**
         * The index that indicates that super type type annotations are written onto a super class.
         */
        private static final int SUPER_CLASS_INDEX = -1;

        /**
         * The annotation appender to use.
         */
        private final AnnotationAppender annotationAppender;

        /**
         * The annotation value filter to use.
         */
        private final AnnotationValueFilter annotationValueFilter;

        /**
         * The type reference to use.
         */
        private final int typeReference;

        /**
         * The type path to use.
         */
        private final String typePath;

        /**
         * Creates a new type annotation appending visitor for an empty type path.
         *
         * @param annotationAppender    The annotation appender to use.
         * @param annotationValueFilter The annotation value filter to use.
         * @param typeReference         The type reference to use.
         */
        protected ForTypeAnnotations(AnnotationAppender annotationAppender, AnnotationValueFilter annotationValueFilter, TypeReference typeReference) {
            this(annotationAppender, annotationValueFilter, typeReference.getValue(), EMPTY_TYPE_PATH);
        }

        /**
         * Creates a new type annotation appending visitor.
         *
         * @param annotationAppender    The annotation appender to use.
         * @param annotationValueFilter The annotation value filter to use.
         * @param typeReference         The type reference to use.
         * @param typePath              The type path to use.
         */
        protected ForTypeAnnotations(AnnotationAppender annotationAppender, AnnotationValueFilter annotationValueFilter, int typeReference, String typePath) {
            this.annotationAppender = annotationAppender;
            this.annotationValueFilter = annotationValueFilter;
            this.typeReference = typeReference;
            this.typePath = typePath;
        }

        /**
         * Creates a type annotation appender for a type annotations of a super class type.
         *
         * @param annotationAppender    The annotation appender to write any type annotation to.
         * @param annotationValueFilter The annotation value filter to apply.
         * @return A visitor for appending type annotations of a super class.
         */
        public static TypeDescription.Generic.Visitor ofSuperClass(AnnotationAppender annotationAppender,
                                                                                       AnnotationValueFilter annotationValueFilter) {
            return new ForTypeAnnotations(annotationAppender, annotationValueFilter, TypeReference.newSuperTypeReference(SUPER_CLASS_INDEX));
        }

        /**
         * Creates a type annotation appender for type annotations of an interface type.
         *
         * @param annotationAppender    The annotation appender to write any type annotation to.
         * @param annotationValueFilter The annotation value filter to apply.
         * @param index                 The index of the interface type.
         * @return A visitor for appending type annotations of an interface type.
         */
        public static TypeDescription.Generic.Visitor ofInterfaceType(AnnotationAppender annotationAppender,
                                                                                          AnnotationValueFilter annotationValueFilter,
                                                                                          int index) {
            return new ForTypeAnnotations(annotationAppender, annotationValueFilter, TypeReference.newSuperTypeReference(index));
        }

        /**
         * Creates a type annotation appender for type annotations of a field's type.
         *
         * @param annotationAppender    The annotation appender to write any type annotation to.
         * @param annotationValueFilter The annotation value filter to apply.
         * @return A visitor for appending type annotations of a field's type.
         */
        public static TypeDescription.Generic.Visitor ofFieldType(AnnotationAppender annotationAppender,
                                                                                      AnnotationValueFilter annotationValueFilter) {
            return new ForTypeAnnotations(annotationAppender, annotationValueFilter, TypeReference.newTypeReference(TypeReference.FIELD));
        }

        /**
         * Creates a type annotation appender for type annotations of a method's return type.
         *
         * @param annotationAppender    The annotation appender to write any type annotation to.
         * @param annotationValueFilter The annotation value filter to apply.
         * @return A visitor for appending type annotations of a method's return type.
         */
        public static TypeDescription.Generic.Visitor ofMethodReturnType(AnnotationAppender annotationAppender,
                                                                                             AnnotationValueFilter annotationValueFilter) {
            return new ForTypeAnnotations(annotationAppender, annotationValueFilter, TypeReference.newTypeReference(TypeReference.METHOD_RETURN));
        }

        /**
         * Creates a type annotation appender for type annotations of a method's parameter type.
         *
         * @param annotationAppender    The annotation appender to write any type annotation to.
         * @param annotationValueFilter The annotation value filter to apply.
         * @param index                 The parameter index.
         * @return A visitor for appending type annotations of a method's parameter type.
         */
        public static TypeDescription.Generic.Visitor ofMethodParameterType(AnnotationAppender annotationAppender,
                                                                                                AnnotationValueFilter annotationValueFilter,
                                                                                                int index) {
            return new ForTypeAnnotations(annotationAppender, annotationValueFilter, TypeReference.newFormalParameterReference(index));
        }

        /**
         * Creates a type annotation appender for type annotations of a method's exception type.
         *
         * @param annotationAppender    The annotation appender to write any type annotation to.
         * @param annotationValueFilter The annotation value filter to apply.
         * @param index                 The exception type's index.
         * @return A visitor for appending type annotations of a method's exception type.
         */
        public static TypeDescription.Generic.Visitor ofExceptionType(AnnotationAppender annotationAppender,
                                                                                          AnnotationValueFilter annotationValueFilter,
                                                                                          int index) {
            return new ForTypeAnnotations(annotationAppender, annotationValueFilter, TypeReference.newExceptionReference(index));
        }

        /**
         * Creates a type annotation appender for type annotations of a method's receiver type.
         *
         * @param annotationAppender    The annotation appender to write any type annotation to.
         * @param annotationValueFilter The annotation value filter to apply.
         * @return A visitor for appending type annotations of a method's receiver type.
         */
        public static TypeDescription.Generic.Visitor ofReceiverType(AnnotationAppender annotationAppender,
                                                                                         AnnotationValueFilter annotationValueFilter) {
            return new ForTypeAnnotations(annotationAppender, annotationValueFilter, TypeReference.newTypeReference(TypeReference.METHOD_RECEIVER));
        }

        /**
         * Appends all supplied type variables to the supplied method appender.
         *
         * @param annotationAppender    The annotation appender to write any type annotation to.
         * @param annotationValueFilter The annotation value filter to apply.
         * @param variableOnType        {@code true} if the type variables are declared by a type, {@code false} if they are declared by a method.
         * @param typeVariables         The type variables to append.
         * @return The resulting annotation appender.
         */
        public static AnnotationAppender ofTypeVariable(AnnotationAppender annotationAppender,
                                                        AnnotationValueFilter annotationValueFilter,
                                                        boolean variableOnType,
                                                        List typeVariables) {
            return ofTypeVariable(annotationAppender, annotationValueFilter, variableOnType, 0, typeVariables);
        }

        /**
         * Appends all supplied type variables to the supplied method appender.
         *
         * @param annotationAppender    The annotation appender to write any type annotation to.
         * @param annotationValueFilter The annotation value filter to apply.
         * @param variableOnType        {@code true} if the type variables are declared by a type, {@code false} if they are declared by a method.
         * @param subListIndex          The index of the first type variable to append. All previous type variables are ignored.
         * @param typeVariables         The type variables to append.
         * @return The resulting annotation appender.
         */
        public static AnnotationAppender ofTypeVariable(AnnotationAppender annotationAppender,
                                                        AnnotationValueFilter annotationValueFilter,
                                                        boolean variableOnType,
                                                        int subListIndex,
                                                        List typeVariables) {
            int typeVariableIndex = subListIndex, variableBaseReference, variableBoundBaseBase;
            if (variableOnType) {
                variableBaseReference = TypeReference.CLASS_TYPE_PARAMETER;
                variableBoundBaseBase = TypeReference.CLASS_TYPE_PARAMETER_BOUND;
            } else {
                variableBaseReference = TypeReference.METHOD_TYPE_PARAMETER;
                variableBoundBaseBase = TypeReference.METHOD_TYPE_PARAMETER_BOUND;
            }
            for (TypeDescription.Generic typeVariable : typeVariables.subList(subListIndex, typeVariables.size())) {
                int typeReference = TypeReference.newTypeParameterReference(variableBaseReference, typeVariableIndex).getValue();
                for (AnnotationDescription annotationDescription : typeVariable.getDeclaredAnnotations()) {
                    annotationAppender = annotationAppender.append(annotationDescription, annotationValueFilter, typeReference, EMPTY_TYPE_PATH);
                }
                int boundIndex = !typeVariable.getUpperBounds().get(0).getSort().isTypeVariable() && typeVariable.getUpperBounds().get(0).isInterface()
                        ? 1
                        : 0;
                for (TypeDescription.Generic typeBound : typeVariable.getUpperBounds()) {
                    annotationAppender = typeBound.accept(new ForTypeAnnotations(annotationAppender,
                            annotationValueFilter,
                            TypeReference.newTypeParameterBoundReference(variableBoundBaseBase, typeVariableIndex, boundIndex++)));
                }
                typeVariableIndex++;
            }
            return annotationAppender;
        }

        @Override
        public AnnotationAppender onGenericArray(TypeDescription.Generic genericArray) {
            return genericArray.getComponentType().accept(new ForTypeAnnotations(apply(genericArray, typePath),
                    annotationValueFilter,
                    typeReference,
                    typePath + COMPONENT_TYPE_PATH));
        }

        @Override
        public AnnotationAppender onWildcard(TypeDescription.Generic wildcard) {
            TypeList.Generic lowerBounds = wildcard.getLowerBounds();
            return (lowerBounds.isEmpty()
                    ? wildcard.getUpperBounds().getOnly()
                    : lowerBounds.getOnly()).accept(new ForTypeAnnotations(apply(wildcard, typePath), annotationValueFilter, typeReference, typePath + WILDCARD_TYPE_PATH));
        }

        @Override
        public AnnotationAppender onParameterizedType(TypeDescription.Generic parameterizedType) {
            StringBuilder typePath = new StringBuilder(this.typePath);
            for (int index = 0; index < parameterizedType.asErasure().getSegmentCount(); index++) {
                typePath = typePath.append(INNER_CLASS_PATH);
            }
            AnnotationAppender annotationAppender = apply(parameterizedType, typePath.toString());
            TypeDescription.Generic ownerType = parameterizedType.getOwnerType();
            if (ownerType != null) {
                annotationAppender = ownerType.accept(new ForTypeAnnotations(annotationAppender,
                        annotationValueFilter,
                        typeReference,
                        this.typePath));
            }
            int index = 0;
            for (TypeDescription.Generic typeArgument : parameterizedType.getTypeArguments()) {
                annotationAppender = typeArgument.accept(new ForTypeAnnotations(annotationAppender,
                        annotationValueFilter,
                        typeReference,
                        typePath.toString() + index++ + INDEXED_TYPE_DELIMITER));
            }
            return annotationAppender;
        }

        @Override
        public AnnotationAppender onTypeVariable(TypeDescription.Generic typeVariable) {
            return apply(typeVariable, typePath);
        }

        @Override
        public AnnotationAppender onNonGenericType(TypeDescription.Generic typeDescription) {
            StringBuilder typePath = new StringBuilder(this.typePath);
            for (int index = 0; index < typeDescription.asErasure().getSegmentCount(); index++) {
                typePath = typePath.append(INNER_CLASS_PATH);
            }
            AnnotationAppender annotationAppender = apply(typeDescription, typePath.toString());
            if (typeDescription.isArray()) {
                annotationAppender = typeDescription.getComponentType().accept(new ForTypeAnnotations(annotationAppender,
                        annotationValueFilter,
                        typeReference,
                        this.typePath + COMPONENT_TYPE_PATH)); // Impossible to be inner class
            }
            return annotationAppender;
        }

        /**
         * Writes all annotations of the supplied type to this instance's annotation appender.
         *
         * @param typeDescription The type of what all annotations should be written of.
         * @param typePath        The type path to use.
         * @return The resulting annotation appender.
         */
        private AnnotationAppender apply(TypeDescription.Generic typeDescription, String typePath) {
            AnnotationAppender annotationAppender = this.annotationAppender;
            for (AnnotationDescription annotationDescription : typeDescription.getDeclaredAnnotations()) {
                annotationAppender = annotationAppender.append(annotationDescription, annotationValueFilter, typeReference, typePath);
            }
            return annotationAppender;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy