/*
 * 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.myfaces.extensions.validator.core.interceptor;

import org.apache.myfaces.extensions.validator.core.el.ELHelper;
import org.apache.myfaces.extensions.validator.internal.UsageCategory;
import org.apache.myfaces.extensions.validator.internal.UsageInformation;
import org.apache.myfaces.extensions.validator.core.ExtValContext;
import org.apache.myfaces.extensions.validator.core.ValidationModuleKey;
import org.apache.myfaces.extensions.validator.core.ExtValCoreConfiguration;
import org.apache.myfaces.extensions.validator.core.metadata.extractor.MetaDataExtractor;
import org.apache.myfaces.extensions.validator.core.property.PropertyInformation;
import org.apache.myfaces.extensions.validator.core.storage.RendererInterceptorPropertyStorage;
import org.apache.myfaces.extensions.validator.core.renderkit.exception.SkipBeforeInterceptorsException;
import org.apache.myfaces.extensions.validator.core.renderkit.exception.SkipRendererDelegationException;
import org.apache.myfaces.extensions.validator.core.renderkit.exception.SkipAfterInterceptorsException;
import org.apache.myfaces.extensions.validator.core.renderkit.RendererProxy;
import org.apache.myfaces.extensions.validator.core.recorder.ProcessedInformationRecorder;
import org.apache.myfaces.extensions.validator.util.ExtValUtils;

import javax.faces.context.FacesContext;
import javax.faces.component.UIComponent;
import javax.faces.component.EditableValueHolder;
import javax.faces.convert.ConverterException;
import javax.faces.render.Renderer;
import javax.faces.validator.ValidatorException;
import javax.el.PropertyNotFoundException;
import java.io.IOException;
import java.util.Map;
import java.util.HashMap;
import java.util.logging.Level;

/**
 * A basic implementation of {@link RendererInterceptor} for validating fields.
 * It adds some extension point for subclasses and performs tasks like : <br/>
 * - storing field values ({@link #recordProcessedInformation}) <br/>
 * - resetting required information property UIComponent <br/>
 * - calling before and after Validation interceptors <br/>
 * - etc ...
 *
 * @author Gerhard Petracek
 * @since x.x.3
 */
@UsageInformation(UsageCategory.REUSE)
public abstract class AbstractValidationInterceptor extends AbstractRendererInterceptor
{
    private ELHelper elHelper;

    /**
     * In case of required initialization
     * it's needed for some use-cases to reset the (required-)state of the components.
     * Such a reset is just needed if required initialization is activated.
     *
     * @return true if required initialization is supported by the current implementation, false otherwise
     */
    protected boolean isRequiredInitializationSupported()
    {
        return false;
    }

    /**
     * Sets required property of UIComponent to false after decoding.
     * It's needed for special use-cases if required initialization is supported.
     * The final required validation will be done by the corresponding constraint validator.
     *
     * {@inheritDoc}
     */
    @Override
    public void afterDecode(FacesContext facesContext, UIComponent uiComponent, Renderer wrapped)
            throws SkipAfterInterceptorsException
    {
        /*
         * component initialization sets a component to required if there are constraints which indicate it
         * the required flag in a component leads to problems with h:messages (additional message) as well as
         * incompatibilities with skip validation and severities
         */
        if(uiComponent instanceof EditableValueHolder && ExtValUtils.isRequiredResetActivated() &&
                isRequiredInitializationSupported() && ExtValUtils.isRequiredInitializationActive())
        {
            ((EditableValueHolder)uiComponent).setRequired(false);
        }
    }

    /**
     * Before the component gets rendered the interceptor initializes the component based on the meta-data
     * which is provided by the referenced property (if component initialization is activated).
     *
     * {@inheritDoc}
     */
    @Override
    public void beforeEncodeBegin(FacesContext facesContext, UIComponent uiComponent, Renderer wrapped)
            throws IOException, SkipBeforeInterceptorsException, SkipRendererDelegationException
    {
        if(processComponent(uiComponent) && !isComponentInitializationDeactivated())
        {
            initComponent(facesContext, uiComponent);
        }
    }

    /**
     * Initialize the component based on the meta-data which is provided by the referenced property.
     *
     * @param facesContext The JSF Context
     * @param uiComponent The component which is processed
     */
    protected abstract void initComponent(FacesContext facesContext, UIComponent uiComponent);

    /**
     * The method performs the validation of the field and calls the registered interceptors regarding the validation.
     *
     * The main steps are :<br/>
     * - Get the converted value from the renderer (possibly cached by the RendererProxy)<br/>
     * - Record the value (e.g. for cross validation)<br/>
     * - Adjust the converted value for interpret empty values as null.<br/>
     * - Execute the beforeValidation method of the registered PropertyValidationInterceptor's. <br/>
     * - Perform the validation when the PropertyValidationInterceptor have indicate it that it should be performed.
     * <br/>
     * - When a validation error occurred, ask the ViolationSeverityInterpreter if this validation should result in an
     *  exception <br/>
     * - Execute the afterValidation method of the registered PropertyValidationInterceptor's. (when validation actually
     *  tooks place)
     *
     * {@inheritDoc}
     */
    @Override
    public void beforeGetConvertedValue(FacesContext facesContext, UIComponent uiComponent, Object o, Renderer wrapped)
            throws ConverterException, SkipBeforeInterceptorsException, SkipRendererDelegationException
    {
        Object convertedObject;

        try
        {
            if(wrapped instanceof RendererProxy)
            {
                convertedObject = ((RendererProxy)wrapped).getCachedConvertedValue(facesContext, uiComponent, o);
            }
            else
            {
                convertedObject = wrapped.getConvertedValue(facesContext, uiComponent, o);
            }
        }
        catch (PropertyNotFoundException r)
        {
            this.logger.log(Level.SEVERE, "it seems you are using an invalid binding. " + wrapped.getClass().getName()
                    + ": conversion failed. normally this is >not< a myfaces extval issue!", r);

            throw r;
        }

        setRendererInterceptorProperties(uiComponent);
        
        if(recordProcessedInformation())
        {
            //record user input e.g. for cross-component validation
            for(ProcessedInformationRecorder recorder : ExtValContext.getContext().getProcessedInformationRecorders())
            {
                recorder.recordUserInput(uiComponent, convertedObject);

                logger.finest(recorder.getClass().getName() + " called");
            }
        }

        boolean validateValue = false;
        try
        {
            if(processComponent(uiComponent))
            {
                convertedObject = transformValueForValidation(convertedObject);

                validateValue = validateValue(convertedObject);
                if(validateValue && processBeforeValidation(facesContext, uiComponent, convertedObject))
                {
                    processValidation(facesContext, uiComponent, convertedObject);
                }
            }
        }
        catch (ValidatorException e)
        {
            try
            {
                //ViolationSeverityInterpreter might decide that it isn't an exception
                ExtValUtils.tryToThrowValidatorExceptionForComponent(uiComponent, e.getFacesMessage(), e);
            }
            catch (ValidatorException finalException)
            {
                throw new ConverterException(e.getFacesMessage(), e);
            }
        }
        finally
        {
            if(validateValue)
            {
                processAfterValidation(facesContext, uiComponent, convertedObject);
            }
            resetRendererInterceptorProperties(uiComponent);
        }
    }

    /**
     * Execute the beforeValidation method of the registered PropertyValidationInterceptor's.
     *
     * @param facesContext The JSF Context
     * @param uiComponent The UIComponent which is processed.
     * @param value The value to validate
     * @return true when validation can proceed, false otherwise.
     */
    protected boolean processBeforeValidation(FacesContext facesContext, UIComponent uiComponent, Object value)
    {
        return ExtValUtils.executeGlobalBeforeValidationInterceptors(facesContext, uiComponent, value,
                PropertyInformation.class.getName(), getPropertyInformation(facesContext, uiComponent), getModuleKey());
    }

    /**
     * Execute the afterValidation method of the registered PropertyValidationInterceptor's.
     *
     * @param facesContext The JSF Context
     * @param uiComponent The UIComponent which is processed.
     * @param value The value which has just been validated.
     */
    protected void processAfterValidation(FacesContext facesContext, UIComponent uiComponent, Object value)
    {
        ExtValUtils.executeGlobalAfterValidationInterceptors(facesContext, uiComponent, value,
                PropertyInformation.class.getName(), getPropertyInformation(facesContext, uiComponent), getModuleKey());
    }

    /**
     * Extrancts the {@link PropertyInformation} for the given component.
     *
     * @param facesContext The JSF Context
     * @param uiComponent The UIComponent which is processed.
     * @return the information of the referenced property (e.g. base object, property name, meta-data, ...)
     */
    protected PropertyInformation getPropertyInformation(FacesContext facesContext, UIComponent uiComponent)
    {
        Map<String, Object> properties = getPropertiesForComponentMetaDataExtractor(uiComponent);

        MetaDataExtractor metaDataExtractor = getComponentMetaDataExtractor(properties);

        return metaDataExtractor.extract(facesContext, uiComponent);
    }

    /**
     * Implementations must return the MetaDataExtractor that will perform the extraction of the meta data from the
     * component. The component itself is present in the properties map (it might influence the type of the returned
     * {@link MetaDataExtractor}.
     *
     * @param properties Properties that can be used to determine the extractor which is returned.
     * @return The MetaDataExtractor used for performing the xtraction of the meta data from the component.
     */
    protected abstract MetaDataExtractor getComponentMetaDataExtractor(Map<String, Object> properties);

    /**
     * Converts an empty String to null when the parameter interpretEmptyStringValuesAsNull is set.
     *
     * @param convertedObject  Converted objected
     * @return Adjusted value that should be used from now on as converted value.
     */
    protected Object transformValueForValidation(Object convertedObject)
    {
        if ("".equals(convertedObject) && interpretEmptyStringValuesAsNull())
        {
            return null;
        }

        return convertedObject;
    }

    /**
     * Evaluates if the value should be validated in case it is empty (null or no characters).
     * ExtVal also uses a config parameter introduced by JSF 2 which allows to skip the validation of empty fields.
     *
     * @param convertedObject The converted value.
     * @return true if the given value should be validated, false otherwise
     */
    protected boolean validateValue(Object convertedObject)
    {
        if(isValueToValidateEmpty(convertedObject) && !validateEmptyFields())
        {
            this.logger.fine("empty field validation is deactivated in the web.xml - see: " +
                    "javax.faces.VALIDATE_EMPTY_FIELDS");

            return false;
        }

        return true;
    }

    /**
     * Defines if a value is empty. The definition of empty is that it is null or has no characters in the String value.
     *
     * @param convertedObject The converted value.
     * @return true if the given value is the representation of an empty value
     */
    protected boolean isValueToValidateEmpty(Object convertedObject)
    {
        return convertedObject == null || "".equals(convertedObject);
    }

    /**
     * Uses a config parameter (javax.faces.VALIDATE_EMPTY_FIELDS) which was introduced by JSF 2 for deactivating
     * the validation of empty fields.
     * 
     * @return true if the validation of empty fields is enabled, false otherwise
     */
    protected boolean validateEmptyFields()
    {
        return ExtValUtils.validateEmptyFields();
    }

    /**
     * Uses a config parameter (javax.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL)
     * which was introduced by JSF 2 for converting empty strings to null.
     *
     * @return true if an empty string should be replaced with null (for the validation process), false otherwise
     */
    protected boolean interpretEmptyStringValuesAsNull()
    {
        return ExtValUtils.interpretEmptyStringValuesAsNull();
    }

    /**
     * A concrete implementation has to perform the actual validation of the value.
     *
     * @param facesContext The JSF Context
     * @param uiComponent The UIComponent which is processed.
     * @param convertedObject The (adjusted) converted value.
     */
    protected abstract void processValidation(
            FacesContext facesContext, UIComponent uiComponent, Object convertedObject);

    /**
     * Based on basic rules the method checks if the current component should be processed.
     *
     * @param uiComponent The UIComponent which is processed.
     * @return true if the given component should be processed by the current interceptor, false otherwise
     */
    protected boolean processComponent(UIComponent uiComponent)
    {
        return uiComponent instanceof EditableValueHolder && isValueBindingOfComponentValid(uiComponent);
    }

    /**
     * Returns the ELHelper to be used in the process of the validation. It is cached for performance reasons.
     * @return The ELHelper.
     */
    protected ELHelper getELHelper()
    {
        if(this.elHelper == null)
        {
            this.elHelper = ExtValUtils.getELHelper();
        }
        return this.elHelper;
    }

    private boolean isValueBindingOfComponentValid(UIComponent uiComponent)
    {
        try
        {
            return getELHelper().getPropertyDetailsOfValueBinding(uiComponent) != null;
        }
        catch (Exception e)
        {
            return false;
        }
    }

    private boolean isComponentInitializationDeactivated()
    {
        return ExtValCoreConfiguration.get().deactivateComponentInitialization();
    }

    /**
     * Signals if the converted value should be stored e.g. for the cross-validation process.
     * @return true if the converted value should be recorded, false otherwise
     */
    protected boolean recordProcessedInformation()
    {
        //override if needed
        return false;
    }

    /**
     * Identification of the validation module.
     * A key is just needed if other implementations should be restricted to 1-n special validation modules.
     * 
     * @return Identification of the validation module.
     */
    protected Class getModuleKey()
    {
        //override if needed
        return null;
    }

    /**
     * Create the properties which can be used by a {@link MetaDataExtractor}.
     * By default it adds a key which identifies the current validation-module
     * and the current component (to avoid changes of the api for quite special use-cases).
     *
     * @param uiComponent  The UIComponent which is processed.
     * @return properties used by the selection of the MetaDataExtractor
     */
    protected Map<String, Object> getPropertiesForComponentMetaDataExtractor(UIComponent uiComponent)
    {
        return createProperties(uiComponent);
    }

    /**
     * Create the properties which can be used by a {@link MetaDataExtractor}.
     * Returns the properties which will be made available to interceptors. By default the moduleKey and the UIComponent
     * itself is added.
     *
     * @param uiComponent  The UIComponent which is processed.
     * @return  properties for the interceptors.
     */
    protected Map<String, Object> getInterceptorProperties(UIComponent uiComponent)
    {
        return createProperties(uiComponent);
    }

    private Map<String, Object> createProperties(UIComponent uiComponent)
    {
        Map<String, Object> result = new HashMap<String, Object>();

        if(getModuleKey() != null)
        {
            result.put(ValidationModuleKey.class.getName(), getModuleKey());
        }
        result.put(UIComponent.class.getName(), uiComponent);

        return result;
    }

    /**
     * Stores additional properties for the current process (to avoid api changes).
     *
     * @param uiComponent The UIComponent which is processed.
     */
    private void setRendererInterceptorProperties(UIComponent uiComponent)
    {
        RendererInterceptorPropertyStorage interceptorPropertyStorage = getRendererInterceptorPropertyStorage();

        Map<String, Object> properties = getInterceptorProperties(uiComponent);
        for(Map.Entry<String, Object> entry : properties.entrySet())
        {
            interceptorPropertyStorage.setProperty(entry.getKey(), entry.getValue());
        }
    }

    private void resetRendererInterceptorProperties(UIComponent uiComponent)
    {
        RendererInterceptorPropertyStorage interceptorPropertyStorage = getRendererInterceptorPropertyStorage();

        for(String key : getInterceptorProperties(uiComponent).keySet())
        {
            interceptorPropertyStorage.removeProperty(key);
        }
    }

    private RendererInterceptorPropertyStorage getRendererInterceptorPropertyStorage()
    {
        return ExtValUtils.getStorage(RendererInterceptorPropertyStorage.class,
                RendererInterceptorPropertyStorage.class.getName());
    }
}
