com.astamuse.asta4d.web.form.flow.base.AbstractFormFlowHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.astamuse.asta4d.web.form.flow.base.AbstractFormFlowHandler.java

Source

/*
 * Copyright 2014 astamuse company,Ltd.
 * 
 * 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.astamuse.asta4d.web.form.flow.base;

import java.lang.reflect.Array;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;

import com.astamuse.asta4d.Context;
import com.astamuse.asta4d.data.InjectTrace;
import com.astamuse.asta4d.data.InjectUtil;
import com.astamuse.asta4d.util.annotation.AnnotatedPropertyInfo;
import com.astamuse.asta4d.util.annotation.AnnotatedPropertyUtil;
import com.astamuse.asta4d.web.WebApplicationConfiguration;
import com.astamuse.asta4d.web.WebApplicationContext;
import com.astamuse.asta4d.web.dispatch.RedirectInterceptor;
import com.astamuse.asta4d.web.dispatch.RedirectUtil;
import com.astamuse.asta4d.web.form.CascadeFormUtil;
import com.astamuse.asta4d.web.form.annotation.CascadeFormField;
import com.astamuse.asta4d.web.form.flow.classical.MultiStepFormFlowHandler;
import com.astamuse.asta4d.web.form.validation.FormValidationMessage;
import com.astamuse.asta4d.web.form.validation.FormValidator;
import com.astamuse.asta4d.web.form.validation.JsrValidator;
import com.astamuse.asta4d.web.form.validation.TypeUnMatchValidator;
import com.astamuse.asta4d.web.util.SecureIdGenerator;
import com.astamuse.asta4d.web.util.message.DefaultMessageRenderingHelper;

@SuppressWarnings({ "rawtypes", "unchecked" })
public abstract class AbstractFormFlowHandler<T> {

    private static final String FORM_PRE_DEFINED = "FORM_PRE_DEFINED#" + AbstractFormFlowHandler.class.getName();

    private static final String FORM_EXTRA_DATA = "FORM_EXTRA_DATA#" + AbstractFormFlowHandler.class.getName();

    public static final String PRE_INJECTION_TRACE_INFO = "PRE_INJECTION_TRACE_INFO#"
            + AbstractFormFlowHandler.class.getName();

    private Class<? extends FormProcessData> formProcessDataCls;
    private Class formCls;

    public AbstractFormFlowHandler(Class<T> formCls) {
        this(formCls, SimpleFormProcessData.class);
    }

    public AbstractFormFlowHandler(Class<T> formCls, Class<? extends FormProcessData> formProcessDataCls) {
        this.formCls = formCls;
        this.formProcessDataCls = formProcessDataCls;
    }

    /**
     * Sub classes must tell us the name of first step
     * 
     * @param step
     * @return
     */
    protected abstract String firstStepName();

    /**
     * Sub classes must tell us whether the name of complete step which means to finish current flow
     * 
     * @param step
     * @return
     */
    protected abstract String completeStepName();

    /**
     * Sub classes could override this method to create the initial form data(eg. query from db)
     * 
     * @return
     * @throws Exception
     */
    protected T createInitForm() throws Exception {
        return (T) InjectUtil.retrieveContextDataSetInstance(formCls, FORM_PRE_DEFINED, "");
    }

    /**
     * Convenience for saving some extra data in context in case of being necessary
     * 
     * @param actionInfo
     * @see #getExtraDataFromContext()
     */
    protected <D> void saveExtraDataToContext(D actionInfo) {
        Context.getCurrentThreadContext().setData(FORM_EXTRA_DATA, actionInfo);
    }

    /**
     * Convenience for retrieving some extra data in context in case of being necessary
     * 
     * @param actionInfo
     * @see #saveExtraDataToContext(Object)
     */
    protected <D> D getExtraDataFromContext() {
        return Context.getCurrentThreadContext().getData(FORM_EXTRA_DATA);
    }

    private void savePreDefinedForm(T form) {
        Context.getCurrentThreadContext().setData(FORM_PRE_DEFINED, form);
    }

    /**
     * Sub classes could override this method to translate the returned target step to the actual render target template file path.
     * 
     * @return the render target step name
     * @throws Exception
     */
    protected String handle() throws Exception {
        FormProcessData processData = (FormProcessData) InjectUtil
                .retrieveContextDataSetInstance(formProcessDataCls, "not-exist-IntelligentFormProcessData", "");

        String traceData = processData.getStepTraceData();

        if (processData.getStepExit() != null) {
            clearSavedTraceMap(traceData);
            return null;
        }

        Map<String, Object> traceMap;

        if (StringUtils.isEmpty(traceData)) {
            traceMap = new HashMap<>();
        } else {
            traceMap = restoreTraceMap(traceData);
            if (traceMap == null) {
                traceMap = new HashMap<>();
            }
        }

        String currentStep = processData.getStepCurrent();
        // the first time access without existing input data or saved tracemap could not be retrieved(usually due to timeout)
        if (currentStep == null) {
            currentStep = FormFlowConstants.FORM_STEP_BEFORE_FIRST;
            savePreDefinedForm(createInitForm());
        }

        T form = retrieveFormInstance(traceMap, currentStep);

        traceMap.put(currentStep, form);

        String renderTargetStep = null;
        CommonFormResult formResult = null;

        if (processData.getStepBack() != null) {
            renderTargetStep = processData.getStepBack();
            if (removeCurrentStepDataFromTraceMapWhenStepBack(currentStep, renderTargetStep)) {
                traceMap.remove(currentStep);
            }
            passDataToSnippet(currentStep, renderTargetStep, traceMap);
        } else {
            if (FormFlowConstants.FORM_STEP_BEFORE_FIRST.equals(currentStep)) {
                renderTargetStep = firstStepName();
            } else {
                // since the init step will not enter this branch, so the sub classes which override the process method could retrieve
                // current step without any concern about null pointer exception.
                formResult = process(processData, form);
                if (formResult == CommonFormResult.SUCCESS) {
                    renderTargetStep = processData.getStepSuccess();
                } else {
                    renderTargetStep = processData.getStepFailed();
                }
            }
            passDataToSnippet(currentStep, renderTargetStep, traceMap);
        }

        if (completeStepName().equalsIgnoreCase(renderTargetStep)) {
            WebApplicationContext context = WebApplicationContext.getCurrentThreadWebApplicationContext();
            String newTraceData = context.getData(FormFlowConstants.FORM_STEP_TRACE_MAP_STR);
            clearSavedTraceMap(newTraceData);
        }

        return renderTargetStep;

    }

    /**
     * Sub classes can override this method to customize how to retrieve the form instance by step information.
     * 
     * @param traceMap
     * @param currentStep
     * @return
     */
    protected T retrieveFormInstance(Map<String, Object> traceMap, String currentStep) {
        // The subclass may override this method to retrieving form instance by various ways but we will always generate an instance from
        // the context since we have no idea about the concrete logic of sub classes.
        return generateFormInstanceFromContext();
    }

    /**
     * Sub classes can override this method to do some interception around form instance generation, especially some post processes.
     * <p>
     * <b>NOTE:</b> DO NOT replace this method completely at sub class, if you want to do some customized form retrieving, override the
     * method {@link #retrieveFormInstance(Map, String)} instead.
     * 
     * @return
     */
    protected T generateFormInstanceFromContext() {
        try {

            final T form = (T) InjectUtil.retrieveContextDataSetInstance(formCls, FORM_PRE_DEFINED, "");
            List<AnnotatedPropertyInfo> list = AnnotatedPropertyUtil.retrieveProperties(formCls);
            Context currentContext = Context.getCurrentThreadContext();
            for (final AnnotatedPropertyInfo field : list) {
                CascadeFormField cff = field.getAnnotation(CascadeFormField.class);
                if (cff != null) {
                    if (field.retrieveValue(form) != null) {
                        continue;
                    }

                    if (StringUtils.isEmpty(cff.arrayLengthField())) {
                        continue;
                    }

                    AnnotatedPropertyInfo arrayLengthField = AnnotatedPropertyUtil.retrievePropertyByName(formCls,
                            cff.arrayLengthField());
                    if (arrayLengthField == null) {
                        throw new NullPointerException(
                                "specified array length field [" + cff.arrayLengthField() + "] was not found");
                    }

                    Integer len = (Integer) arrayLengthField.retrieveValue(form);
                    if (len == null) {
                        // throw new NullPointerException("specified array length field [" + cff.arrayLengthField() + "] is null");
                        len = 0;
                    }

                    final Object[] array = (Object[]) Array.newInstance(field.getType().getComponentType(), len);
                    for (int i = 0; i < len; i++) {
                        final int seq = i;
                        Context.with(new DelatedContext(currentContext) {
                            protected String convertKey(String scope, String key) {
                                if (scope.equals(WebApplicationContext.SCOPE_QUERYPARAM)) {
                                    return rewriteArrayIndexPlaceHolder(key, seq);
                                } else {
                                    return key;
                                }
                            }
                        }, new Runnable() {
                            @Override
                            public void run() {
                                try {
                                    Object subform = field.getType().getComponentType().newInstance();
                                    InjectUtil.injectToInstance(subform);
                                    Array.set(array, seq, subform);
                                } catch (Exception e) {
                                    throw new RuntimeException(e);
                                }
                            }
                        });// end runnable and context.with
                    } // end for loop

                    field.assginValue(form, array);
                }
            }
            return form;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Sub classes can override this method to supply a customized array index placeholder mechanism.
     * 
     * @param s
     * @param seq
     * @return
     * @see AbstractFormFlowSnippet#rewriteArrayIndexPlaceHolder(String, int)
     */
    protected String rewriteArrayIndexPlaceHolder(String s, int seq) {
        return CascadeFormUtil.rewriteArrayIndexPlaceHolder(s, seq);
    }

    /**
     * <b>Note</b>: In fact, we should not save the trace map when some steps such as init step to avoid unnecessary memory usage, thus we
     * call the {@link #skipSaveTraceMap(String, String, Map)} to decide save or not.
     * 
     * In other words ,the sub class have the responsibility to tell us save or not by overriding the method
     * {@link #skipSaveTraceMap(String, String, Map)}.
     * 
     * @param currentStep
     * @param renderTargetStep
     * @param traceMap
     * @return
     */
    protected String saveTraceMap(String currentStep, String renderTargetStep, Map<String, Object> traceMap) {
        if (skipSaveTraceMap(currentStep, renderTargetStep, traceMap)) {
            return "";
        } else {
            String id = SecureIdGenerator.createEncryptedURLSafeId();
            WebApplicationConfiguration.getWebApplicationConfiguration().getTimeoutDataManager().put(id, traceMap,
                    cachedTraceMapLivingTimeInMilliSeconds());
            return id;
        }
    }

    /**
     * The sub classes could tell us whether we should remove the current step data from trace map when we back to the previous step. By
     * default, it returns true always.
     * 
     * @param currentStep
     * @param renderTargetStep
     * @return
     */
    protected boolean removeCurrentStepDataFromTraceMapWhenStepBack(String currentStep, String renderTargetStep) {
        return true;
    }

    /**
     * Since we are lacking of necessary step information to judge if we should save or not, we only do the basic judgment for the init
     * step. The sub class have the responsibility to handle other cases.
     * 
     * @see MultiStepFormFlowHandler#skipSaveTraceMap(String, String, Map)
     * 
     */
    protected boolean skipSaveTraceMap(String currentStep, String renderTargetStep, Map<String, Object> traceMap) {
        if (FormFlowConstants.FORM_STEP_BEFORE_FIRST.equals(currentStep)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * 
     * retrieve the stored trace map.
     * 
     * @param traceData
     * @return
     * @see #saveTraceMap(String, String, Map)
     */
    protected Map<String, Object> restoreTraceMap(String traceData) {
        return WebApplicationConfiguration.getWebApplicationConfiguration().getTimeoutDataManager().get(traceData);
    }

    /**
     * 
     * clear the stored trace map.
     * 
     * @param traceData
     * @see #saveTraceMap(String, String, Map)
     */
    protected void clearSavedTraceMap(String traceData) {
        if (StringUtils.isNotEmpty(traceData)) {
            WebApplicationConfiguration.getWebApplicationConfiguration().getTimeoutDataManager().get(traceData);
        }
    }

    /**
     * Sub classes can override this method to customize how long the form flow trace data will keep alive.
     * <p>
     * The default value is 30 minutes.
     * 
     * @return
     */
    protected long cachedTraceMapLivingTimeInMilliSeconds() {
        // 30 minutes
        return 30 * 60 * 1000L;
    }

    /**
     * Sub classes can override this method to customize how to pass data to snippet.
     * <p>
     * This method will retrieve the form of render target from trace map and if it does not exists, the form of current step will be used.
     * By default, the render target form is not set so the current step form will be used always. The sub classes could override this
     * method to store the render target form to trace map before calling super.
     * 
     * @param currentStep
     * @param renderTargetStep
     * @param traceMap
     */
    protected void passDataToSnippet(String currentStep, String renderTargetStep, Map<String, Object> traceMap) {
        T form = (T) traceMap.get(renderTargetStep);
        if (form == null) {
            form = (T) traceMap.get(currentStep);
            traceMap.put(renderTargetStep, form);
        }
        WebApplicationContext context = WebApplicationContext.getCurrentThreadWebApplicationContext();

        boolean byFlash = passDataToSnippetByFlash(currentStep, renderTargetStep, form);

        String traceData = saveTraceMap(currentStep, renderTargetStep, traceMap);

        passData(context, byFlash, FormFlowConstants.FORM_STEP_TRACE_MAP, traceMap);
        passData(context, byFlash, FormFlowConstants.FORM_STEP_TRACE_MAP_STR, traceData);
        passData(context, byFlash, FormFlowConstants.FORM_STEP_RENDER_TARGET, renderTargetStep);

        if (byFlash) {

            RedirectUtil.registerRedirectInterceptor(this.getClass().getName() + "#passDataToSnippet",
                    new RedirectInterceptor() {
                        @Override
                        public void beforeRedirect() {
                            RedirectUtil.addFlashScopeData(PRE_INJECTION_TRACE_INFO,
                                    InjectTrace.retrieveTraceList());
                        }

                        @Override
                        public void afterRedirectDataRestore() {
                            List list = (List) Context.getCurrentThreadContext()
                                    .getData(WebApplicationContext.SCOPE_FLASH, PRE_INJECTION_TRACE_INFO);
                            InjectTrace.restoreTraceList(list);

                        }
                    });

            // used by clearSavedTraceMap
            context.setData(FormFlowConstants.FORM_STEP_TRACE_MAP_STR, traceData);
        }
    }

    private void passData(WebApplicationContext context, boolean byFlash, String key, Object data) {
        if (byFlash) {
            RedirectUtil.addFlashScopeData(key, data);
        } else {
            context.setData(key, data);
        }
    }

    /**
     * Sub classes should tell us whether we should pass data to snippet via flash scope. The default is false.
     * 
     * @param currentStep
     * @param renderTargetStep
     * @param form
     * @param result
     * @return
     */
    protected boolean passDataToSnippetByFlash(String currentStep, String renderTargetStep, T form) {
        return false;
    }

    /**
     * The default process will only call the {@link #processValidation(Object)} and sub classes can override this method to add extra
     * process logics such as updating form when validation succeeds.
     * 
     * @param processData
     * @param form
     * @return
     */
    protected CommonFormResult process(FormProcessData processData, T form) {
        return processValidation(processData, form);
    }

    /**
     * Sub classes can override this method to customize how to handle the validation result
     * 
     * @param form
     * @return
     */
    protected CommonFormResult processValidation(FormProcessData processData, Object form) {
        List<FormValidationMessage> validationMesssages = validate(form);
        if (validationMesssages.isEmpty()) {
            return CommonFormResult.SUCCESS;
        } else {
            for (FormValidationMessage msg : validationMesssages) {
                outputValidationMessage(msg);
            }
            return CommonFormResult.FAILED;
        }
    }

    /**
     * Sub classes can override this method to customize how to output validation messages
     * 
     * @param msg
     */
    protected void outputValidationMessage(FormValidationMessage msg) {
        DefaultMessageRenderingHelper.getConfiguredInstance().err("#" + msg.getFieldName() + "-err-msg",
                msg.getMessage());
    }

    /**
     * 
     * Sub classes can override this method to supply customized validation mechanism.
     * 
     * @param form
     * @return
     */
    protected List<FormValidationMessage> validate(Object form) {
        List<FormValidationMessage> validationMessages = new LinkedList<>();

        Set<String> fieldNameSet = new HashSet<String>();

        List<FormValidationMessage> typeMessages = getTypeUnMatchValidator().validate(form);
        for (FormValidationMessage message : typeMessages) {
            validationMessages.add(message);
            fieldNameSet.add(message.getFieldName());
        }

        List<FormValidationMessage> valueMessages = getValueValidator().validate(form);

        // there may be a not null/empty value validation error for the fields which has been validated as type unmatch, we simply remove
        // them.

        for (FormValidationMessage message : valueMessages) {
            if (!fieldNameSet.contains(message.getFieldName())) {
                validationMessages.add(message);
            }
        }

        return validationMessages;
    }

    /**
     * Sub classes can override this method to supply a customized type unmatch validator
     * 
     * @return
     */
    protected FormValidator getTypeUnMatchValidator() {
        return new TypeUnMatchValidator();
    }

    /**
     * Sub classes can override this method to supply a customized value validator
     * 
     * @return
     */
    protected FormValidator getValueValidator() {
        return new JsrValidator();
    }

}