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