/** * Copyright 2020 VMware, Inc. * <p> * 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 * <p> * https://www.apache.org/licenses/LICENSE-2.0 * <p> * 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 io.micrometer.core.instrument.config.validate; import io.micrometer.core.annotation.Incubating; import io.micrometer.core.instrument.util.StringUtils; import io.micrometer.core.lang.NonNull; import io.micrometer.core.lang.Nullable; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; import static java.util.stream.StreamSupport.stream; /** * Validation support for {@link io.micrometer.core.instrument.config.MeterRegistryConfig}. * * @author Jon Schneider * @since 1.5.0 */ @Incubating(since = "1.5.0") public interface Validated<T> extends Iterable<Validated<T>> { boolean isValid(); default boolean isInvalid() { return !isValid(); } default List<Invalid<?>> failures() { return stream(spliterator(), false) .filter(Validated::isInvalid) .map(v -> (Invalid<T>) v) .collect(Collectors.toList()); } static Secret validSecret(String property, String value) { return new Secret(property, value); } static <T> None<T> none() { return new None<>(); } static <T> Valid<T> valid(String property, @Nullable T value) { return new Valid<>(property, value); } static <T> Invalid<T> invalid(String property, @Nullable Object value, String message, InvalidReason reason) { return invalid(property, value, message, reason, null); } static <T> Invalid<T> invalid(String property, @Nullable Object value, String message, InvalidReason reason, @Nullable Throwable exception) { return new Invalid<>(property, value, message, reason, exception); } default Validated<?> and(Validated<?> validated) { if (this instanceof None) { return validated; } return new Either(this, validated); } <U> Validated<U> map(Function<T, U> mapping); <U> Validated<U> flatMap(BiFunction<T, Valid<T>, Validated<U>> mapping); default <U> Validated<U> flatMap(Function<T, Validated<U>> mapping) { return flatMap((value, original) -> mapping.apply(value)); } /** * When the condition is met, turn a {@link Valid} result into an {@link Invalid} result with the provided message. * * @param condition {@code true} when the property should be considered invalid. * @param message A message explaining the reason why the property is considered invalid. * @param reason An invalid reason. * @return When originally {@link Valid}, apply the test and either retain the valid decision or make it {@link Invalid}. * When originally {@link Invalid} or {@link None}, don't apply the test at all, and pass through the original decision. */ default Validated<T> invalidateWhen(Predicate<T> condition, String message, InvalidReason reason) { return flatMap((value, valid) -> condition.test(value) ? Validated.invalid(valid.property, value, message, reason) : valid); } default Validated<T> required() { return invalidateWhen(Objects::isNull, "is required", InvalidReason.MISSING); } default Validated<T> nonBlank() { return invalidateWhen(t -> StringUtils.isBlank(t.toString()), "cannot be blank", InvalidReason.MISSING); } T get() throws ValidationException; default T orElse(@Nullable T t) throws ValidationException { return orElseGet(() -> t); } T orElseGet(Supplier<T> t) throws ValidationException; void orThrow() throws ValidationException; /** * Indicates that no validation has occurred. None is considered "valid", effectively a no-op validation. * * @param <T> A type that this validation is being coerced to or joined with in a list of validators. */ class None<T> implements Validated<T> { @Override public boolean isValid() { return true; } @SuppressWarnings("unchecked") @Override public <U> Validated<U> map(Function<T, U> mapping) { return (Validated<U>) this; } @SuppressWarnings("unchecked") @Override public <U> Validated<U> flatMap(BiFunction<T, Valid<T>, Validated<U>> mapping) { return (Validated<U>) this; } @Override public T get() { return null; } @Override public T orElseGet(Supplier<T> t) { return null; } @Override public void orThrow() { } @NonNull @Override public Iterator<Validated<T>> iterator() { return Collections.emptyIterator(); } } /** * A specialization {@link Valid} that won't print the secret in plain text if the validation is serialized. */ class Secret extends Valid<String> { public Secret(String property, String value) { super(property, value); } @Override public String toString() { return "Secret{" + "property='" + property + '\'' + '}'; } } /** * A valid property value. * * @param <T> The type of the property. */ class Valid<T> implements Validated<T> { protected final String property; private final T value; public Valid(String property, T value) { this.property = property; this.value = value; } @Override public boolean isValid() { return true; } @NonNull @Override public Iterator<Validated<T>> iterator() { return Stream.of((Validated<T>) this).iterator(); } @Override public T get() { return value; } @Override public void orThrow() { } @Override public T orElseGet(Supplier<T> t) { return value == null ? t.get() : value; } @Override public <U> Validated<U> map(Function<T, U> mapping) { return new Valid<>(property, mapping.apply(value)); } @Override public <U> Validated<U> flatMap(BiFunction<T, Valid<T>, Validated<U>> mapping) { return mapping.apply(value, this); } public String getProperty() { return property; } @Override public String toString() { return "Valid{" + "property='" + property + '\'' + ", value='" + value + '\'' + '}'; } } class Invalid<T> implements Validated<T> { private final String property; @Nullable private final Object value; private final String message; private final InvalidReason reason; @Nullable private final Throwable exception; public Invalid(String property, @Nullable Object value, String message, InvalidReason reason, @Nullable Throwable exception) { this.property = property; this.value = value; this.message = message; this.reason = reason; this.exception = exception; } @Override public boolean isValid() { return false; } @NonNull @Override public Iterator<Validated<T>> iterator() { return Stream.of((Validated<T>) this).iterator(); } public String getMessage() { return message; } public InvalidReason getReason() { return reason; } @Nullable public Throwable getException() { return exception; } @Override public T get() throws ValidationException { throw new ValidationException(this); } @Override public T orElseGet(Supplier<T> t) throws ValidationException { throw new ValidationException(this); } @Override public void orThrow() throws ValidationException { throw new ValidationException(this); } @SuppressWarnings("unchecked") @Override public <U> Validated<U> map(Function<T, U> mapping) { return (Validated<U>) this; } @SuppressWarnings("unchecked") @Override public <U> Validated<U> flatMap(BiFunction<T, Valid<T>, Validated<U>> mapping) { return (Validated<U>) this; } public String getProperty() { return property; } @Nullable public Object getValue() { return value; } @Override public String toString() { return "Invalid{" + "property='" + property + '\'' + ", value='" + value + '\'' + ", message='" + message + '\'' + '}'; } } class Either implements Validated<Object> { private final Validated<?> left; private final Validated<?> right; public Either(Validated<?> left, Validated<?> right) { this.left = left; this.right = right; } @Override public boolean isValid() { return left.isValid() && right.isValid(); } @Override public Object get() { throw new UnsupportedOperationException("get not supported on more than one Validated object"); } @Override public Object orElseGet(Supplier<Object> o) throws ValidationException { throw new UnsupportedOperationException("orElse not supported on more than one Validated object"); } @Override public void orThrow() throws ValidationException { List<Invalid<?>> failures = failures(); if (!failures.isEmpty()) { throw new ValidationException(this); } } @Override public <U> Validated<U> map(Function<Object, U> mapping) { throw new UnsupportedOperationException("cannot invoke map on more than one Validated object"); } @Override public <U> Validated<U> flatMap(BiFunction<Object, Valid<Object>, Validated<U>> mapping) { throw new UnsupportedOperationException("cannot invoke flatMap on more than one Validated object"); } @NonNull @Override public Iterator<Validated<Object>> iterator() { return Stream.concat( stream(left.spliterator(), false).map(v -> v.map(o -> (Object) o)), stream(right.spliterator(), false).map(v -> v.map(o -> (Object) o)) ).iterator(); } } }