ControllerUtil.java

package se.jobtechdev.personaldatagateway.api.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.lang.NonNull;
import se.jobtechdev.personaldatagateway.api.exception.ApiException;
import se.jobtechdev.personaldatagateway.api.exception.ValueAssignmentException;

import java.lang.reflect.Field;
import java.util.*;
import java.util.function.Predicate;

import static se.jobtechdev.personaldatagateway.api.util.ProblemDetailsFactory.createProblemDetails;

public class ControllerUtil {
  private static final Logger logger = LoggerFactory.getLogger(ControllerUtil.class);

  private ControllerUtil() {
  }

  public static <T> Predicate<T> notAmong(Set<T> set) {
    return t -> !set.contains(t);
  }

  @NonNull
  public static <T> T throwApiExceptionOnAbsentValue(
      @SuppressWarnings("OptionalUsedAsFieldOrParameterType") Optional<T> value,
      HttpStatus httpStatus,
      String logMessageOnFailure) {
    if (Objects.isNull(value) || value.isEmpty()) {
      final var apiException = new ApiException(createProblemDetails(httpStatus, logMessageOnFailure));
      if (apiException.getProblemDetails().getStatus() >= 500) {
        logger.error(logMessageOnFailure, apiException);
      }
      throw apiException;
    }
    return value.get();
  }

  public static <T> T earlyExit(
      T value, Predicate<T> check, HttpStatus httpStatus, String messageOnExit) {
    try {
      if (Boolean.TRUE.equals(check.test(value))) {
        final var errorResponse = (messageOnExit != null)
            ? createProblemDetails(httpStatus, messageOnExit)
            : ProblemDetailsFactory.createProblemDetails(httpStatus);
        throw new ApiException(errorResponse);
      }
      return value;
    } catch (ApiException apiException) {
      if (messageOnExit != null && apiException.getProblemDetails().getStatus() >= 500) {
        logger.error(messageOnExit, apiException);
      }

      throw apiException;
    } catch (RuntimeException exception) {
      logger.error("Failure when applying earlyExit check condition", exception);
      throw new ApiException(ProblemDetailsFactory.createProblemDetails(HttpStatus.INTERNAL_SERVER_ERROR));
    }
  }

  protected static <T> void preventReassignment(T currentValue, T newValue, String valueName) {
    if (newValue != null && currentValue != null && !currentValue.equals(newValue)) {
      throw ApiExceptionFactory.createApiException(
          ProblemDetailsFactory.createProblemDetails(
              HttpStatus.CONFLICT,
              valueName + " already exist and can not be overwritten with new value"));
    }
  }

  /**
   * This method assumes that all fields present in the "input" type has getters that follows camel
   * casing naming style. It also assumes that the "target" type has a setter with the same naming
   * style.
   *
   * <p>Example of two compatible types: class Input { String val; public String getVal() { return
   * val; } } class Target { String val; public String getVal() { return val; } public void setVal()
   * { this.val = val; } }
   *
   * <p>This method will only set fields in the "target" instance if the "input" field is non-null
   * and different from the "target" field.
   */
  public static <I, T> T assignNonNull(I input, T target) {
    return assignNonNull(input, target, List.of());
  }

  public static <I, T> T assignNonNull(
      I input, T target, List<String> preventReassignmentOfFields) {
    final var fields = Arrays.stream(input.getClass().getDeclaredFields()).toList();

    final var allFieldNames = fields.stream().map(Field::getName).toList();

    return assignNonNull(input, target, allFieldNames, preventReassignmentOfFields);
  }

  public static <I, T> T assignNonNull(
      I input, T target, List<String> whitelistedFields, List<String> preventReassignmentOfFields) {
    final var fields = Arrays.stream(input.getClass().getDeclaredFields()).toList();
    for (final var field : fields) {
      try {
        final var fieldName = field.getName();
        final var fieldNamePascalCase =
            fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);

        if (!whitelistedFields.contains(fieldName)) continue;

        final var getterName = "get" + fieldNamePascalCase;
        final var setterName = "set" + fieldNamePascalCase;
        final var inputGetter = input.getClass().getMethod(getterName);
        final var targetGetter = target.getClass().getMethod(getterName);
        final var targetSetter = target.getClass().getMethod(setterName, field.getType());

        final var inputValue = inputGetter.invoke(input);
        final var targetValue = targetGetter.invoke(target);

        if (preventReassignmentOfFields.contains(fieldName))
          preventReassignment(targetValue, inputValue, fieldName);

        if (inputValue != null && !inputValue.equals(targetValue)) {
          targetSetter.invoke(target, inputValue);
        }
      } catch (Exception e) {
        if (e instanceof ApiException apiException) {
          throw apiException;
        }
        throw new ValueAssignmentException(
            "Failed to assign non-null values to target instance!", e);
      }
    }

    return target;
  }

  public static ClientAndRole loggedInClientAndRole() {
    final var clientId = AuthUtil.getLoggedInClientId();
    final var role = AuthUtil.getLoggedInRole();
    return new ClientAndRole(clientId, role);
  }

  public record ClientAndRole(String clientId, String role) {
  }
}