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

feign.DeclarativeContract Maven / Gradle / Ivy

/*
 * Copyright 2012-2023 The Feign Authors
 *
 * Licensed 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 feign;

import feign.Contract.BaseContract;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
 * {@link Contract} base implementation that works by declaring witch annotations should be
 * processed and how each annotation modifies {@link MethodMetadata}
 */
public abstract class DeclarativeContract extends BaseContract {

  private final List classAnnotationProcessors = new ArrayList<>();
  private final List methodAnnotationProcessors = new ArrayList<>();
  private final Map, DeclarativeContract.ParameterAnnotationProcessor> parameterAnnotationProcessors =
      new HashMap<>();

  @Override
  public final List parseAndValidateMetadata(Class targetType) {
    // any implementations must register processors
    return super.parseAndValidateMetadata(targetType);
  }

  /**
   * Called by parseAndValidateMetadata twice, first on the declaring class, then on the target type
   * (unless they are the same).
   *
   * @param data metadata collected so far relating to the current java method.
   * @param targetType the class to process
   */
  @Override
  protected final void processAnnotationOnClass(MethodMetadata data, Class targetType) {
    final List processors = Arrays.stream(targetType.getAnnotations())
        .flatMap(annotation -> classAnnotationProcessors.stream()
            .filter(processor -> processor.test(annotation)))
        .collect(Collectors.toList());

    if (!processors.isEmpty()) {
      Arrays.stream(targetType.getAnnotations())
          .forEach(annotation -> processors.stream()
              .filter(processor -> processor.test(annotation))
              .forEach(processor -> processor.process(annotation, data)));
    } else {
      if (targetType.getAnnotations().length == 0) {
        data.addWarning(String.format(
            "Class %s has no annotations, it may affect contract %s",
            targetType.getSimpleName(),
            getClass().getSimpleName()));
      } else {
        data.addWarning(String.format(
            "Class %s has annotations %s that are not used by contract %s",
            targetType.getSimpleName(),
            Arrays.stream(targetType.getAnnotations())
                .map(annotation -> annotation.annotationType()
                    .getSimpleName())
                .collect(Collectors.toList()),
            getClass().getSimpleName()));
      }
    }
  }

  /**
   * @param data metadata collected so far relating to the current java method.
   * @param annotation annotations present on the current method annotation.
   * @param method method currently being processed.
   */
  @Override
  protected final void processAnnotationOnMethod(MethodMetadata data,
                                                 Annotation annotation,
                                                 Method method) {
    List processors = methodAnnotationProcessors.stream()
        .filter(processor -> processor.test(annotation))
        .collect(Collectors.toList());

    if (!processors.isEmpty()) {
      processors.forEach(processor -> processor.process(annotation, data));
    } else {
      data.addWarning(String.format(
          "Method %s has an annotation %s that is not used by contract %s",
          method.getName(),
          annotation.annotationType()
              .getSimpleName(),
          getClass().getSimpleName()));
    }
  }


  /**
   * @param data metadata collected so far relating to the current java method.
   * @param annotations annotations present on the current parameter annotation.
   * @param paramIndex if you find a name in {@code annotations}, call
   *        {@link #nameParam(MethodMetadata, String, int)} with this as the last parameter.
   * @return true if you called {@link #nameParam(MethodMetadata, String, int)} after finding an
   *         http-relevant annotation.
   */
  @Override
  protected final boolean processAnnotationsOnParameter(MethodMetadata data,
                                                        Annotation[] annotations,
                                                        int paramIndex) {
    List matchingAnnotations = Arrays.stream(annotations)
        .filter(
            annotation -> parameterAnnotationProcessors.containsKey(annotation.annotationType()))
        .collect(Collectors.toList());

    if (!matchingAnnotations.isEmpty()) {
      matchingAnnotations.forEach(annotation -> parameterAnnotationProcessors
          .getOrDefault(annotation.annotationType(), ParameterAnnotationProcessor.DO_NOTHING)
          .process(annotation, data, paramIndex));

    } else {
      final Parameter parameter = data.method().getParameters()[paramIndex];
      String parameterName = parameter.isNamePresent()
          ? parameter.getName()
          : parameter.getType().getSimpleName();
      if (annotations.length == 0) {
        data.addWarning(String.format(
            "Parameter %s has no annotations, it may affect contract %s",
            parameterName,
            getClass().getSimpleName()));
      } else {
        data.addWarning(String.format(
            "Parameter %s has annotations %s that are not used by contract %s",
            parameterName,
            Arrays.stream(annotations)
                .map(annotation -> annotation.annotationType()
                    .getSimpleName())
                .collect(Collectors.toList()),
            getClass().getSimpleName()));
      }
    }
    return false;
  }

  /**
   * Called while class annotations are being processed
   *
   * @param annotationType to be processed
   * @param processor function that defines the annotations modifies {@link MethodMetadata}
   */
  protected  void registerClassAnnotation(Class annotationType,
                                                                DeclarativeContract.AnnotationProcessor processor) {
    registerClassAnnotation(
        annotation -> annotation.annotationType().equals(annotationType),
        processor);
  }

  /**
   * Called while class annotations are being processed
   *
   * @param predicate to check if the annotation should be processed or not
   * @param processor function that defines the annotations modifies {@link MethodMetadata}
   */
  protected  void registerClassAnnotation(Predicate predicate,
                                                                DeclarativeContract.AnnotationProcessor processor) {
    this.classAnnotationProcessors.add(new GuardedAnnotationProcessor(predicate, processor));
  }

  /**
   * Called while method annotations are being processed
   *
   * @param annotationType to be processed
   * @param processor function that defines the annotations modifies {@link MethodMetadata}
   */
  protected  void registerMethodAnnotation(Class annotationType,
                                                                 DeclarativeContract.AnnotationProcessor processor) {
    registerMethodAnnotation(
        annotation -> annotation.annotationType().equals(annotationType),
        processor);
  }

  /**
   * Called while method annotations are being processed
   *
   * @param predicate to check if the annotation should be processed or not
   * @param processor function that defines the annotations modifies {@link MethodMetadata}
   */
  protected  void registerMethodAnnotation(Predicate predicate,
                                                                 DeclarativeContract.AnnotationProcessor processor) {
    this.methodAnnotationProcessors.add(new GuardedAnnotationProcessor(predicate, processor));
  }

  /**
   * Called while method parameter annotations are being processed
   *
   * @param annotation to be processed
   * @param processor function that defines the annotations modifies {@link MethodMetadata}
   */
  protected  void registerParameterAnnotation(Class annotation,
                                                                    DeclarativeContract.ParameterAnnotationProcessor processor) {
    this.parameterAnnotationProcessors.put((Class) annotation,
        (DeclarativeContract.ParameterAnnotationProcessor) processor);
  }

  @FunctionalInterface
  public interface AnnotationProcessor {

    /**
     * @param annotation present on the current element.
     * @param metadata collected so far relating to the current java method.
     */
    void process(E annotation, MethodMetadata metadata);
  }

  @FunctionalInterface
  public interface ParameterAnnotationProcessor {

    DeclarativeContract.ParameterAnnotationProcessor DO_NOTHING = (ann, data, i) -> {
    };

    /**
     * @param annotation present on the current parameter annotation.
     * @param metadata metadata collected so far relating to the current java method.
     * @param paramIndex if you find a name in {@code annotations}, call
     *        {@link #nameParam(MethodMetadata, String, int)} with this as the last parameter.
     * @return true if you called {@link #nameParam(MethodMetadata, String, int)} after finding an
     *         http-relevant annotation.
     */
    void process(E annotation, MethodMetadata metadata, int paramIndex);
  }

  private class GuardedAnnotationProcessor
      implements Predicate, DeclarativeContract.AnnotationProcessor {

    private final Predicate predicate;
    private final DeclarativeContract.AnnotationProcessor processor;

    @SuppressWarnings({"rawtypes", "unchecked"})
    private GuardedAnnotationProcessor(Predicate predicate,
        DeclarativeContract.AnnotationProcessor processor) {
      this.predicate = predicate;
      this.processor = processor;
    }

    @Override
    public void process(Annotation annotation, MethodMetadata metadata) {
      processor.process(annotation, metadata);
    }

    @Override
    public boolean test(Annotation t) {
      return predicate.test(t);
    }

  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy