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

fish.payara.microprofile.metrics.cdi.AnnotationReader Maven / Gradle / Ivy

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2020-2023 Payara Foundation and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://github.com/payara/Payara/blob/main/LICENSE.txt
 * See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * The Payara Foundation designates this particular file as subject to the "Classpath"
 * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */
package fish.payara.microprofile.metrics.cdi;

import jakarta.enterprise.inject.Stereotype;
import jakarta.enterprise.inject.spi.Annotated;
import jakarta.enterprise.inject.spi.AnnotatedMember;
import jakarta.enterprise.inject.spi.AnnotatedParameter;
import jakarta.enterprise.inject.spi.InjectionPoint;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import org.eclipse.microprofile.metrics.Metadata;
import org.eclipse.microprofile.metrics.MetricID;
import org.eclipse.microprofile.metrics.MetricRegistry;
import org.eclipse.microprofile.metrics.MetricUnits;
import org.eclipse.microprofile.metrics.Tag;
import org.eclipse.microprofile.metrics.annotation.Counted;
import org.eclipse.microprofile.metrics.annotation.Gauge;
import org.eclipse.microprofile.metrics.annotation.Metric;
import org.eclipse.microprofile.metrics.annotation.Timed;

import static java.util.Arrays.asList;

/**
 * Utility that allows reading the different MP metrics {@link Annotation}s from different annotated abstractions
 * providing a common interface to allow generic handling and a common logic independent of the source of the
 * {@link Annotation}.
 *
 * Supported are:
 * 
    *
  • {@link AnnotatedElement}
  • *
  • {@link Annotated}
  • *
  • {@link InjectionPoint}
  • *
* * It is important to realise that {@link Annotated} and {@link InjectionPoint} have to be used as a source when * available as they allow to add or remove {@link Annotation} effectively acting as a runtime override of the compiled * information provided by {@link AnnotatedElement}. * * This utility also encodes most of the logic as defined by the MP Metrics specification. This includes the logic of * which annotation applied and how the metrics effective name if computed from annotation values and the annotated * element. For this reason the methods are documented in great detail. * * @author Jan Bernitt * @since 5.202 * * @param Type of the MP metrics annotation */ public final class AnnotationReader { /** * Get {@link AnnotationReader} for a provided {@link Annotation}. * * @param annotationType * @return The {@link AnnotationReader} for the provided {@link Annotation} type * @throws IllegalAccessException In case no such reader exists */ @SuppressWarnings("unchecked") public static AnnotationReader forAnnotation(Class annotationType) { AnnotationReader reader = READERS_BY_ANNOTATION.get(annotationType); if (reader == null) { throw new IllegalArgumentException("Unsupported Metrics [" + annotationType.getName() + "]"); } return (AnnotationReader) reader; } /** * @return all available {@link AnnotationReader}s */ public static Iterable> readers() { return READERS_BY_ANNOTATION.values(); } private static final Map, AnnotationReader> READERS_BY_ANNOTATION = new HashMap<>(); public static final AnnotationReader COUNTED = new AnnotationReader<>( Counted.class, Counted.class.getName(), Counted::name, Counted::tags, Counted::description, Counted::absolute, Counted::unit, Counted::scope); public static final AnnotationReader GAUGE = new AnnotationReader<>( Gauge.class, Gauge.class.getName(), Gauge::name, Gauge::tags, Gauge::description, Gauge::absolute, Gauge::unit, Gauge::scope); public static final AnnotationReader TIMED = new AnnotationReader<>( Timed.class, Timed.class.getName(), Timed::name, Timed::tags, Timed::description, Timed::absolute, Timed::unit, Timed::scope); public static final AnnotationReader METRIC = new AnnotationReader<>( Metric.class, Metric.class.getName(), Metric::name, Metric::tags, Metric::description, Metric::absolute, Metric::unit, Metric::scope); private static void register(AnnotationReader reader) { READERS_BY_ANNOTATION.put(reader.annotationType(), reader); } static { register(COUNTED); register(GAUGE); register(TIMED); register(METRIC); } private final Class annotationType; private final String nameType; private final Function name; private final Function tags; private final Function description; private final Predicate absolute; private final Function unit; private final Function scope; private AnnotationReader(Class annotationType, String nameType, Function name, Function tags, Function description, Predicate absolute, Function unit, Function scope) { this.annotationType = annotationType; this.nameType = nameType; this.name = name; this.tags = tags; this.description = description; this.absolute = absolute; this.unit = unit; this.scope = scope; } public Class annotationType() { return annotationType; } /** * If this {@link AnnotationReader} reads {@link Metric} {@link Annotation} it can be associated with different * metric class name type using the {@link AnnotationReader}. * * @param nameType String representing the type class of the metric * @return A new {@link AnnotationReader} using the provided * @throws IllegalStateException In case this method is called on {@link AnnotationReader} that is not reading * {@link Metric} {@link Annotation}. */ public AnnotationReader asType(String nameType) { if (this.annotationType != Metric.class) { throw new IllegalStateException("Only Metric reader can be typed!"); } return new AnnotationReader<>(annotationType, nameType, name, tags, description, absolute, unit, scope); } /** * Infers the {@link org.eclipse.microprofile.metrics.Metric} {@link Class}, from the provided generic Type * * @param genericType the actual type of the {@link org.eclipse.microprofile.metrics.Metric} as declared by a * {@link Member} or {@link Parameter}. * @return A new {@link AnnotationReader} which uses the inferred * @throws IllegalArgumentException in case the given type does not implement any of the known metric types. */ private AnnotationReader asType(Type genericType) { Class type = (Class) (genericType instanceof Class ? genericType : ((java.lang.reflect.ParameterizedType) genericType).getRawType()); return asType(type.getName()); } private AnnotationReader asAutoType(Type genericType) { return annotationType == Metric.class ? asType(genericType) : this; } /** * Returns the effective annotation for the provided bean and element. * * @param bean type of the bean that declared the provided element * @param element a {@link AnnotatedElement} possibly annotated with this {@link AnnotationReader}'s * {@link #annotationType()} * @return the effective {@link Annotation}, or {@code null}. The element's annotations take precedence over the * bean's annotations. */ public T annotation(Class bean, E element) { try { return compute(bean, element, Function.identity(), Function.identity()); } catch (IllegalArgumentException ex) { return null; } } /** * Reads the effective {@link Annotation} for the provided {@link InjectionPoint}. * * @param point source {@link InjectionPoint} for an annotated element having this {@link AnnotationReader}'s * {@link #annotationType()}, not {@code null} * @return the effective annotation for the provided {@link InjectionPoint}, never {@code null} * @throws IllegalArgumentException In case the provided {@link InjectionPoint} isn't effectively annotated with * this {@link AnnotationReader}'s {@link Annotation}. */ public T annotation(InjectionPoint point) { return compute(point, (annotation, name) -> annotation); } /** * Checks if this {@link AnnotationReader}'s {@link #annotationType()} is present either at the provided * {@link AnnotatedElement} or the provided bean {@link Class}. * * @param bean type of the bean that declared the provided element * @param element a {@link AnnotatedElement} possibly annotated with this {@link AnnotationReader}'s * {@link #annotationType()} * @return true, if provided element or bean are annotated with this {@link AnnotationReader}'s * {@link #annotationType()}, else false. */ public boolean isPresent(Class bean, E element) { return nameType == GAUGE.nameType ? element instanceof Method && element.isAnnotationPresent(annotationType) : annotation(bean, element) != null; } /** * Returns the metric name as defined by the provided {@link Annotation} * * @param annotation source annotation to read, not {@code null} * @return name value of the provided source annotation */ public String name(T annotation) { return name.apply(annotation); } /** * Returns the metric name as defined by the MP specification for the annotation situation at hand for the provided * {@link InjectionPoint}. This does take into account that annotations might have been added or removed at runtime. * * @param point source {@link InjectionPoint} for an annotated element having this {@link AnnotationReader}'s * {@link #annotationType()}, not {@code null} * @return full metric name as required by the MP specification * @throws IllegalArgumentException In case the provided {@link InjectionPoint} isn't effectively annotated with * this {@link AnnotationReader}'s {@link Annotation}. */ public String name(InjectionPoint point) { return compute(point, this::name); } /** * Returns the metric name as defined by the MP specification for the annotation situation at hand for the provided * {@link AnnotatedMember}. This does take into account that annotations might have been added or removed at runtime. * * @param member source {@link AnnotatedMember} for an annotated element having this {@link AnnotationReader}'s * {@link #annotationType()}, not {@code null} * @return full metric name as required by the MP specification * @throws IllegalArgumentException In case the provided {@link AnnotatedMember} isn't effectively annotated with * this {@link AnnotationReader}'s {@link Annotation}. */ public String name(AnnotatedMember member) { return compute(member, this::name); } /** * Reads the effective name for the provided bean {@link Class} and {@link AnnotatedElement}. Either bean or element * must have this {@link AnnotationReader}'s {@link Annotation}. * * @param bean type of the bean that declared the provided element possibly annotated with this * {@link AnnotationReader}'s {@link #annotationType()} * @param element a {@link AnnotatedElement} possibly annotated with this {@link AnnotationReader}'s * {@link #annotationType()} * @return full metric name as required by the MP specification * @throws IllegalArgumentException In case neither the {@link AnnotatedElement} or the {@link Class} isn't * annotated with this {@link AnnotationReader}'s {@link Annotation}. */ public String name(Class bean, E element) { return compute(bean, element, this::name); } private String name(@SuppressWarnings("unused") T annotation, String name) { return name; // used as method-ref-lambda } /** * Returns the metric tags as defined by the provided {@link Annotation} * * @param annotation source annotation to read, not {@code null} * @return tags value of the provided source annotation */ public Tag[] tags(T annotation) { return tagsFromString(tags.apply(annotation)); } /** * Returns the {@link MetricID} as defined by the provided {@link Annotation}'s name and tags attributes. * * @param annotation source annotation to read, not {@code null} * @return {@link MetricID} value of the provided source annotation */ public MetricID metricID(T annotation) { return new MetricID(name(annotation), tags(annotation)); } /** * Returns the metric {@link MetricID} as defined by the MP specification for the annotation situation at hand for * the provided {@link InjectionPoint}. This does take into account that annotations might have been added or * removed at runtime. * * @param point source {@link InjectionPoint} for an annotated element having this {@link AnnotationReader}'s * {@link #annotationType()}, not {@code null} * @return {@link MetricID} with full metric name as required by the MP specification * @throws IllegalArgumentException In case the provided {@link InjectionPoint} isn't effectively annotated with * this {@link AnnotationReader}'s {@link Annotation}. */ public MetricID metricID(InjectionPoint point) { return compute(point, this::metricID); } /** * Returns the metric {@link MetricID} as defined by the MP specification for the annotation situation at hand for * the provided {@link InjectionPoint}. This does take into account that annotations might have been added or * removed at runtime. * * @param point source {@link AnnotatedMember} for an annotated element having this {@link AnnotationReader}'s * {@link #annotationType()}, not {@code null} * @return {@link MetricID} with full metric name as required by the MP specification * @throws IllegalArgumentException In case the provided {@link AnnotatedMember} isn't effectively annotated with * this {@link AnnotationReader}'s {@link Annotation}. */ public MetricID metricID(AnnotatedMember member) { return compute(member, this::metricID); } /** * Reads the effective {@link MetricID} for the provided bean {@link Class} and {@link AnnotatedElement}. Either * bean or element must have this {@link AnnotationReader}'s {@link Annotation}. * * @param bean type of the bean that declared the provided element possibly annotated with this * {@link AnnotationReader}'s {@link #annotationType()} * @param element a {@link AnnotatedElement} possibly annotated with this {@link AnnotationReader}'s * {@link #annotationType()} * @return {@link MetricID} with full metric name as required by the MP specification * @throws IllegalArgumentException In case neither the {@link AnnotatedElement} or the {@link Class} isn't * annotated with this {@link AnnotationReader}'s {@link Annotation}. */ public MetricID metricID(Class bean, E element) { return compute(bean, element, this::metricID); } private MetricID metricID(T annotation, String name) { return new MetricID(name, tags(annotation)); } /** * Returns the metric description as defined by the provided {@link Annotation} * * @param annotation source annotation to read, not {@code null} * @return description of the provided source annotation */ public String description(T annotation) { return description.apply(annotation); } /** * Returns the metric unit as defined by the provided {@link Annotation} * * @param annotation source annotation to read, not {@code null} * @return unit of the provided source annotation */ public String unit(T annotation) { return unit.apply(annotation); } /** * Returns the scope as defined by the provided {@Link Annotation} * @param annotation source annotation to read, not {@code null} * @return scope of the provided source annotation */ public String scope(T annotation){ return scope.apply(annotation); } /** * Returns the metric absolute flag as defined by the provided {@link Annotation} * * @param annotation source annotation to read, not {@code null} * @return absolute flag of the provided source annotation */ public boolean absolute(T annotation) { return absolute.test(annotation); } /** * Returns the full {@link Metadata} as defined by the provided {@link Annotation} * * @param annotation source annotation to read, not {@code null} * @return {@link Metadata} of the provided source annotation */ public Metadata metadata(T annotation) { return metadata(annotation, name(annotation)); } /** * Returns the metric {@link Metadata} as defined by the MP specification for the annotation situation at hand for * the provided {@link InjectionPoint}. This does take into account that annotations might have been added or * removed at runtime. * * @param point source {@link InjectionPoint} for an annotated element having this {@link AnnotationReader}'s * {@link #annotationType()}, not {@code null} * @return {@link Metadata} with full metric name as required by the MP specification * @throws IllegalArgumentException In case the provided {@link InjectionPoint} isn't effectively annotated with * this {@link AnnotationReader}'s {@link Annotation}. */ public Metadata metadata(InjectionPoint point) { AnnotationReader reader = asAutoType(point.getType()); return reader.compute(point, reader::metadata); } /** * Returns the metric {@link Metadata} as defined by the MP specification for the annotation situation at hand for * the provided {@link InjectionPoint}. This does take into account that annotations might have been added or * removed at runtime. * * @param point source {@link AnnotatedMember} for an annotated element having this {@link AnnotationReader}'s * {@link #annotationType()}, not {@code null} * @return {@link Metadata} with full metric name as required by the MP specification * @throws IllegalArgumentException In case the provided {@link AnnotatedMember} isn't effectively annotated with * this {@link AnnotationReader}'s {@link Annotation}. */ public Metadata metadata(AnnotatedMember member) { AnnotationReader reader = asAutoType(member.getBaseType()); return reader.compute(member, reader::metadata); } /** * Reads the effective {@link Metadata} for the provided bean {@link Class} and {@link AnnotatedElement}. Either * bean or element must have this {@link AnnotationReader}'s {@link Annotation}. * * @param bean type of the bean that declared the provided element possibly annotated with this * {@link AnnotationReader}'s {@link #annotationType()} * @param element a {@link AnnotatedElement} possibly annotated with this {@link AnnotationReader}'s * {@link #annotationType()} * @return {@link Metadata} with full metric name as required by the MP specification * @throws IllegalArgumentException In case neither the {@link AnnotatedElement} or the {@link Class} isn't * annotated with this {@link AnnotationReader}'s {@link Annotation}. */ public Metadata metadata(Class bean, E element) { AnnotationReader reader = this; if (element instanceof Method) { reader = asAutoType(((Method) element).getGenericReturnType()); } else if (element instanceof Field) { reader = asAutoType(((Field) element).getGenericType()); } return reader.compute(bean, element, reader::metadata); } private Metadata metadata(T annotation, String name) { return Metadata.builder() .withName(name) .withDescription(description(annotation)) .withUnit(unit(annotation)) .build(); } @Override public boolean equals(Object obj) { return obj instanceof AnnotationReader && annotationType == ((AnnotationReader) obj).annotationType; } @Override public int hashCode() { return annotationType.hashCode(); } @Override public String toString() { return annotationType.toString(); } /** * Checks if an {@link Annotation} does not provide any information beyond the required name and tags. * * @param annotation source annotation to read, not {@code null} * @return true, of no property is set to a value that would require using {@link Metadata} when registering, else * false. */ public boolean isReference(T annotation) { return unit(annotation).equals(MetricUnits.NONE) && description(annotation).isEmpty(); } /** * Resolves the {@link org.eclipse.microprofile.metrics.Metric} referred to by the provided {@link InjectionPoint}. * If it does not exist, the metric is created. Lookup and creation are one atomic operation. Depending on the * provided information in the effective {@link Annotation} for the provided {@link InjectionPoint} the metric is * resolved or registered using {@link Metadata}, name and {@link Tag}s or just its name. * * A {@link org.eclipse.microprofile.metrics.Gauge} can only be resolved, not created. * * @param point source {@link InjectionPoint} for an annotated element having this {@link AnnotationReader}'s * {@link #annotationType()}, not {@code null} * @param metric type of the {@link org.eclipse.microprofile.metrics.Metric} to find or create, not {@code null} * @param registry {@link MetricRegistry} to use, not {@code null} * @return the resolved or registered metric, or {@code null} if a {@link org.eclipse.microprofile.metrics.Gauge} * did not exist */ public M getOrRegister(InjectionPoint point, Class metric, MetricRegistry registry) { T annotation = null; try { annotation = annotation(point); } catch (IllegalArgumentException ex) { // there was no annotation String name = MetricRegistry.name(point.getMember().getDeclaringClass().getCanonicalName(), localName(point.getMember())); return MetricUtils.getOrRegisterByName(registry, metric, name); } if (isReference(annotation)) { return MetricUtils.getOrRegisterByNameAndTags(registry, metric, name(point), tags(annotation)); } return MetricUtils.getOrRegisterByMetadataAndTags(registry, metric, metadata(point), tags(annotation)); } private R compute(InjectionPoint point, BiFunction func) { Annotated annotated = point.getAnnotated(); if (annotated instanceof AnnotatedMember) { return compute((AnnotatedMember) annotated, func); } if (annotated instanceof AnnotatedParameter) { return compute(point, (AnnotatedParameter) annotated, func); } throw new IllegalArgumentException("Unable to retrieve data for injection point [" + point + "], only members and parameters are supported"); } private R compute(InjectionPoint point, AnnotatedParameter parameter, BiFunction func) { //NB: This is a workaround as arquillians InjectionPoint implementation for parameters does return null for getJavaParameter Executable annotated = (Executable) point.getMember(); Member member = new MemberParameter(annotated.getParameters()[parameter.getPosition()]); return compute(member.getDeclaringClass(), member, parameter::getAnnotation, func); } private R compute(AnnotatedMember member, BiFunction func) { Member jmember = ((AnnotatedMember) member).getJavaMember(); return compute(jmember.getDeclaringClass(), jmember, member::getAnnotation, func); } private R compute(Class bean, E element, BiFunction func) { return compute(bean, element, element::getAnnotation, func); } private R compute(Class bean, Member member, Function, T> element, BiFunction func) { return compute(bean, member, element, annotation -> func.apply(annotation, namedOnElementLevel(annotation, member)), annotation -> func.apply(annotation, namedOnClassLevel(annotation, bean, member))); } private R compute(Class bean, E element, Function onElement, Function onClass) { return compute(bean, element, element::getAnnotation, onElement, onClass); } private R compute(Class bean, Member member, Function, T> element, Function onElement, Function onClass) { T annotation = element.apply(annotationType); if (annotation != null) { return onElement.apply(annotation); } if (bean.isAnnotationPresent(annotationType)) { return onClass.apply(bean.getAnnotation(annotationType)); } for (Annotation a : bean.getAnnotations()) { if (a.annotationType().isAnnotationPresent(Stereotype.class) && a.annotationType().isAnnotationPresent(annotationType)) { return onClass.apply(a.annotationType().getAnnotation(annotationType)); } } if (bean.getSuperclass() != null) { return compute(bean.getSuperclass(), member, element, onElement, onClass); } throw illegal(bean, member); } private String namedOnElementLevel(T annotation, Member member) { String localName = name(annotation); if (localName.isEmpty()) { localName = localName(member); } return absolute(annotation) ? localName : MetricRegistry.name(member.getDeclaringClass().getCanonicalName(), localName); } private String namedOnClassLevel(T annotation, Class bean, Member member) { String context = name(annotation); if (context.isEmpty()) { context = absolute(annotation) ? bean.getSimpleName() : bean.getCanonicalName(); } else if (!absolute(annotation)) { context = MetricRegistry.name(bean.getPackage().getName(), context); } return MetricRegistry.name(context, localName(member)); } private static String localName(Member member) { return member instanceof Constructor ? member.getDeclaringClass().getSimpleName() : member.getName(); } private IllegalArgumentException illegal(Class bean, Member member) { return new IllegalArgumentException("Neither given member " + member + "nor the given bean " + bean + " are annotated with " + annotationType); } /** * A simple wrapper that lets us pass a {@link Parameter} as if it is a {@link Member} * so that rest of the code can use {@link Member} as common abstraction. */ private static final class MemberParameter implements Member { private final Parameter param; MemberParameter(Parameter param) { this.param = param; } @Override public Class getDeclaringClass() { return param.getDeclaringExecutable().getDeclaringClass(); } @Override public String getName() { return param.getName(); } @Override public int getModifiers() { return param.getModifiers(); } @Override public boolean isSynthetic() { return param.isSynthetic(); } @Override public String toString() { return param.toString(); } } public static Tag[] tagsFromString(String[] tags) { if (tags == null || tags.length == 0) { return new Tag[0]; } return asList(tags).stream().map(AnnotationReader::tagFromString).toArray(Tag[]::new); } private static Tag tagFromString(String tag) { int splitIndex = tag.indexOf('='); if (splitIndex == -1) { throw new IllegalArgumentException("invalid tag: " + tag + ", tags must be in the form key=value"); } return new Tag(tag.substring(0, splitIndex), tag.substring(splitIndex + 1)); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy