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 */
036public interface Constraint<V> {
037
038    /**
039     * Check if the provided deserialized value matches an expected condition.
040     *
041     * @param value value to test
042     * @throws SerializationException if the value falls outside its constraint.
043     * @since 4.0.0
044     */
045    void validate(@Nullable V value) throws SerializationException;
046
047    /**
048     * Provider for a specific constraint given a field type.
049     *
050     * @param <A> annotation type
051     * @param <V> data type
052     * @since 4.0.0
053     */
054    interface Factory<A extends Annotation, V> {
055
056        /**
057         * Create a new specialized constraint.
058         *
059         * @param data annotation with metadata
060         * @param type annotated type. is a subtype of {@code V}
061         * @return new constraint
062         * @since 4.0.0
063         */
064        Constraint<V> make(A data, Type type);
065    }
066
067    /**
068     * Require a value to be present for fields marked with the
069     * annotation {@code T}.
070     *
071     * @param <T> marker annotation type
072     * @return new constraint factory
073     * @since 4.0.0
074     */
075    static <T extends Annotation> Constraint.Factory<T, Object> required() {
076        return (data, type) -> value -> {
077            if (value == null) {
078                throw new SerializationException("A value is required for this field");
079            }
080        };
081    }
082
083    /**
084     * Require values to match the {@link Matches#value() pattern} provided.
085     *
086     * <p>Upon failure, an error message will be taken from the annotation.</p>
087     *
088     * @return factory providing matching pattern test
089     * @since 4.0.0
090     */
091    static Constraint.Factory<Matches, String> pattern() {
092        return (data, type) -> {
093            final Pattern test = Pattern.compile(data.value(), data.flags());
094            final MessageFormat format = new MessageFormat(data.failureMessage(), Locale.getDefault());
095            return value -> {
096                if (value != null) {
097                    final Matcher match = test.matcher(value);
098                    if (!match.matches()) {
099                        throw new SerializationException(format.format(new Object[]{value, data.value()}));
100                    }
101                }
102            };
103        };
104    }
105
106    /**
107     * Require values to match the {@link Matches#value() pattern} provided.
108     *
109     * <p>Upon failure, an error message will be taken from {@code bundle} with
110     * a key defined in the annotation.</p>
111     *
112     * @param bundle source for localized messages
113     * @return factory providing matching pattern test
114     * @since 4.0.0
115     */
116    static Constraint.Factory<Matches, String> localizedPattern(final ResourceBundle bundle) {
117        return (data, type) -> {
118            final Pattern test = Pattern.compile(data.value(), data.flags());
119            final MessageFormat format = new MessageFormat(Localization.key(bundle, data.failureMessage()), bundle.getLocale());
120            return value -> {
121                if (value != null) {
122                    final Matcher match = test.matcher(value);
123                    if (!match.matches()) {
124                        throw new SerializationException(format.format(new Object[]{value, data.value()}));
125                    }
126                }
127            };
128        };
129    }
130
131}