001/*
002 * Configurate
003 * Copyright (C) zml and Configurate contributors
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *    http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.spongepowered.configurate.objectmapping.meta;
018
019import org.checkerframework.checker.nullness.qual.Nullable;
020import org.spongepowered.configurate.serialize.SerializationException;
021
022import java.lang.annotation.Annotation;
023import java.lang.reflect.Type;
024import java.text.MessageFormat;
025import java.util.Locale;
026import java.util.ResourceBundle;
027import java.util.regex.Matcher;
028import java.util.regex.Pattern;
029
030/**
031 * Perform a validation on data upon load.
032 *
033 * @param <V> value type
034 * @since 4.0.0
035 */
036@FunctionalInterface
037public interface Constraint<V> {
038
039    /**
040     * Check if the provided deserialized value matches an expected condition.
041     *
042     * @param value value to test
043     * @throws SerializationException if the value falls outside its constraint.
044     * @since 4.0.0
045     */
046    void validate(@Nullable V value) throws SerializationException;
047
048    /**
049     * Provider for a specific constraint given a field type.
050     *
051     * @param <A> annotation type
052     * @param <V> data type
053     * @since 4.0.0
054     */
055    @FunctionalInterface
056    interface Factory<A extends Annotation, V> {
057
058        /**
059         * Create a new specialized constraint.
060         *
061         * @param data annotation with metadata
062         * @param type annotated type. is a subtype of {@code V}
063         * @return new constraint
064         * @since 4.0.0
065         */
066        Constraint<V> make(A data, Type type);
067    }
068
069    /**
070     * Require a value to be present for fields marked with the
071     * annotation {@code T}.
072     *
073     * @param <T> marker annotation type
074     * @return new constraint factory
075     * @since 4.0.0
076     */
077    static <T extends Annotation> Constraint.Factory<T, Object> required() {
078        return (data, type) -> value -> {
079            if (value == null) {
080                throw new SerializationException("A value is required for this field");
081            }
082        };
083    }
084
085    /**
086     * Require values to match the {@link Matches#value() pattern} provided.
087     *
088     * <p>Upon failure, an error message will be taken from the annotation.</p>
089     *
090     * @return factory providing matching pattern test
091     * @since 4.0.0
092     */
093    static Constraint.Factory<Matches, String> pattern() {
094        return (data, type) -> {
095            final Pattern test = Pattern.compile(data.value(), data.flags());
096            final MessageFormat format = new MessageFormat(data.failureMessage(), Locale.getDefault());
097            return value -> {
098                if (value != null) {
099                    final Matcher match = test.matcher(value);
100                    if (!match.matches()) {
101                        throw new SerializationException(format.format(new Object[]{value, data.value()}));
102                    }
103                }
104            };
105        };
106    }
107
108    /**
109     * Require values to match the {@link Matches#value() pattern} provided.
110     *
111     * <p>Upon failure, an error message will be taken from {@code bundle} with
112     * a key defined in the annotation.</p>
113     *
114     * @param bundle source for localized messages
115     * @return factory providing matching pattern test
116     * @since 4.0.0
117     */
118    static Constraint.Factory<Matches, String> localizedPattern(final ResourceBundle bundle) {
119        return (data, type) -> {
120            final Pattern test = Pattern.compile(data.value(), data.flags());
121            final MessageFormat format = new MessageFormat(Localization.key(bundle, data.failureMessage()), bundle.getLocale());
122            return value -> {
123                if (value != null) {
124                    final Matcher match = test.matcher(value);
125                    if (!match.matches()) {
126                        throw new SerializationException(format.format(new Object[]{value, data.value()}));
127                    }
128                }
129            };
130        };
131    }
132
133}