/*
 * 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.openjpa.persistence.validation;

import java.security.AccessController;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import javax.persistence.ValidationMode;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.metadata.BeanDescriptor;

import org.apache.openjpa.conf.OpenJPAConfiguration;
import org.apache.openjpa.event.LifecycleEvent;
import org.apache.openjpa.lib.conf.Configuration;
import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.util.J2DoPrivHelper;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.StringUtil;
import org.apache.openjpa.persistence.JPAProperties;
import org.apache.openjpa.validation.AbstractValidator;
import org.apache.openjpa.validation.ValidationException;

public class ValidatorImpl extends AbstractValidator {

    private static final Localizer _loc = Localizer.forPackage(ValidatorImpl.class);

    private ValidatorFactory _validatorFactory = null;
    private Validator _validator = null;
    private ValidationMode _mode = ValidationMode.AUTO;
    private OpenJPAConfiguration _conf = null;
    private transient Log _log = null;

    // A map storing the validation groups to use for a particular event type
    private Map<Integer, Class<?>[]> _validationGroups = new HashMap<>();

    // Lookup table for event to group property mapping
    private static HashMap<String, Integer> _vgMapping = new HashMap<> ();

    static {
        _vgMapping.put(JPAProperties.VALIDATE_PRE_PERSIST, LifecycleEvent.BEFORE_PERSIST);
        _vgMapping.put(JPAProperties.VALIDATE_PRE_REMOVE,  LifecycleEvent.BEFORE_DELETE);
        _vgMapping.put(JPAProperties.VALIDATE_PRE_UPDATE,  LifecycleEvent.BEFORE_UPDATE);
    }

    /**
     * Default constructor.  Builds a default validator factory, if available
     * and creates the validator.
     * Returns an Exception if a Validator could not be created.
     */
    public ValidatorImpl() {
        initialize();
    }

    public ValidatorImpl(Configuration conf) {
        if (conf instanceof OpenJPAConfiguration) {
            _conf = (OpenJPAConfiguration)conf;
            _log = _conf.getLog(OpenJPAConfiguration.LOG_RUNTIME);
            Object validatorFactory = _conf.getValidationFactoryInstance();
            String mode = _conf.getValidationMode();
            _mode = Enum.valueOf(ValidationMode.class, mode);
            if (validatorFactory != null) {
                if (validatorFactory instanceof ValidatorFactory) {
                    _validatorFactory = (ValidatorFactory)validatorFactory;
                } else {
                    // Supplied object was not an instance of a ValidatorFactory
                    throw new IllegalArgumentException(
                        _loc.get("invalid-factory").getMessage());
                }
            }
        }
        initialize();
    }

    /**
     * Type-specific constructor
     * Returns an Exception if a Validator could not be created.
     * @param validatorFactory Instance of validator factory to use.  Specify
     *        null to use the default factory.
     * @param mode ValdiationMode enum value
     */
    public ValidatorImpl(ValidatorFactory validatorFactory,
        ValidationMode mode) {
        if (mode != null) {
            _mode = mode;
        }
        if (validatorFactory != null) {
            _validatorFactory = validatorFactory;
        }
        initialize();
    }

    /**
     * Common setup code factored out of the constructors
     */
    private void initialize() {
        // only try setting up a validator if mode is not NONE
        if (_mode != ValidationMode.NONE) {
            if (_validatorFactory == null) {
                _validatorFactory = getDefaultValidatorFactory();
            }
            if (_validatorFactory != null) {
                // use our TraversableResolver instead of BV provider one
                _validator = _validatorFactory.usingContext().
                    traversableResolver(new TraversableResolverImpl()).
                    getValidator();
            } else {
                // A default ValidatorFactory could not be created.
                throw new RuntimeException(
                    _loc.get("no-default-factory").getMessage());
            }

            // throw an exception if we have no Validator
            if (_validator == null) {
                // A Validator provider could not be created.
                throw new RuntimeException(
                    _loc.get("no-validator").getMessage());
            }

            if (_conf != null) {
                addValidationGroup(JPAProperties.VALIDATE_PRE_PERSIST, _conf.getValidationGroupPrePersist());
                addValidationGroup(JPAProperties.VALIDATE_PRE_UPDATE,  _conf.getValidationGroupPreUpdate());
                addValidationGroup(JPAProperties.VALIDATE_PRE_REMOVE,  _conf.getValidationGroupPreRemove());
            } else {
                // add in default validation groups, which can be over-ridden later
                addDefaultValidationGroups();
            }
        } else {
            // A Validator should not be created based on the supplied ValidationMode.
            throw new RuntimeException(
                _loc.get("no-validation").getMessage());
        }
    }

    /**
     * Add a validation group for the specific property.  The properties map
     * to a specific lifecycle event.  To disable validation for a group, set
     * the validation group to null.
     *
     * @param validationGroupName
     * @param vgs
     */
    public void addValidationGroup(String validationGroupName, Class<?>...vgs) {
        Integer event = findEvent(validationGroupName);
        if (event != null) {
            _validationGroups.put(event, vgs);
            return;
        } else {
            // There were no events found for group "{0}".
            throw new IllegalArgumentException(
                _loc.get("no-group-events", validationGroupName).getMessage());
        }
    }

    /**
     * Add a validation group for a specified event.  Event definitions
     * are defined in LifecycleEvent.  To disable validation for a group, set
     * the validation group to null.
     *
     * @param event
     * @param validationGroup
     */
    public void addValidationGroup(Integer event, Class<?>... validationGroup) {
        _validationGroups.put(event, validationGroup);
    }

    /**
     * Add the validation group(s) for the specified event.  Event definitions
     * are defined in LifecycleEvent
     * @param event
     * @param group
     */
    public void addValidationGroup(String validationGroupName, String group) {
        Integer event = findEvent(validationGroupName);
        if (event != null) {
            Class<?>[] vgs = getValidationGroup(validationGroupName, group);
            if (vgs != null) {
                addValidationGroup(event, vgs);
            }
        } else {
            // There were no events found for group "{0}".
            throw new IllegalArgumentException(
                _loc.get("no-group-events", validationGroupName).getMessage());
        }
    }

    /**
     * Converts a comma separated list of validation groups into an array
     * of classes.
     * @param group
     */
    private Class<?>[] getValidationGroup(String vgName, String group) {
        Class<?>[] vgGrp = null;
        if (group == null || group.trim().length() == 0) {
            return null;
        }
        String[] strClasses = group.split(",");
        if (strClasses.length > 0) {
            vgGrp = new Class<?>[strClasses.length];
            for (int i = 0; i < strClasses.length; i++) {
                try {
                    vgGrp[i] =  Class.forName(StringUtil.trim(strClasses[i]));
                } catch (Throwable t) {
                    throw new IllegalArgumentException(
                        _loc.get("invalid-validation-group", StringUtil.trim(strClasses[i]),
                            vgName).getMessage(), t);
                }
            }
        }
        return vgGrp;
    }

    /**
     * Return the validation groups to be validated for a specified event
     * @param event Lifecycle event id
     * @return An array of validation groups
     */
    public Class<?>[] getValidationGroup(Integer event) {
        return _validationGroups.get(event);
    }

    /**
     * Returns whether the Validator is validating for the
     * specified event.  Based on whether validation groups are specified for
     * the event.
     *
     * @param event the event to check for validation
     * @return returns true if validating for this particular event
     */
    public boolean isValidating(Integer event) {
        return (_validationGroups.get(event) != null);
    }

    /**
     * Returns the validation constraints for the specified class
     *
     * @param cls Class for which constraints to return
     * @return The validation bean descriptor
     */
    public BeanDescriptor getConstraintsForClass(Class<?> cls) {
        return _validator.getConstraintsForClass(cls);
    }

    /**
     * Validates a given instance
     *
     * @param <T> The instance to validate
     * @param arg0 The class, of type T to validate
     * @param event The event id
     * @return ValidationException if the validator produces one or more
     *         constraint violations.
     */
    @Override
    public <T> ValidationException validate(T arg0, int event) {
        if (!isValidating(event))
            return null;
        Set<ConstraintViolation<T>> violations = AccessController.doPrivileged(
                J2DoPrivHelper.validateAction(_validator, arg0, getValidationGroup(event)));

        if (violations != null && violations.size() > 0) {
            return new ValidationException(
                new ConstraintViolationException(
                    // A validation constraint failure occurred for class "{0}".
                    _loc.get("validate-failed",
                        arg0.getClass().getName()).getMessage(),
                    (Set)violations),
                true);
        }
        return null;
    }

    /**
     * Validates a property of a given instance
     *
     * @param <T> The instance to validate
     * @param arg0 The property to validate
     * @param property The property to validate
     * @param event The event id
     * @return ValidationException if the validator produces one or more
     *         constraint violations.
     */
    @Override
    public <T> ValidationException validateProperty(T arg0, String property,
        int event) {
        if (!isValidating(event))
            return null;
        Set<ConstraintViolation<T>> violations =
            _validator.validateProperty(arg0, property,
                getValidationGroup(event));
        if (violations != null && violations.size() > 0) {
            return new ValidationException(
                new ConstraintViolationException(
                    // A validation constraint failure occurred for
                    // property "{1}" in class "{0}".
                    _loc.get("valdiate-property-failed",
                        arg0.getClass().getName(),property).getMessage(),
                    (Set)violations),
                true);
        }
        return null;
    }

    /**
     * Validates a value based upon the constraints applied to a given class
     * attribute.
     * @param <T> The instance type to base validation upon
     * @param arg0 The class of type T to validate
     * @param arg1 The property to validate
     * @param arg2 The property value to validate
     * @param event The event id
     * @return ValidationException if the validator produces one or more
     *         constraint violations.
     */
    @Override
    public <T> ValidationException validateValue(Class<T> arg0,
        String arg1, Object arg2, int event)  {
        if (!isValidating(event))
            return null;
        Set<ConstraintViolation<T>> violations =
            _validator.validateValue(arg0, arg1, arg2,
                getValidationGroup(event));
        if (violations != null && violations.size() > 0) {
            return new ValidationException(
                new ConstraintViolationException(
                    // A validation constraint failure occurred for
                    // value "{2}" of property "{1}" in class "{0}".
                    _loc.get("validate-value-failed", arg0.getName(),
                        arg1, arg2.toString()).getMessage(),
                    (Set)violations),
                true);
        }
        return null;
    }

    /**
     * Returns whether validation is active for the given event.
     *
     * @param <T>
     * @param arg0 Type being validated
     * @param event event type
     * @return true if validation is active for the specified event
     */
    @Override
    public <T> boolean validating(T arg0, int event) {
        // TODO: This method will also make a determination based upon which
        // groups are validating and the group defined on the class
        return isValidating(event);
    }

    // Lookup the lifecycle event id for the validationProperty
    private Integer findEvent(String validationProperty) {
        return _vgMapping.get(validationProperty);
    }

    // Get the default validator factory
    private ValidatorFactory getDefaultValidatorFactory() {
        ValidatorFactory factory = null;
        try {
            factory = AccessController.doPrivileged(J2DoPrivHelper.buildDefaultValidatorFactoryAction());
        } catch (javax.validation.ValidationException e) {
            if (_log != null && _log.isTraceEnabled())
                _log.trace(_loc.get("factory-create-failed"), e);
        }
        return factory;
    }

    // Per JSR-317, the pre-persist and pre-update groups will validate using
    // the default validation group and pre-remove will not validate (no
    // validation group)
    private void addDefaultValidationGroups() {
        addValidationGroup(JPAProperties.VALIDATE_PRE_PERSIST, javax.validation.groups.Default.class);
        addValidationGroup(JPAProperties.VALIDATE_PRE_UPDATE,  javax.validation.groups.Default.class);
    }
}
