/*
 * Redistribution and use of this software and associated documentation
 * ("Software"), with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * 1. Redistributions of source code must retain copyright
 *    statements and notices.  Redistributions must also contain a
 *    copy of this document.
 *
 * 2. Redistributions in binary form must reproduce the
 *    above copyright notice, this list of conditions and the
 *    following disclaimer in the documentation and/or other
 *    materials provided with the distribution.
 *
 * 3. The name "Exolab" must not be used to endorse or promote
 *    products derived from this Software without prior written
 *    permission of Intalio, Inc.  For written permission,
 *    please contact info@exolab.org.
 *
 * 4. Products derived from this Software may not be called "Exolab"
 *    nor may "Exolab" appear in their names without prior written
 *    permission of Intalio, Inc. Exolab is a registered
 *    trademark of Intalio, Inc.
 *
 * 5. Due credit should be given to the Exolab Project
 *    (http://www.exolab.org/).
 *
 * THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
 * INTALIO, INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Copyright 2000-2004 (C) Intalio, Inc. All Rights Reserved.
 *
 * $Id: FieldValidator.java 8617 2010-05-17 21:01:28Z wguttmn $
 */
package org.exolab.castor.xml;

import java.lang.reflect.Array;
import java.text.MessageFormat;
import java.util.Enumeration;
import java.util.List;
import java.util.Vector;

import org.exolab.castor.mapping.FieldHandler;
import org.exolab.castor.xml.location.XPathLocation;

/**
 * Handles field validation.
 *
 * @author <a href="mailto:kvisco-at-intalio.com">Keith Visco</a>
 * @version $Revision: 8617 $ $Date: 2004-10-08 22:58:55 -0600 (Fri, 08 Oct 2004) $
 */
public class FieldValidator extends Validator {

    /** Default value for descriptor names. If a Descriptor XML name value is
     * set to this, then no name has been assigned yet. */
    private static final String ERROR_NAME   = "-error-if-this-is-used-";
    /** Default minimum occurrance. */
    private static final int    DEFAULT_MIN  = 0;
    /** Default maximum occurrance. */
    private static final int    DEFAULT_MAX  = -1;

    /** Minimum number of occurrences for this element to pass validation. */
    private int                 _minOccurs   = DEFAULT_MIN;
    /** Maximum number of occurrences for this element to pass validation. */
    private int                 _maxOccurs   = DEFAULT_MAX;
    /** The Field Descriptor describing the field we validate. */
    private XMLFieldDescriptor  _descriptor  = null;
    /** The actual type validator which is used to validate single instances of
     * the field. */
    private TypeValidator       _validator   = null;
    
    /**
     * Creates a default FieldValidator.
     */
    public FieldValidator() {
        super();
    }

    /**
     * Creates a new FieldValidator using the given TypeValidator.
     * @param validator the TypeValidator to delegate validation to
     */
    public FieldValidator(final TypeValidator validator) {
        super();
        this._validator = validator;
    }

    /**
     * Returns the mimimum number of occurances for a given object.
     *
     * @return The mimimum number of occurances for a given object. A zero value
     *         denotes no lower bound (ie. the object is optional).
     */
    public int getMinOccurs() {
        return _minOccurs;
    }

    /**
     * Returns the maximum number of occurances for a given object.
     *
     * @return The maximum number of occurances for a given object. A negative
     *         value denotes no upper bound.
     */
    public int getMaxOccurs() {
        return _maxOccurs;
    }

    /**
     * Returns the TypeValidator.
     * @return the TypeValidator.
     */
    public TypeValidator getTypeValidator() {
        return _validator;
    }

    /**
     * Returns true if a TypeValidator has been set.
     * @return true if a TypeValidator has been set.
     */
    public boolean hasTypeValidator() {
        return _validator != null;
    }

    /**
     * Sets the mimimum number of occurances for a given object. A zero, or
     * negative value denotes no lower bound (i.e., the object is optional).
     *
     * @param minOccurs the minimum number of times an object must occur in
     *        order to be valid.
     */
    public void setMinOccurs(final int minOccurs) {
        this._minOccurs = (minOccurs < 0) ? 0 : minOccurs;
    }

    /**
     * Sets the maximum number of occurances for a given object. A negative
     * value denotes no upper bound.
     *
     * @param maxOccurs the maximum number of times an object may occur.
     */
    public void setMaxOccurs(final int maxOccurs) {
        this._maxOccurs = maxOccurs;
    }

    /**
     * Sets the field descriptor to use for obtaining information about the
     * field to validate, such as the field name, the field handler, etc.
     *
     * @param descriptor the field descriptor for the field to validate
     */
    public void setDescriptor(final XMLFieldDescriptor descriptor) {
        this._descriptor = descriptor;
    }

    public void setValidator(final TypeValidator validator) {
        this._validator = validator;
    }

    /**
     * Validates the given Object.
     *
     * @param object the Object that contains the field to validate
     * @param context the ValidationContext
     * @throws ValidationException if validation fails
     */
    public void validate(final Object object, final ValidationContext context)
                                                      throws ValidationException {
        if (_descriptor == null || object == null || context.isValidated(object)) {
            return;
        }

        // Don't validate "transient" fields.
        if (_descriptor.isTransient()) {
            return;
        }

        FieldHandler handler = _descriptor.getHandler();
        if (handler == null) {
            return;
        }

        // Get the value of the field
        Object value = handler.getValue(object);
        if (value == null) {
            if (!_descriptor.isRequired() || _descriptor.isNillable()) {
                return;
            }
            // deal with lenient id/idref validation accordingly, skipping exception handling
            // in this case
            if (_descriptor.isRequired() 
                    && _descriptor.getSchemaType() != null
                    && _descriptor.getSchemaType().equals("IDREF") 
                    && context.getInternalContext().getLenientIdValidation()) {
                return;
            }
            StringBuffer buff = new StringBuffer();
            if (!ERROR_NAME.equals(_descriptor.getXMLName())) {
                buff.append(MessageFormat.format(resourceBundle.getString("validatorField.error.required.field.whose"), 
                        new Object[] {_descriptor.getFieldName(), object.getClass().getName(), _descriptor.getXMLName()}));
            }
            else {
                buff.append(MessageFormat.format(resourceBundle.getString("validatorField.error.required.field"), 
                        new Object[] {_descriptor.getFieldName(), object.getClass().getName()}));
            }
            
            throw new ValidationException(buff.toString());
        }

        if (_descriptor.isReference()) {
            if (_validator != null) {
                _validator.validate(value, context);
            }
            return;
        }

        // Prevent endless loop!  Have we seen this object yet?
        if (context != null) {
            if (context.isValidated(object)) {
                return;
            }
            //-- mark object as processed
            context.addValidated(object);
        }

        // We are now ready to do actual validation

        Class type = value.getClass();
        int size = 1;
        long occurence = -1;

        try {
            if (type.isArray()) {
                // We don't validate Byte array types
                if (type.getComponentType() != Byte.TYPE) {
                    size = Array.getLength(value);
                    if (_validator != null) {
                        for (int i = 0; i < size; i++) {
                            occurence = i + 1;
                            _validator.validate(Array.get(value, i), context);
                        }
                    } else {
                        for (int i = 0; i < size; i++) {
                            super.validate(Array.get(value, i), context);
                        }
                    }
                }
            } else if (value instanceof Enumeration) {
                // <NOTE>
                // The following code should be changed to use CollectionHandler
                // </NOTE>
                size = 0;
                for (Enumeration enumeration = (Enumeration) value; 
                   enumeration.hasMoreElements(); ) {
                    ++size;
                    validateInstance(context, enumeration.nextElement());
                }
            } else if (value instanceof Vector) {
                Vector vector = (Vector) value;
                size = vector.size();
                for (int i = 0; i < size; i++) {
                    occurence = i + 1;
                    validateInstance(context, vector.elementAt(i));
                }
            } else if (value instanceof List) {
                List list = (List) value;
                size = list.size();
                for (int i = 0; i < size; i++) {
                    occurence = i + 1;
                    validateInstance(context, list.get(i));
                }
            } else {
                validateInstance(context, value);
            }
        } catch (ValidationException vx) {
            //-- add additional validation information
            String err = MessageFormat.format(resourceBundle.getString("validatorField.error.exception"), 
                    new Object[] { _descriptor.getFieldName(), object.getClass().getName()});
            ValidationException validationException = new ValidationException(err, vx);
            addLocationInformation(_descriptor, validationException, occurence);
            throw validationException;
        }

        // Check sizes of collection

        // Check minimum.
        // If any items exist (size != 0) or the descriptor is marked as
        // required then we need to report the error. Otherwise size == 0 and
        // field is not required, so no error.
        if (size < _minOccurs && (size != 0 || _descriptor.isRequired())) {
            StringBuffer buff = new StringBuffer();
            if (!ERROR_NAME.equals(_descriptor.getXMLName())) {
                buff.append(MessageFormat.format(resourceBundle.getString("validatorField.error.exception.min.occurs.whose"), 
                        new Object[] { _minOccurs, _descriptor.getFieldName(), object.getClass().getName(),
                    _descriptor.getXMLName()}));
            } else {
                buff.append(MessageFormat.format(resourceBundle.getString("validatorField.error.exception.min.occurs"), 
                        new Object[] { _minOccurs, _descriptor.getFieldName(), object.getClass().getName()}));
            }
            throw new ValidationException(buff.toString());
        }

        // Check maximum.
        if (_maxOccurs >= 0 && size > _maxOccurs) {
            StringBuffer buff = new StringBuffer();
            if (!ERROR_NAME.equals(_descriptor.getXMLName())) {
                buff.append(MessageFormat.format(resourceBundle.getString("validatorField.error.exception.max.occurs.whose"), 
                        new Object[] { _maxOccurs, _descriptor.getFieldName(), object.getClass().getName(), _descriptor.getXMLName()}));
            } else {
                buff.append(MessageFormat.format(resourceBundle.getString("validatorField.error.exception.max.occurs"), 
                        new Object[] { _maxOccurs, _descriptor.getFieldName(), object.getClass().getName()}));
            }

            throw new ValidationException(buff.toString());
        }

        if (context != null) {
            context.removeValidated(object);
        }
    }

    /**
     * Validate an individual instance.
     * @param context the validation context.
     * @param value The instance to validate.
     * @throws ValidationException if validation fails
     */
    private void validateInstance(final ValidationContext context, final Object value)
                                                            throws ValidationException {
        if (_validator != null) {
            _validator.validate(value, context);
        } else {
            super.validate(value, context);
        }
    }

    /**
     * Adds location information to the {@link ValidationException} instance.
     * @param fieldDescriptor The {@link XMLFieldDescriptor} instance whose has been responsible for
     * generating the error.
     * @param e The {@link ValidationException} to enrich.
     * @param occurence If greater than 0, denotes the position of the invalid collection value.
     */
    private void addLocationInformation(final XMLFieldDescriptor fieldDescriptor,
            final ValidationException e, final long occurence) {
        XPathLocation loc = (XPathLocation) e.getLocation();
        if (loc == null) {
            loc = new XPathLocation();
            e.setLocation(loc);
            String xmlName = fieldDescriptor.getXMLName();
            if (occurence > 0) {
                xmlName += "[" + occurence + "]";
            }
            if (fieldDescriptor.getNodeType() == NodeType.Attribute) {
                loc.addAttribute(xmlName);
            } else {
                loc.addChild(xmlName);
            }
        }
    }

}
