Package org.apache.sis.parameter

Source Code of org.apache.sis.parameter.Verifier

/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements.  See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.sis.parameter;

import java.util.Map;
import java.util.Set;
import java.lang.reflect.Array;
import javax.measure.unit.Unit;
import javax.measure.converter.UnitConverter;
import javax.measure.converter.ConversionException;
import org.opengis.parameter.ParameterDescriptor;
import org.opengis.parameter.GeneralParameterDescriptor;
import org.opengis.parameter.InvalidParameterValueException;
import org.apache.sis.measure.Range;
import org.apache.sis.measure.Units;
import org.apache.sis.util.Numbers;
import org.apache.sis.util.resources.Errors;


/**
* Verifies the validity of a given value.
* An instance of {@code Verifier} is created only if an error is detected.
* In such case, the error message is given by {@link #message(Map, String, Object)}.
*
* @author  Martin Desruisseaux (IRD, Geomatys)
* @since   0.4 (derived from geotk-2.0)
* @version 0.4
* @module
*/
final class Verifier {
    /**
     * The {@link Errors.Keys} value that describe the invalid value.
     */
    private final short errorKey;

    /**
     * {@code true} if the last element in {@link #arguments} shall be set to the erroneous value.
     */
    private final boolean needsValue;

    /**
     * The arguments to be used with the error message to format.
     * The current implementation relies on the following invariants:
     *
     * <ul>
     *   <li>The first element in this array will be the parameter name. Before the name is known,
     *       this element is either {@code null} or the index to append to the name.</li>
     *   <li>The last element shall be set to the erroneous value if {@link #needsValue} is {@code true}.</li>
     * </ul>
     */
    private final Object[] arguments;

    /**
     * Stores information about an error.
     */
    private Verifier(final short errorKey, final boolean needsValue, final Object... arguments) {
        this.errorKey   = errorKey;
        this.needsValue = needsValue;
        this.arguments  = arguments;
    }

    /**
     * Ensures that the given value is valid according the specified parameter descriptor.
     * This method ensures that {@code value} is assignable to the
     * {@linkplain ParameterDescriptor#getValueClass() expected class}, is between the
     * {@linkplain ParameterDescriptor#getMinimumValue() minimum} and
     * {@linkplain ParameterDescriptor#getMaximumValue() maximum} values and is one of the
     * {@linkplain ParameterDescriptor#getValidValues() set of valid values}.
     * If the value fails any of those tests, then an exception is thrown.
     *
     * @param  <T> The type of parameter value. The given {@code value} should typically be an instance of this class.
     *             This is not required by this method signature but is checked by this method implementation.
     *
     * @param  descriptor The parameter descriptor to check against.
     * @param  value      The value to check, or {@code null}.
     * @param  unit       The unit of the value to check, or {@code null}.
     * @return The given value converted to the descriptor unit if any,
     *         then casted to the descriptor parameterized type.
     * @throws InvalidParameterValueException if the parameter value is invalid.
     */
    @SuppressWarnings("unchecked")
    static <T> T ensureValidValue(final ParameterDescriptor<T> descriptor, final Object value, final Unit<?> unit)
            throws InvalidParameterValueException
    {
        final Class<T> valueClass = descriptor.getValueClass();
        /*
         * Before to verify if the given value is inside the bounds, we need to convert the value
         * to the units used by the parameter descriptor. The first part of this block verifies
         * the validity of the unit argument, so we execute it even if 'value' is null.
         */
        UnitConverter converter = null;
        Object convertedValue = value;
        if (unit != null) {
            final Unit<?> def = descriptor.getUnit();
            if (def == null) {
                final String name = getName(descriptor);
                throw new InvalidParameterValueException(Errors.format(Errors.Keys.UnitlessParameter_1, name), name, unit);
            }
            if (!unit.equals(def)) {
                final short expectedID = getUnitMessageID(def);
                if (getUnitMessageID(unit) != expectedID) {
                    throw new IllegalArgumentException(Errors.format(expectedID, unit));
                }
                /*
                 * Verify the type of the user's value before to perform the unit conversion,
                 * because the conversion will create a new object not necessarily of the same type.
                 */
                if (value != null) {
                    if (!valueClass.isInstance(value)) {
                        final String name = getName(descriptor);
                        throw new InvalidParameterValueException(
                                Errors.format(Errors.Keys.IllegalParameterValueClass_3,
                                name, valueClass, value.getClass()), name, value);
                    }
                    /*
                     * From this point we will perform the actual unit conversion. The value may be either
                     * a Number instance, or an array of numbers (typically an array of type double[]). In
                     * the array case, we will store the converted values in a new array of the same type.
                     */
                    try {
                        converter = unit.getConverterToAny(def);
                    } catch (ConversionException e) {
                        throw new IllegalArgumentException(Errors.format(Errors.Keys.IncompatibleUnits_2, unit, def), e);
                    }
                    Class<?> componentType = valueClass.getComponentType();
                    if (componentType == null) {
                        /*
                         * Usual case where the value is not an array. Convert the value directly.
                         * Note that the value can only be a number because the unit is associated
                         * to MeasurementRange, which accepts only numbers.
                         */
                        Number n = converter.convert(((Number) value).doubleValue());
                        try {
                            convertedValue = Numbers.cast(n, (Class<? extends Number>) valueClass);
                        } catch (IllegalArgumentException e) {
                            throw new InvalidParameterValueException(e.getLocalizedMessage(), getName(descriptor), value);
                        }
                    } else {
                        /*
                         * The value is an array. Creates a new array and store the converted values
                         * using Array reflection.
                         */
                        final int length = Array.getLength(value);
                        convertedValue = Array.newInstance(componentType, length);
                        componentType = Numbers.primitiveToWrapper(componentType);
                        for (int i=0; i<length; i++) {
                            Number n = (Number) Array.get(value, i);
                            n = converter.convert(n.doubleValue()); // Value in units that we can compare.
                            try {
                                n = Numbers.cast(n, (Class<? extends Number>) componentType);
                            } catch (IllegalArgumentException e) {
                                throw new InvalidParameterValueException(e.getLocalizedMessage(),
                                        getName(descriptor) + '[' + i + ']', value);
                            }
                            Array.set(convertedValue, i, n);
                        }
                    }
                }
            }
        }
        /*
         * At this point the user's value has been fully converted to the unit of measurement specified
         * by the ParameterDescriptor. Now compares the converted value to the restricting given by the
         * descriptor (set of valid values and range of value domain).
         */
        if (convertedValue != null) {
            final Verifier error;
            final Set<T> validValues = descriptor.getValidValues();
            if (descriptor instanceof DefaultParameterDescriptor<?>) {
                error = ensureValidValue(valueClass, validValues,
                        ((DefaultParameterDescriptor<?>) descriptor).getValueDomain(), convertedValue);
            } else {
                error = ensureValidValue(valueClass, validValues,
                        descriptor.getMinimumValue(), descriptor.getMaximumValue(), convertedValue);
            }
            if (error != null) {
                error.convertRange(converter);
                final String name = getName(descriptor);
                throw new InvalidParameterValueException(error.message(null, name, value), name, value);
            }
        }
        return (T) convertedValue;
    }

    /**
     * Compares the given value against the given descriptor properties. If the value is valid, returns {@code null}.
     * Otherwise returns an object that can be used for formatting the error message.
     *
     * @param convertedValue The value <em>converted to the units specified by the descriptor</em>.
     *        This is not necessarily the user-provided value.
     */
    @SuppressWarnings("unchecked")
    static <T> Verifier ensureValidValue(final Class<T> valueClass, final Set<T> validValues,
            final Range<?> valueDomain, final Object convertedValue)
    {
        final Verifier verifier = ensureValidValue(valueClass, validValues, null, null, convertedValue);
        if (verifier == null && valueDomain != null) {
            if (!valueClass.isArray()) {
                /*
                 * Following assertion should never fail with DefaultParameterDescriptor instances.
                 * It could fail if the user overrides DefaultParameterDescriptor.getValueDomain()
                 * in a way that break the method contract.
                 */
                assert valueDomain.getElementType() == valueClass : valueDomain;
                if (!((Range) valueDomain).contains((Comparable<?>) convertedValue)) {
                    return new Verifier(Errors.Keys.ValueOutOfRange_4, true, null,
                            valueDomain.getMinValue(), valueDomain.getMaxValue(), convertedValue);
                }
            } else {
                /*
                 * Following assertion should never fail under the same condition than above.
                 */
                assert valueDomain.getElementType() == Numbers.primitiveToWrapper(valueClass.getComponentType()) : valueDomain;
                final int length = Array.getLength(convertedValue);
                for (int i=0; i<length; i++) {
                    final Object e = Array.get(convertedValue, i);
                    if (!((Range) valueDomain).contains((Comparable<?>) e)) {
                        return new Verifier(Errors.Keys.ValueOutOfRange_4, true, i,
                                valueDomain.getMinValue(), valueDomain.getMaxValue(), e);
                    }
                }
            }
        }
        return verifier;
    }

    /**
     * Same as {@link #ensureValidValue(Class, Set, Range, Object)}, used as a fallback when
     * the descriptor is not an instance of {@link DefaultParameterDescriptor}.
     *
     * <div class="note"><b>Implementation note:</b>
     * At the difference of {@code ensureValidValue(…, Range, …)}, this method does not need to verify array elements
     * because the type returned by {@link ParameterDescriptor#getMinimumValue()} and {@code getMaximumValue()}
     * methods (namely {@code Comparable<T>}) does not allow usage with arrays.</div>
     *
     * @param convertedValue The value <em>converted to the units specified by the descriptor</em>.
     *        This is not necessarily the user-provided value.
     */
    @SuppressWarnings("unchecked")
    private static <T> Verifier ensureValidValue(final Class<T> valueClass, final Set<T> validValues,
            final Comparable<T> minimum, final Comparable<T> maximum, final Object convertedValue)
    {
        if (!valueClass.isInstance(convertedValue)) {
            return new Verifier(Errors.Keys.IllegalParameterValueClass_3, false, null, valueClass, convertedValue.getClass());
        }
        if (validValues != null && !validValues.contains(convertedValue)) {
            return new Verifier(Errors.Keys.IllegalParameterValue_2, true, null, convertedValue);
        }
        if ((minimum != null && minimum.compareTo((T) convertedValue) > 0) ||
            (maximum != null && maximum.compareTo((T) convertedValue) < 0))
        {
            return new Verifier(Errors.Keys.ValueOutOfRange_4, true, null, minimum, maximum, convertedValue);
        }
        return null;
    }

    /**
     * Converts the information about an "value out of range" error. The range in the error message will be formatted
     * in the unit given by the user, which is not necessarily the same than the unit of the parameter descriptor.
     *
     * @param converter The conversion from user unit to descriptor unit, or {@code null} if none. This method
     *        uses the inverse of that conversion for converting the given minimum and maximum values.
     */
    private void convertRange(UnitConverter converter) {
        if (converter != null && errorKey == Errors.Keys.ValueOutOfRange_4) {
            converter = converter.inverse();
            Object minimumValue = arguments[1];
            Object maximumValue = arguments[2];
            minimumValue = (minimumValue != null) ? converter.convert(((Number) minimumValue).doubleValue()) : "-∞";
            maximumValue = (maximumValue != null) ? converter.convert(((Number) maximumValue).doubleValue()) "∞";
            arguments[1] = minimumValue;
            arguments[2] = maximumValue;
        }
    }

    /**
     * Returns an error message for the error detected by
     * {@link #ensureValidValue(Class, Set, Range, Object)}.
     *
     * @param name  The parameter name.
     * @param value The user-supplied value (not necessarily equals to the converted value).
     */
    String message(final Map<?,?> properties, String name, Object value) {
        final Object index = arguments[0];
        if (index != null) {
            name = name + '[' + index + ']';
            value = Array.get(value, (Integer) index);
        }
        arguments[0] = name;
        if (needsValue) {
            arguments[arguments.length - 1] = value;
        }
        return Errors.getResources(properties).getString(errorKey, arguments);
    }

    /**
     * Convenience method returning the name of the specified descriptor.
     * This method is used mostly for output to be read by human, not for processing.
     * Consequently, we may consider to returns a localized name in a future version.
     */
    static String getName(final GeneralParameterDescriptor descriptor) {
        return descriptor.getName().getCode();
    }

    /**
     * Returns the unit type as one of error message code.
     * Used for checking unit with a better error message formatting if needed.
     */
    static short getUnitMessageID(final Unit<?> unit) {
        if (Units.isLinear  (unit)) return Errors.Keys.NonLinearUnit_1;
        if (Units.isAngular (unit)) return Errors.Keys.NonAngularUnit_1;
        if (Units.isTemporal(unit)) return Errors.Keys.NonTemporalUnit_1;
        if (Units.isScale   (unit)) return Errors.Keys.NonScaleUnit_1;
        return Errors.Keys.IncompatibleUnit_1;
    }
}
TOP

Related Classes of org.apache.sis.parameter.Verifier

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.