com.hemou.component.saripaar.Validator.java Source code

Java tutorial

Introduction

Here is the source code for com.hemou.component.saripaar.Validator.java

Source

/*
 * Copyright (C) 2012 Mobs and Geeks
 *
 * 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 com.hemou.component.saripaar;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import android.app.Activity;
import android.content.res.Resources;
import android.graphics.Color;
import android.os.AsyncTask;
import android.support.v4.app.Fragment;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;

import com.hemou.android.R;
import com.hemou.component.saripaar.annotation.Checked;
import com.hemou.component.saripaar.annotation.ConfirmPassword;
import com.hemou.component.saripaar.annotation.Email;
import com.hemou.component.saripaar.annotation.IpAddress;
import com.hemou.component.saripaar.annotation.NumberRule;
import com.hemou.component.saripaar.annotation.Password;
import com.hemou.component.saripaar.annotation.Regex;
import com.hemou.component.saripaar.annotation.Required;
import com.hemou.component.saripaar.annotation.Select;
import com.hemou.component.saripaar.annotation.TextRule;
import com.hemou.component.saripaar.annotation.URL;

import de.keyboardsurfer.android.widget.crouton.Configuration;
import de.keyboardsurfer.android.widget.crouton.Crouton;
import de.keyboardsurfer.android.widget.crouton.Style;

/**
 * A processor that checks all the {@link Rule}s against their {@link View}s.
 * 
 * @author Ragunath Jawahar <rj@mobsandgeeks.com>
 */
public class Validator {

    /**
     * Interface definition for a callback to be invoked when {@code validate()}
     * is called.
     */
    public interface ValidationListener {

        /**
         * Called when all the {@link Rule}s added to this Validator are valid.
         */
        public void onValidationSucceeded();

        /**
         * Called if any of the {@link Rule}s fail.
         * 
         * @param failedView
         *            The {@link View} that did not pass validation.
         * @param failedRule
         *            The failed {@link Rule} associated with the {@link View}.
         */
        public void onValidationFailed(Object failedView, Rule<?> failedRule);
    }

    // Debug
    static final String TAG = Validator.class.getSimpleName();
    static final boolean DEBUG = false;
    private Activity mHost;
    private Object mController;
    private boolean mAnnotationsProcessed;
    private List<ViewRulePair> mViewsAndRules;
    private Map<String, Object> mProperties;
    private AsyncTask<Void, Void, ViewRulePair> mAsyncValidationTask;
    private ValidationListener mValidationListener;

    private Crouton mTip;

    /**
     * Private constructor. Cannot be instantiated.
     */
    private Validator() {
        mAnnotationsProcessed = false;
        mViewsAndRules = new ArrayList<Validator.ViewRulePair>();
        mProperties = new HashMap<String, Object>();
    }

    private Activity getHost(Object controller) {
        if (controller == null)
            throw new IllegalArgumentException("The parameter of 'controller' cannot be null!");
        if (controller instanceof Activity)
            return (Activity) controller;
        if (controller instanceof Fragment)
            return ((Fragment) controller).getActivity();
        if (controller instanceof android.app.Fragment)
            return ((android.app.Fragment) controller).getActivity();
        throw new IllegalArgumentException(
                "The parameter of 'controller' should be instance of Activity or android.support.v4.app.Fragment");
    }

    /**
     * Creates a new {@link Validator}.
     * 
     * @param controller
     *            The instance that holds references to the Views that are being
     *            validated. Usually an {@code Activity} or a {@code Fragment}.
     *            Also accepts controller instances that have annotated
     *            {@code View} references.
     */
    public Validator(Object controller) {
        this();
        if (controller == null) {
            throw new IllegalArgumentException("'controller' cannot be null");
        }
        mController = controller;
        mHost = getHost(mController);
        _mResource = mHost.getResources();
        _setEditTextTip();
    }

    private Resources _mResource;

    /**
     * Add a {@link View} and it's associated {@link Rule} to the Validator.
     * 
     * @param view
     *            The {@link View} to be validated.
     * @param rule
     *            The {@link Rule} associated with the view.
     * 
     * @throws IllegalArgumentException
     *             If {@code rule} is {@code null}.
     */
    public void put(View view, Rule<?> rule) {
        if (rule == null) {
            throw new IllegalArgumentException("'rule' cannot be null");
        }

        mViewsAndRules.add(new ViewRulePair(view, rule));
        // if (rule != null && !TextUtils.isEmpty(rule.getFailureMessage()))
        // mViewsAndRules.add(new ViewRulePair(view, rule));
    }

    /**
     * Convenience method for adding multiple {@link Rule}s for a single
     * {@link View}.
     * 
     * @param view
     *            The {@link View} to be validated.
     * @param rules
     *            {@link List} of {@link Rule}s associated with the view.
     * 
     * @throws IllegalArgumentException
     *             If {@code rules} is {@code null}.
     */
    public void put(View view, List<Rule<?>> rules) {
        if (rules == null) {
            throw new IllegalArgumentException("\'rules\' cannot be null");
        }

        for (Rule<?> rule : rules) {
            put(view, rule);
        }
    }

    /**
     * Convenience method for adding just {@link Rule}s to the Validator.
     * 
     * @param rule
     *            A {@link Rule}, usually composite or custom.
     */
    public void put(Rule<?> rule) {
        put(null, rule);
    }

    /**
     * Validate all the {@link Rule}s against their {@link View}s.
     * 
     * @throws IllegalStateException
     *             If a {@link ValidationListener} is not registered.
     */
    public synchronized void validate() {
        // Log.v(TAG, "validate() got called!...");
        if (mValidationListener == null) {
            throw new IllegalStateException(
                    "Set a " + ValidationListener.class.getSimpleName() + " before attempting to validate.");
        }

        // +start

        // +end

        ViewRulePair failedViewRulePair = validateAllRules();
        if (mTip != null)
            mTip.hide();
        if (failedViewRulePair == null) {
            mValidationListener.onValidationSucceeded();
        } else {
            showTip(failedViewRulePair.rule.getFailureMessage());
            Log.d(TAG, "failedViewRulePair" + failedViewRulePair);
            mValidationListener.onValidationFailed(failedViewRulePair.view, failedViewRulePair.rule);
        }
    }

    private void showTip(String mess) {
        mTip = Crouton.makeText(mHost, mess, Style.ALERT);
        mTip.setConfiguration(new Configuration.Builder().setDuration(Configuration.DURATION_LONG).build()).show();
    }

    /**
     * Asynchronously validates all the {@link Rule}s against their {@link View}
     * s. Subsequent calls to this method will cancel any pending asynchronous
     * validations and start a new one.
     * 
     * @throws IllegalStateException
     *             If a {@link ValidationListener} is not registered.
     */
    public void validateAsync() {
        if (mValidationListener == null) {
            throw new IllegalStateException(
                    "Set a " + ValidationListener.class.getSimpleName() + " before attempting to validate.");
        }

        // Cancel the existing task
        if (mAsyncValidationTask != null) {
            mAsyncValidationTask.cancel(true);
            mAsyncValidationTask = null;
        }

        // Start a new one ;)
        mAsyncValidationTask = new AsyncTask<Void, Void, ViewRulePair>() {

            @Override
            protected ViewRulePair doInBackground(Void... params) {
                return validateAllRules();
            }

            @Override
            protected void onPostExecute(ViewRulePair pair) {
                Log.d(TAG, "post validate task...:" + pair);
                if (mTip != null)
                    mTip.hide();
                if (pair == null) {
                    mValidationListener.onValidationSucceeded();
                } else {
                    showTip(pair.rule.getFailureMessage());
                    mValidationListener.onValidationFailed(pair.view, pair.rule);
                }

                mAsyncValidationTask = null;
            }

            @Override
            protected void onCancelled() {
                mAsyncValidationTask = null;
            }
        };

        mAsyncValidationTask.execute((Void[]) null);
    }

    /**
     * Used to find if the asynchronous validation task is running, useful only
     * when you run the Validator in asynchronous mode using the
     * {@code validateAsync} method.
     * 
     * @return True if the asynchronous task is running, false otherwise.
     */
    public boolean isValidating() {
        return mAsyncValidationTask != null && mAsyncValidationTask.getStatus() != AsyncTask.Status.FINISHED;
    }

    /**
     * Cancels the asynchronous validation task if running, useful only when you
     * run the Validator in asynchronous mode using the {@code validateAsync}
     * method.
     * 
     * @return True if the asynchronous task was cancelled.
     */
    public boolean cancelAsync() {
        boolean cancelled = false;
        if (mAsyncValidationTask != null) {
            cancelled = mAsyncValidationTask.cancel(true);
            mAsyncValidationTask = null;
        }

        return cancelled;
    }

    /**
     * Returns the callback registered for this Validator.
     * 
     * @return The callback, or null if one is not registered.
     */
    public ValidationListener getValidationListener() {
        return mValidationListener;
    }

    /**
     * Register a callback to be invoked when {@code validate()} is called.
     * 
     * @param validationListener
     *            The callback that will run.
     */
    public void setValidationListener(ValidationListener validationListener) {
        this.mValidationListener = validationListener;
    }

    /**
     * Updates a property value if it exists, else creates a new one.
     * 
     * @param name
     *            The property name.
     * @param value
     *            Value of the property.
     * 
     * @throws IllegalArgumentException
     *             If {@code name} is {@code null}.
     */
    public void setProperty(String name, Object value) {
        if (name == null) {
            throw new IllegalArgumentException("\'name\' cannot be null");
        }

        mProperties.put(name, value);
    }

    /**
     * Retrieves the value of the given property.
     * 
     * @param name
     *            The property name.
     * 
     * @throws IllegalArgumentException
     *             If {@code name} is {@code null}.
     * 
     * @return Value of the property or {@code null} if the property does not
     *         exist.
     */
    public Object getProperty(String name) {
        if (name == null) {
            throw new IllegalArgumentException("\'name\' cannot be null");
        }

        return mProperties.get(name);
    }

    /**
     * Removes the property from this Validator.
     * 
     * @param name
     *            The property name.
     * 
     * @return The value of the removed property or {@code null} if the property
     *         was not found.
     */
    public Object removeProperty(String name) {
        return name != null ? mProperties.remove(name) : null;
    }

    /**
     * Checks if the specified property exists in this Validator.
     * 
     * @param name
     *            The property name.
     * 
     * @return True if the property exists.
     */
    public boolean containsProperty(String name) {
        return name != null ? mProperties.containsKey(name) : false;
    }

    /**
     * Removes all properties from this Validator.
     */
    public void removeAllProperties() {
        mProperties.clear();
    }

    /**
     * Removes all the rules for the matching {@link View}
     * 
     * @param view
     *            The {@code View} whose rules must be removed.
     */
    public void removeRulesFor(View view) {
        if (view == null) {
            throw new IllegalArgumentException("'view' cannot be null");
        }

        int index = 0;
        while (index < mViewsAndRules.size()) {
            ViewRulePair pair = mViewsAndRules.get(index);
            if (pair.view == view) {
                mViewsAndRules.remove(index);
                continue;
            }

            index++;
        }
    }

    /**
     * Validates all rules added to this Validator.
     * 
     * @return {@code null} if all {@code Rule}s are valid, else returns the
     *         failed {@code ViewRulePair}.
     */
    private ViewRulePair validateAllRules() {
        Log.v(TAG, "validateAllRules() got called!...");
        if (!mAnnotationsProcessed) {
            createRulesFromAnnotations(getSaripaarAnnotatedFields());
            mAnnotationsProcessed = true;
        }

        if (mViewsAndRules.size() == 0) {
            Log.i(TAG, "No rules found. Passing validation by default.");
            return null;
        }

        ViewRulePair failedViewRulePair = null;
        for (ViewRulePair pair : mViewsAndRules) {
            if (pair == null)
                continue;

            // Validate views only if they are visible and enabled
            if (pair.view != null) {
                if (!pair.view.isShown() || !pair.view.isEnabled())
                    continue;
            }

            if (!pair.rule.isValid(pair.view)) {
                failedViewRulePair = pair;
                break;
            }
        }

        return failedViewRulePair;
    }

    private void createRulesFromAnnotations(List<AnnotationFieldPair> annotationFieldPairs) {
        TextView passwordTextView = null;
        TextView confirmPasswordTextView = null;

        for (AnnotationFieldPair pair : annotationFieldPairs) {
            // Password
            if (pair.annotation.annotationType().equals(Password.class)) {
                if (passwordTextView == null) {
                    passwordTextView = (TextView) getView(pair.field);
                } else {
                    throw new IllegalStateException(
                            "You cannot annotate " + "two fields in the same Activity with @Password.");
                }
            }

            // Confirm password
            if (pair.annotation.annotationType().equals(ConfirmPassword.class)) {
                if (passwordTextView == null) {
                    throw new IllegalStateException(
                            "A @Password annotated field is required " + "before you can use @ConfirmPassword.");
                } else if (confirmPasswordTextView != null) {
                    throw new IllegalStateException(
                            "You cannot annotate " + "two fields in the same Activity with @ConfirmPassword.");
                } else if (confirmPasswordTextView == null) {
                    confirmPasswordTextView = (TextView) getView(pair.field);
                }
            }

            // Others
            ViewRulePair viewRulePair = null;
            if (pair.annotation.annotationType().equals(ConfirmPassword.class)) {
                viewRulePair = getViewAndRule(pair.field, pair.annotation, passwordTextView);
            } else {
                viewRulePair = getViewAndRule(pair.field, pair.annotation);
            }
            if (viewRulePair != null) {
                if (DEBUG) {
                    Log.d(TAG, String.format("Added @%s rule for %s.",
                            pair.annotation.annotationType().getSimpleName(), pair.field.getName()));
                }
                mViewsAndRules.add(viewRulePair);
                // if (viewRulePair != null
                // && !TextUtils.isEmpty(viewRulePair.rule
                // .getFailureMessage()))
                // mViewsAndRules.add(viewRulePair);
            }
        }
    }

    private ViewRulePair getViewAndRule(Field field, Annotation annotation, Object... params) {
        View view = getView(field);
        if (view == null) {
            Log.w(TAG, String.format("Your %s - %s is null. Please check your field assignment(s).",
                    field.getType().getSimpleName(), field.getName()));
            return null;
        }

        Rule<?> rule = null;
        if (params != null && params.length > 0) {
            rule = AnnotationRuleFactory.getRule(field, view, annotation, params);
        } else {
            rule = AnnotationRuleFactory.getRule(field, view, annotation);
        }

        return rule != null ? new ViewRulePair(view, rule) : null;
    }

    private View getView(Field field) {
        try {
            field.setAccessible(true);
            Object instance = mController;

            return (View) field.get(instance);
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }

    private List<Field> _allViewFields = null;

    private List<Field> _getEverCalculateAllViewFields() {

        if (_allViewFields == null)
            _allViewFields = new ArrayList<Field>(getAllViewFields());
        return _allViewFields;
    }

    private void _setEditTextTip() {
        List<Field> viewFields = _getEverCalculateAllViewFields();
        for (Field field : viewFields) {
            Annotation[] annotations = field.getAnnotations();
            if (annotations == null || annotations.length == 0) {
                continue;
            } else
                for (Annotation annotation : annotations) {
                    if (annotation.annotationType().equals(TextRule.class)) {
                        TextRule textRule = (TextRule) annotation;
                        int tip;
                        if ((tip = textRule.messageResId()) != 0) {
                            final int mMax = textRule.maxLength();
                            final int mMin = textRule.minLength();
                            if (mMin > mMax)
                                throw new IllegalArgumentException(
                                        "Illegal attribute on @TextRule annotation type:[minLength > maxLength]");

                            EditText editText = (EditText) getView(field);
                            editText.addTextChangedListener(new _ListenerOnTextSize(textRule, tip));
                        }
                    }
                }
        }
    }

    private List<AnnotationFieldPair> getSaripaarAnnotatedFields() {
        List<AnnotationFieldPair> annotationFieldPairs = new ArrayList<AnnotationFieldPair>();
        List<Field> fieldsWithAnnotations = getViewFieldsWithAnnotations();

        for (Field field : fieldsWithAnnotations) {
            Annotation[] annotations = field.getAnnotations();
            for (Annotation annotation : annotations) {
                if (isSaripaarAnnotation(annotation)) {
                    if (DEBUG) {
                        Log.d(TAG, String.format("%s %s is annotated with @%s", field.getType().getSimpleName(),
                                field.getName(), annotation.annotationType().getSimpleName()));
                    }
                    annotationFieldPairs.add(new AnnotationFieldPair(annotation, field));
                }
            }
        }

        Collections.sort(annotationFieldPairs, new AnnotationFieldPairCompartor());

        return annotationFieldPairs;
    }

    private List<Field> getViewFieldsWithAnnotations() {
        List<Field> fieldsWithAnnotations = new ArrayList<Field>();
        List<Field> viewFields = getAllViewFields();
        for (Field field : viewFields) {
            Annotation[] annotations = field.getAnnotations();
            if (annotations == null || annotations.length == 0) {
                continue;
            }
            fieldsWithAnnotations.add(field);
        }
        return fieldsWithAnnotations;
    }

    private List<Field> getAllViewFields() {
        List<Field> viewFields = new ArrayList<Field>();

        // Declared fields
        Class<?> superClass = null;
        if (mController != null) {
            viewFields.addAll(getDeclaredViewFields(mController.getClass()));
            superClass = mController.getClass().getSuperclass();
        }

        // Inherited fields
        while (superClass != null && !superClass.equals(Object.class)) {
            List<Field> declaredViewFields = getDeclaredViewFields(superClass);
            if (declaredViewFields.size() > 0) {
                viewFields.addAll(declaredViewFields);
            }
            superClass = superClass.getSuperclass();
        }

        return viewFields;
    }

    private List<Field> getDeclaredViewFields(Class<?> clazz) {
        List<Field> viewFields = new ArrayList<Field>();
        Field[] declaredFields = clazz.getDeclaredFields();
        for (Field f : declaredFields) {
            if (View.class.isAssignableFrom(f.getType())) {
                viewFields.add(f);
            }
        }
        return viewFields;
    }

    private class _ListenerOnTextSize implements TextWatcher {

        final int mMax;
        final int mMin;
        final TextView mTip;
        final int defaultColor;
        private final boolean mTrim;

        private final TextRule mTextRule;

        public _ListenerOnTextSize(TextRule textRule, int tip) {
            mTextRule = textRule;

            mMax = mTextRule.maxLength();
            mMin = mTextRule.minLength();
            mTip = (TextView) mHost.findViewById(tip);
            mTrim = mTextRule.trim();
            defaultColor = mTip.getTextColors().getDefaultColor();
        }

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
        }

        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        }

        @Override
        public void afterTextChanged(Editable s) {

            int len = mTrim ? s.toString().trim().length() : s.length();
            int tem;
            if (mMax != Integer.MAX_VALUE && len > mMax) {

                mTip.setTextColor(Color.RED);
                mTip.setText(String
                        .format(_mResource.getQuantityString(R.plurals.text_size_exceed, tem = (len - mMax)), tem));
            }

            else if (len < mMin) {
                mTip.setTextColor(Color.RED);
                mTip.setText(String
                        .format(_mResource.getQuantityString(R.plurals.text_size_lack, tem = (mMin - len), tem)));
            } else if (mMax - len < 5) {
                mTip.setTextColor(Color.GRAY);
                mTip.setText(String
                        .format(_mResource.getQuantityString(R.plurals.text_size_remain, tem = (mMax - len), tem)));
            } else {
                mTip.setTextColor(defaultColor);
                mTip.setText("");
            }
        }
    }

    private boolean isSaripaarAnnotation(Annotation annotation) {
        Class<?> annotationType = annotation.annotationType();
        /*
         * Log.v(TAG,
         * "isSaripaarAnnotation() got called!...annotationType.equals(UnNullable.class)"
         * + (annotationType.equals(UnNullable.class)));
         */
        return annotationType.equals(Checked.class) || annotationType.equals(ConfirmPassword.class)
                || annotationType.equals(Email.class) || annotationType.equals(URL.class)
                || annotationType.equals(IpAddress.class) || annotationType.equals(NumberRule.class)
                || annotationType.equals(Password.class) || annotationType.equals(Regex.class)
                || annotationType.equals(Required.class) || annotationType.equals(Select.class)
                || annotationType.equals(TextRule.class);
    }

    private class ViewRulePair {
        public View view;
        public final Rule rule;

        public ViewRulePair(View view, Rule<?> rule) {
            if (rule == null)
                throw new IllegalArgumentException();
            this.view = view;
            this.rule = rule;
        }

        @Override
        public String toString() {

            return "{" + (view == null ? "" : "view:" + view.getClass().getSimpleName() + ",") + "rule:"
                    + rule.toString() + "}";
        }
    }

    private class AnnotationFieldPair {
        public Annotation annotation;
        public Field field;

        public AnnotationFieldPair(Annotation annotation, Field field) {
            this.annotation = annotation;
            this.field = field;
        }
    }

    private class AnnotationFieldPairCompartor implements Comparator<AnnotationFieldPair> {

        @Override
        public int compare(AnnotationFieldPair lhs, AnnotationFieldPair rhs) {
            int lhsOrder = getAnnotationOrder(lhs.annotation);
            int rhsOrder = getAnnotationOrder(rhs.annotation);
            return lhsOrder < rhsOrder ? -1 : lhsOrder == rhsOrder ? 0 : 1;
        }

        private int getAnnotationOrder(Annotation annotation) {
            Class<?> annotatedClass = annotation.annotationType();
            if (annotatedClass.equals(Checked.class)) {
                return ((Checked) annotation).order();

            } else if (annotatedClass.equals(ConfirmPassword.class)) {
                return ((ConfirmPassword) annotation).order();

            } else if (annotatedClass.equals(Email.class)) {
                return ((Email) annotation).order();
            } else if (annotatedClass.equals(URL.class)) {
                return ((URL) annotation).order();
            } else if (annotatedClass.equals(IpAddress.class)) {
                return ((IpAddress) annotation).order();

            } else if (annotatedClass.equals(NumberRule.class)) {
                return ((NumberRule) annotation).order();

            } else if (annotatedClass.equals(Password.class)) {
                return ((Password) annotation).order();

            } else if (annotatedClass.equals(Regex.class)) {
                return ((Regex) annotation).order();

            } else if (annotatedClass.equals(Required.class)) {
                return ((Required) annotation).order();

            } else if (annotatedClass.equals(Select.class)) {
                return ((Select) annotation).order();

            } else if (annotatedClass.equals(TextRule.class)) {
                return ((TextRule) annotation).order();

            } else {
                throw new IllegalArgumentException(
                        String.format("%s is not a Saripaar annotation", annotatedClass.getName()));
            }
        }
    }

}