AbstractNumberValidator.java

/*
 * Copyright 2021 Red Hat, Inc. and/or its affiliates
 * and other contributors as indicated by the @author tags.
 *
 * 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
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * 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 org.keycloak.validate.validators;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.ConfiguredProvider;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.utils.StringUtil;
import org.keycloak.validate.AbstractSimpleValidator;
import org.keycloak.validate.ValidationContext;
import org.keycloak.validate.ValidationError;
import org.keycloak.validate.ValidationResult;
import org.keycloak.validate.ValidatorConfig;

/**
 * Abstract class for number validator. Supports min and max value validations using {@link #KEY_MIN} and
 * {@link #KEY_MAX} config options.
 * 
 * @author Vlastimil Elias <velias@redhat.com>
 */
public abstract class AbstractNumberValidator extends AbstractSimpleValidator implements ConfiguredProvider {

    public static final String MESSAGE_INVALID_NUMBER = "error-invalid-number";
    public static final String MESSAGE_NUMBER_OUT_OF_RANGE = "error-number-out-of-range";
    public static final String MESSAGE_NUMBER_OUT_OF_RANGE_TOO_SMALL = "error-number-out-of-range-too-small";
    public static final String MESSAGE_NUMBER_OUT_OF_RANGE_TOO_BIG = "error-number-out-of-range-too-big";

    public static final String KEY_MIN = "min";
    public static final String KEY_MAX = "max";

    private final ValidatorConfig defaultConfig;
    
    protected static final List<ProviderConfigProperty> configProperties = new ArrayList<>();

    static {
        ProviderConfigProperty property;
        property = new ProviderConfigProperty();
        property.setName(KEY_MIN);
        property.setLabel("Minimum");
        property.setHelpText("The minimal allowed value - this config is optional.");
        property.setType(ProviderConfigProperty.STRING_TYPE);
        configProperties.add(property);
        property = new ProviderConfigProperty();
        property.setName(KEY_MAX);
        property.setLabel("Maximum");
        property.setHelpText("The maximal allowed value - this config is optional.");
        property.setType(ProviderConfigProperty.STRING_TYPE);
        configProperties.add(property);
    }

    public AbstractNumberValidator() {
        // for reflection
        this(ValidatorConfig.EMPTY);
    }

    public AbstractNumberValidator(ValidatorConfig config) {
        this.defaultConfig = config;
    }
    
    public List<ProviderConfigProperty> getConfigProperties() {
        return configProperties;
    }

    @Override
    protected boolean skipValidation(Object value, ValidatorConfig config) {
        if (isIgnoreEmptyValuesConfigured(config) && (value == null || value instanceof String)) {
            return value == null || StringUtil.isBlank(value.toString());
        }
        return false;
    }

    @Override
    protected void doValidate(Object value, String inputHint, ValidationContext context, ValidatorConfig config) {
        if (config == null || config.isEmpty()) {
            config = defaultConfig;
        }

        Number number = null;

        if (value != null) {
            try {
                number = convert(value, config);
            } catch (NumberFormatException ignore) {
                // N/A
            }
        }

        if (number == null) {
            context.addError(new ValidationError(getId(), inputHint, MESSAGE_INVALID_NUMBER));
            return;
        }

        Number min = getMinMaxConfig(config, KEY_MIN);
        Number max = getMinMaxConfig(config, KEY_MAX);

        if (min != null && isFirstGreaterThanToSecond(min, number)) {
            context.addError(new ValidationError(getId(), inputHint, selectRangeErrorMessage(config), min, max));
            return;
        }

        if (max != null && isFirstGreaterThanToSecond(number, max)) {
            context.addError(new ValidationError(getId(), inputHint, selectRangeErrorMessage(config), min, max));
            return;
        }

        return;
    }
    
    /**
     * Select error message depending on the allowed range interval bound configuration.
     */
    protected String selectRangeErrorMessage(ValidatorConfig config) {
        if (!config.containsKey(KEY_MAX)) {
            return MESSAGE_NUMBER_OUT_OF_RANGE_TOO_SMALL;
        } else if (!config.containsKey(KEY_MIN)) {
            return MESSAGE_NUMBER_OUT_OF_RANGE_TOO_BIG;
        } else {
            return MESSAGE_NUMBER_OUT_OF_RANGE;
        }
    }

    @Override
    public ValidationResult validateConfig(KeycloakSession session, ValidatorConfig config) {
        Set<ValidationError> errors = new LinkedHashSet<>();

        if (config != null) {
            boolean containsMin = config.containsKey(KEY_MIN);
            boolean containsMax = config.containsKey(KEY_MAX);

            Number min = getMinMaxConfig(config, KEY_MIN);
            Number max = getMinMaxConfig(config, KEY_MAX);

            if (containsMin && min == null) {
                errors.add(new ValidationError(getId(), KEY_MIN, ValidatorConfigValidator.MESSAGE_CONFIG_INVALID_NUMBER_VALUE, config.get(KEY_MIN)));
            }

            if (containsMax && max == null) {
                errors.add(new ValidationError(getId(), KEY_MAX, ValidatorConfigValidator.MESSAGE_CONFIG_INVALID_NUMBER_VALUE, config.get(KEY_MAX)));
            }

            if (errors.isEmpty() && containsMin && containsMax && (!isFirstGreaterThanToSecond(max, min))) {
                errors.add(new ValidationError(getId(), KEY_MAX, ValidatorConfigValidator.MESSAGE_CONFIG_INVALID_VALUE));
            }
        }

        ValidationResult s = super.validateConfig(session, config);

        if (!s.isValid()) {
            errors.addAll(s.getErrors());
        }

        return new ValidationResult(errors);
    }

    /**
     * Convert input value to instance of Number supported by this validator.
     * 
     * @param value to convert
     * @param config
     * @return value converted to supported Number instance
     * @throws NumberFormatException if value is not convertible to supported Number instance so
     *             {@link #MESSAGE_INVALID_NUMBER} error is reported.
     */
    protected abstract Number convert(Object value, ValidatorConfig config);

    /**
     * Get config value for min and max validation bound as a Number supported by this validator
     * 
     * @param config to get from
     * @param key of the config value
     * @return bound value or null
     */
    protected abstract Number getMinMaxConfig(ValidatorConfig config, String key);

    /**
     * Compare two numbers of supported type (fed by {@link #convert(Object, ValidatorConfig)} and
     * {@link #getMinMaxConfig(ValidatorConfig, String)} )
     * 
     * @param n1
     * @param n2
     * @return true if first number is greater than second
     */
    protected abstract boolean isFirstGreaterThanToSecond(Number n1, Number n2);

}