org.amplafi.flow.impl.FlowStateImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.amplafi.flow.impl.FlowStateImpl.java

Source

/*
 * 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 org.amplafi.flow.impl;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;

import static com.sworddance.util.CUtilities.*;

import org.amplafi.flow.Flow;
import org.amplafi.flow.FlowActivity;
import org.amplafi.flow.FlowActivityImplementor;
import org.amplafi.flow.FlowActivityPhase;
import org.amplafi.flow.FlowImplementor;
import org.amplafi.flow.FlowManagement;
import org.amplafi.flow.FlowPropertyDefinition;
import org.amplafi.flow.FlowState;
import org.amplafi.flow.FlowStateLifecycle;
import org.amplafi.flow.FlowStepDirection;
import org.amplafi.flow.FlowValueMapKey;
import org.amplafi.flow.FlowValuesMap;
import org.amplafi.flow.flowproperty.FlowPropertyDefinitionBuilder;
import org.amplafi.flow.flowproperty.FlowPropertyDefinitionImplementor;
import org.amplafi.flow.flowproperty.FlowPropertyProvider;
import org.amplafi.flow.flowproperty.FlowPropertyValueChangeListener;
import org.amplafi.flow.flowproperty.InvalidatingFlowPropertyValueChangeListener;
import org.amplafi.flow.flowproperty.PropertyUsage;
import org.amplafi.flow.validation.FlowValidationException;
import org.amplafi.flow.validation.FlowValidationResult;
import org.amplafi.flow.validation.ReportAllValidationResult;
import com.sworddance.util.NotNullIterator;
import com.sworddance.util.RandomKeyGenerator;
import com.sworddance.util.map.NamespaceMapKey;
import com.sworddance.util.perf.LapTimer;

import org.apache.commons.collections.map.MultiKeyMap;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;

import static org.amplafi.flow.FlowConstants.*;
import static org.amplafi.flow.FlowStateLifecycle.*;
import static org.apache.commons.lang.StringUtils.isNotBlank;
import static org.apache.commons.lang.StringUtils.isBlank;

/**
 * Application State Object that tracks the current state of a flow. Holds any
 * state information related to a specific flow
 *
 * Each flow state has all the information to run the flow and re-enter it if
 * needed.
 *
 * defines an actively executing flow. Each FlowState has an attached Flow which
 * is the instantiated definition. This copy is made to avoid problems with flow
 * definitions changing while an instance of a flow is active.
 */
public class FlowStateImpl implements FlowStateImplementor {

    private static final long serialVersionUID = -7694935572121566257L;

    /**
     * used when displaying the FlowEntryPoint.
     */
    private String activeFlowLabel;

    /**
     * unique flow id so that flows can be started, stopped, restarted easily.
     * Important! This key must be immutable as it is the key or part of the key for many for namespace lookups and maps.
     */
    private String lookupKey;

    /**
     * key, value pairs that will be used to hold the current state of the flow.
     * The values here should be fairly lightweight.
     */
    protected FlowValuesMap flowValuesMap;

    private String flowTypeName;

    protected transient FlowImplementor flow;

    /**
     * index into flow.activities.
     */
    private Integer currentActivityIndex;

    /**
     * to be used when advancing flow to a fixed step. Use case: changing
     * flowtype.
     */
    private String currentActivityByName;

    /**
     * flowManagement instance that this FlowState is attached to.
     */
    private transient FlowManagement flowManagement;

    /**
     * a map of values that the current components have placed here for the
     * FlowActivity and Flow definitions to use for processing this request.
     * This map only contains values up until the completion of the
     * selectActivity() call
     */
    private transient MultiKeyMap cachedValues;

    private FlowStateLifecycle flowStateLifecycle;

    private List<FlowPropertyValueChangeListener> globalFlowPropertyValueChangeListeners = new ArrayList<FlowPropertyValueChangeListener>(
            Arrays.asList(new InvalidatingFlowPropertyValueChangeListener()));

    public FlowStateImpl() {

    }

    public FlowStateImpl(String flowTypeName, FlowManagement sessionFlowManagement,
            Map<String, String> initialFlowState) {
        this(flowTypeName, sessionFlowManagement);
        //TODO Kostya: Should we put initial state into the flow namespace??
        this.setFlowValuesMap(new DefaultFlowValuesMap(initialFlowState));
    }

    public FlowStateImpl(String flowTypeName, FlowManagement sessionFlowManagement) {
        this();
        this.flowTypeName = flowTypeName;
        this.flowManagement = sessionFlowManagement;
        this.lookupKey = createLookupKey();
        this.setFlowLifecycleState(created);
    }

    // HACK : TODO lookupKey injection
    private String createLookupKey() {
        return this.flowTypeName + "_" + new RandomKeyGenerator(8).nextKey().toString();
    }

    @Override
    public String begin() {
        FlowStateLifecycle lifecycle = this.getFlowStateLifecycle();
        if (lifecycle != null) {
            switch (lifecycle) {
            case initialized:
                // normal case continue with begin
                break;
            case created:
                // o.k. behavior - not initialized yet so do the initialization.
                this.initializeFlow();
                break;
            case initializing:
                // begin() within an initializing ??? very odd. for now just log and return
                getLog().debug(this + ": begin() called but state is " + lifecycle);
                return null;
            case started:
                // double begin() called. slightly bad coding but o.k. otherwise
                return this.getCurrentPage();
            case starting:
                // nested begin()'s also odd but for now just log and return.
                getLog().debug(this + ": begin() called but state is " + lifecycle);
                return getCurrentPage();
            default:
                // the flowState has completed -- trying to restart is not supported.
                getLog().debug(this + ": begin() called but flow completed already state is " + lifecycle);
                return getCurrentPage();
            }
        }
        this.setFlowLifecycleState(starting);
        FlowStateLifecycle nextFlowLifecycleState = failed;
        try {
            // TODO ... should we just be using next()... seems better.
            selectActivity(0, true);
            nextFlowLifecycleState = started;
        } finally {
            // because may throw flow validation exception
            if (this.getFlowStateLifecycle() == starting) {
                this.setFlowLifecycleState(nextFlowLifecycleState);
            }
        }
        return getCurrentPage();
    }

    /**
     * @see org.amplafi.flow.FlowState#resume()
     */
    @Override
    public String resume() {
        if (!isActive()) {
            return begin();
        } else {
            if (activateFlowActivity(getCurrentActivity(), FlowStepDirection.inPlace)) {
                selectActivity(nextIndex(), true);
            }
            return getCurrentPage();
        }
    }

    /**
     *
     * @see org.amplafi.flow.FlowState#initializeFlow()
     */
    @Override
    public void initializeFlow() {
        this.setFlowLifecycleState(initializing);
        FlowStateLifecycle nextFlowLifecycleState = failed;
        try {
            Map<String, FlowPropertyDefinitionImplementor> propertyDefinitions = this.getFlow()
                    .getPropertyDefinitions();
            if (propertyDefinitions != null) {
                Collection<FlowPropertyDefinitionImplementor> flowPropertyDefinitions = propertyDefinitions
                        .values();
                initializeFlowProperties(this, flowPropertyDefinitions);
            }

            int size = this.size();
            for (int i = 0; i < size; i++) {
                FlowActivity activity = getActivity(i);
                activity.initializeFlow();
                LapTimer.sLap(activity.getFlowPropertyProviderFullName(), ".initializeFlow() completed");
            }
            nextFlowLifecycleState = initialized;
        } finally {
            // because may throw flow validation exception - which could have already changed the flowStateLifecycle.
            if (this.getFlowStateLifecycle() == initializing) {
                this.setFlowLifecycleState(nextFlowLifecycleState);
            }
        }
    }

    @Override
    public void initializeFlowProperties(FlowPropertyProvider flowPropertyProvider,
            Iterable<FlowPropertyDefinitionImplementor> flowPropertyDefinitions) {
        for (FlowPropertyDefinitionImplementor flowPropertyDefinition : flowPropertyDefinitions) {
            initializeFlowProperty(flowPropertyProvider, flowPropertyDefinition);
        }
    }

    /**
     * Look through the FlowState map to find all values with a valid key. ( see {@link FlowPropertyDefinitionImplementor#getNamespaceKeySearchList(FlowState, FlowPropertyProvider, boolean)} )
     * The first match found is used.
     *
     * See note in FactoryFlowPropertyDefinitionProvider - properties defined by FactoryFlowPropertyDefinitionProviders need to be initialized as well ( they are not ).
     * @param flowPropertyProvider
     * @param flowPropertyDefinition
     */
    @Override
    public void initializeFlowProperty(FlowPropertyProvider flowPropertyProvider,
            FlowPropertyDefinitionImplementor flowPropertyDefinition) {
        // move values from alternateNames to the true name.
        // or just clear out the alternate names of their values.
        List<String> namespaces = flowPropertyDefinition.getNamespaceKeySearchList(this, flowPropertyProvider,
                true);
        String value = null;
        boolean valueExternallySet = false;
        PropertyUsage propertyUsage = flowPropertyDefinition.getPropertyUsage();
        // make sure property clean up happens even for properties that cannot be set externally.
        for (String namespace : namespaces) {
            for (String alternateName : flowPropertyDefinition.getAllNames()) {
                if (getFlowValuesMap().containsKey(namespace, alternateName)) {
                    if (!valueExternallySet) {
                        value = getRawProperty(namespace, alternateName);
                        valueExternallySet = true;
                    }
                    if (propertyUsage.isCleanOnInitialization()) {
                        // if clearing then we need to clear all possible matches - so we continue with loop.
                        remove(namespace, alternateName);
                    } else if (valueExternallySet) {
                        break;
                    }
                }
            }
        }
        boolean valueSet = valueExternallySet;
        if (!valueExternallySet || !propertyUsage.isExternallySettable()) {
            value = flowPropertyDefinition.getInitial();
            valueSet = true;
        }
        String namespace = flowPropertyDefinition.getNamespaceKey(this, flowPropertyProvider);
        String currentValue = getRawProperty(namespace, flowPropertyDefinition.getName());
        if (valueSet && !StringUtils.equals(value, currentValue)) {
            // This code allows FlowPropertyChangeListeners to be triggered when the flow starts up.
            if (!propertyUsage.isExternallySettable() && valueExternallySet) {
                // TODO use ExternalPropertyAccessRestriction
                // property cannot be overridden.
                // note: this can happen when transitioning (morphing between 2 different flows).
                // In this case internalState properties are being copied not just user externally supplied values.
                // TODO: investigate to see if we can clean this up.
                getLog().info((flowPropertyProvider == null ? getFlow().getFlowPropertyProviderName()
                        : flowPropertyProvider.getFlowPropertyProviderFullName()) + '.'
                        + flowPropertyDefinition.getName() + " cannot be set to '" + currentValue
                        + "' external to the flow. It is being force to the initial value of '" + value + "'");
            }
            setRawProperty(flowPropertyProvider, flowPropertyDefinition, value);
        }
    }

    /**
     * @see org.amplafi.flow.FlowState#getExportedValuesMap()
     */
    @SuppressWarnings("unchecked")
    @Override
    public FlowValuesMap getExportedValuesMap() {
        FlowValuesMap valuesMap = exportProperties(false);
        return valuesMap;
    }

    /**
     * Method to allow overriding.
     * @return a copied FlowValuesMap
     */
    @SuppressWarnings("unchecked")
    protected FlowValuesMap createFlowValuesMapCopy() {
        return new DefaultFlowValuesMap((FlowValuesMap<FlowValueMapKey, CharSequence>) getFlowValuesMap());
    }

    /**
     * When exporting we start from all the values in the flowValuesMap. This is because the current flow may not be aware of/understand
     * all the properties. This flow is just passing the values on unaltered.
     *
     * TODO: if flow is still in progress then flowLocal/activityLocal values should be in the export map -- so that callees can get values.
     * @param clearFrom removes the exported values from this's FlowValuesMap.
     * @return the exported values
     */
    @SuppressWarnings("unchecked")
    protected FlowValuesMap<? extends FlowValueMapKey, ? extends CharSequence> exportProperties(boolean clearFrom) {
        FlowValuesMap exportValueMap = createFlowValuesMapCopy();
        Map<String, FlowPropertyDefinitionImplementor> propertyDefinitions = this.getFlow()
                .getPropertyDefinitions();
        if (isNotEmpty(propertyDefinitions)) {
            Collection<FlowPropertyDefinitionImplementor> flowPropertyDefinitions = propertyDefinitions.values();
            exportProperties(exportValueMap, flowPropertyDefinitions, null, clearFrom);
        }

        int size = this.size();
        for (int i = 0; i < size; i++) {
            FlowActivityImplementor activity = getActivity(i);
            Map<String, FlowPropertyDefinitionImplementor> activityFlowPropertyDefinitions = activity
                    .getPropertyDefinitions();
            if (isNotEmpty(activityFlowPropertyDefinitions)) {
                exportProperties(exportValueMap, activityFlowPropertyDefinitions.values(), activity, clearFrom);
            }
        }
        // TODO should we clear all non-global namespace values? We have slight leak through when undefined properties are set on a flow.
        return exportValueMap;
    }

    @SuppressWarnings("unchecked")
    protected void exportProperties(FlowValuesMap exportValueMap,
            Iterable<FlowPropertyDefinitionImplementor> flowPropertyDefinitions,
            FlowActivityImplementor flowActivity, boolean clearFrom) {
        for (FlowPropertyDefinitionImplementor flowPropertyDefinition : flowPropertyDefinitions) {
            exportFlowProperty(exportValueMap, flowPropertyDefinition, flowActivity, clearFrom);
        }
    }

    /**
     * @param exportValueMap map with namespaced properties. All properties should be copied back to the global namespace when done.
     * @param flowPropertyDefinition
     * @param flowActivity
     * @param flowCompletingExport
     * The FlowState is completing so all clean up actions on the FlowState can be performed as part of this export.
     * (TODO: pat 3 Oct 2010 removing state is a one time operation - maybe make it an explicit separate method call?)
     */
    @SuppressWarnings("unchecked")
    protected void exportFlowProperty(FlowValuesMap exportValueMap,
            FlowPropertyDefinitionImplementor flowPropertyDefinition, FlowActivityImplementor flowActivity,
            boolean flowCompletingExport) {
        // move values from alternateNames to the true name.
        // or just clear out the alternate names of their values.
        String value = null;
        boolean valueSet = false;
        List<String> namespaces = flowPropertyDefinition.getNamespaceKeySearchList(this, flowActivity, false);
        for (String namespace : namespaces) {
            for (String key : flowPropertyDefinition.getAllNames()) {
                if (getFlowValuesMap().containsKey(namespace, key)) {
                    if (!valueSet) {
                        // preserve the value from the most precise namespace.
                        value = getRawProperty(namespace, key);
                        valueSet = true;
                    }
                    if (namespace != null) {
                        // exclude the global namespace as we clear because global values may not be altered by this property ( propertyUsage.isCopyBackOnFlowSuccess() may be false )
                        exportValueMap.remove(namespace, key);
                        if (flowCompletingExport) {
                            remove(namespace, key);
                        }
                    }
                }
            }
        }
        if (valueSet) {
            // TODO HANDLE case where we need a to copy from caller to callee. This situation suggests that if PropertyUsage != internalState then the property should be exposed.
            // but need to know the situation: copy to callee or back to caller?
            if (flowPropertyDefinition.isCopyBackOnFlowSuccess()) {
                // HACK
                // HACK: investigate. Any use of getExportedValuesMap() will trigger this copyback to the FlowState's default namespace.
                put(null, flowPropertyDefinition.getName(), value);
                exportValueMap.put(null, flowPropertyDefinition.getName(), value);
            }
        }
    }

    @Override
    public void copyTrustedValuesMapToFlowState(Map<String, String> trustedValues) {
        if (isNotEmpty(trustedValues)) {
            for (Map.Entry<String, String> entry : trustedValues.entrySet()) {
                String key = entry.getKey();
                String value = entry.getValue();
                FlowPropertyDefinition flowPropertyDefinition = getFlowPropertyDefinitionWithCreate(key, null,
                        value);
                setRawProperty(this, flowPropertyDefinition, value);
            }
        }
    }

    /**
     *
     * @see org.amplafi.flow.FlowState#morphFlow(java.lang.String, java.util.Map)
     */
    @Override
    public String morphFlow(String morphingToFlowTypeName, Map<String, String> initialFlowState) {
        if (isCompleted()) {
            return null;
        }
        if (this.getFlowTypeName().equals(morphingToFlowTypeName)) {
            return this.getCurrentPage();
        }
        Flow nextFlow = getFlowManagement().getInstanceFromDefinition(morphingToFlowTypeName);
        List<FlowActivityImplementor> originalFAs = getActivities();
        List<FlowActivityImplementor> nextFAs = nextFlow.getActivities();

        // make sure FAs in both the flows are in order
        boolean inOrder = areFlowActivitiesInOrder(originalFAs, nextFAs);
        if (!inOrder) {
            throw new IllegalStateException(
                    "The FlowActivities in the original and the morphed flow are not in order"
                            + "\nOriginal Flow FlowActivities : " + originalFAs + "\nNext Flow FlowActivities : "
                            + nextFAs);
        }

        // complete the current FA in the current Flow
        FlowActivityImplementor currentFAInOriginalFlow = getCurrentFlowActivityImplementor();
        // So the current FlowActivity does not try to do validation.
        passivate(false, FlowStepDirection.inPlace);

        // morph and initialize to next flow
        setFlowTypeName(morphingToFlowTypeName);
        copyTrustedValuesMapToFlowState(initialFlowState);
        this.setCurrentActivityIndex(0);
        // new flow will have different flow activities (and properties ) that needs to be
        initializeFlow();
        begin();

        FlowActivityImplementor targetFAInNextFlow = getTargetFAInNextFlow(currentFAInOriginalFlow, originalFAs,
                nextFAs);

        // No common FAs, No need to run nextFlow at all, just return
        if (targetFAInNextFlow != null) {

            // move the second flow to appropriate FA
            while (hasNext() && !isEqualTo(getCurrentActivity(), targetFAInNextFlow)) {
                next();
            }
        }
        return this.getCurrentPage();
    }

    private FlowActivityImplementor getTargetFAInNextFlow(FlowActivityImplementor currentFAInOriginalFlow,
            List<FlowActivityImplementor> originalFAs, List<FlowActivityImplementor> nextFAs) {
        FlowActivity flowActivity = this.getActivity(currentFAInOriginalFlow.getFlowPropertyProviderName());
        if (flowActivity != null) {
            // cool .. exact match on the names.
            return (FlowActivityImplementor) flowActivity;
        }
        // find the first FlowActivity that is after all the flowActivities with the same names
        // as FlowActivities in the previous flow.to find the same approximate spot in the the new flow.
        int newCurrentIndex = this.getCurrentActivityIndex();
        for (int prevIndex = 0; prevIndex < originalFAs.size(); prevIndex++) {
            FlowActivity originalFA = originalFAs.get(prevIndex);
            if (isEqualTo(originalFA, currentFAInOriginalFlow)) {
                break;
            }
            for (int nextIndex = newCurrentIndex; nextIndex < nextFAs.size(); nextIndex++) {
                FlowActivity nextFA = nextFAs.get(nextIndex);
                if (isEqualTo(originalFA, nextFA)) {
                    newCurrentIndex = nextIndex + 1;
                }
            }
        }
        return (FlowActivityImplementor) this.getActivity(newCurrentIndex);
    }

    private boolean isEqualTo(FlowActivity fa1, FlowActivity fa2) {
        return fa1 != null && fa2 != null
                && fa1.getFlowPropertyProviderName().equals(fa2.getFlowPropertyProviderName());
    }

    private boolean areFlowActivitiesInOrder(List<FlowActivityImplementor> prevFAs,
            List<FlowActivityImplementor> nextFAs) {
        int lastPrevIndex = -1;
        int lastNextIndex = -1;
        for (int prevIndex = 0; prevIndex < prevFAs.size(); prevIndex++) {
            for (int nextIndex = 0; nextIndex < nextFAs.size(); nextIndex++) {
                if (isEqualTo(prevFAs.get(prevIndex), nextFAs.get(nextIndex))) {
                    if (nextIndex > lastNextIndex && prevIndex > lastPrevIndex) {
                        lastNextIndex = nextIndex;
                        lastPrevIndex = prevIndex;
                    } else {
                        return false;
                    }
                }
            }
        }
        return true;
    }

    /**
     *
     * @param verifyValues if true check the flow to validate the {@link FlowActivityPhase#finish} properties.
     * @return the next flowState 'this' FlowActivities believe should be run.
     */
    protected FlowState finishFlowActivities(boolean verifyValues) {
        FlowValidationResult flowValidationResult = null;
        if (verifyValues) {
            flowValidationResult = getFullFlowValidationResult(FlowActivityPhase.finish, FlowStepDirection.forward);
        }
        FlowValidationException.valid(this, flowValidationResult);
        FlowState currentNextFlowState = getFlowManagement().transitionToFlowState(this, FSFLOW_TRANSITIONS);
        int size = this.size();
        for (int i = 0; i < size; i++) {
            FlowActivity activity = getActivity(i);
            FlowState returned = activity.finishFlow(currentNextFlowState);
            // activity.refresh(); -- commented out because saves default values back to the flowState
            // avoids lose track of FlowState if another FA later in the Flow
            // definition returns a null. ( this means that a FA cannot override a previous decision ).
            if (returned != null && currentNextFlowState != returned) {
                currentNextFlowState = returned;
            }
        }
        return currentNextFlowState;
    }

    /**
     * @see org.amplafi.flow.FlowState#selectActivity(int, boolean)
     */
    @SuppressWarnings("unchecked")
    @Override
    public <T extends FlowActivity> T selectActivity(int newActivity, boolean verifyValues) {
        if (isCompleted()) {
            return null;
        }
        FlowActivityIterator flowActivityIterator = new FlowActivityIterator(newActivity);

        do {
            if (this.isActive()) {
                FlowValidationResult flowValidationResult;
                // call passivate even if just returning to the current
                // activity. but not if we are going back to a previous step
                flowValidationResult = this.passivate(verifyValues, flowActivityIterator.getFlowStepDirection());
                if (!flowValidationResult.isValid()) {
                    activateFlowActivity(getCurrentActivity(), FlowStepDirection.inPlace);
                    throw new FlowValidationException(this, getCurrentActivity(), flowValidationResult);
                }
            }
            if (flowActivityIterator.hasNext()) {
                flowActivityIterator.activate();
            }
        } while (flowActivityIterator.hasNext());
        if (flowActivityIterator.isTimeToFinish()) {
            // ran out .. time to complete...
            // if chaining FlowStates the actual page may be from another
            // flowState.
            finishFlow();
            return null;
        } else {
            return (T) getCurrentActivity();
        }
    }

    /**
     * @param flowStepDirection
     */
    private boolean activateFlowActivity(FlowActivity flowActivity, FlowStepDirection flowStepDirection) {
        getFlowManagement().activityChange(this, flowActivity, flowStepDirection, FlowActivityPhase.activate);
        return flowActivity.activate(flowStepDirection);
    }

    /**
     * @see org.amplafi.flow.FlowState#selectVisibleActivity(int)
     */
    @SuppressWarnings("unchecked")
    @Override
    public <T extends FlowActivity> T selectVisibleActivity(int visibleIndex) {
        int index = -1;
        int realIndex = -1;
        for (FlowActivity activity : getActivities()) {
            if (!activity.isInvisible()) {
                index++;
            }
            realIndex++;
            if (index == visibleIndex) {
                break;
            }
        }
        return (T) selectActivity(realIndex, false);
    }

    /**
     * @see org.amplafi.flow.FlowState#saveChanges()
     */
    @Override
    public void saveChanges() {
        LapTimer.sLap(this.getFlowPropertyProviderName(), " beginning saveChanges()");
        for (int i = 0; i < this.size(); i++) {
            FlowActivity flowActivity = getActivity(i);
            FlowValidationResult flowActivityValidationResult = flowActivity
                    .getFlowValidationResult(FlowActivityPhase.saveChanges, FlowStepDirection.forward);
            FlowValidationException.valid(this, flowActivityValidationResult);
            flowActivity.saveChanges();
            // activity.refresh(); -- commented out because saves default values back to the flowState
            LapTimer.sLap(flowActivity.getFlowPropertyProviderFullName(), ".saveChanges() completed");
        }
        LapTimer.sLap(this.getFlowPropertyProviderName(), " end saveChanges()");
    }

    /**
     * @see org.amplafi.flow.FlowState#finishFlow()
     */
    @Override
    public String finishFlow() {
        return completeFlow(FlowStateLifecycle.successful);
    }

    /**
     * @see org.amplafi.flow.FlowState#cancelFlow()
     */
    @Override
    public String cancelFlow() {
        return completeFlow(FlowStateLifecycle.canceled);
    }

    protected String completeFlow(FlowStateLifecycle nextFlowLifecycleState) {
        String pageName = null;
        if (!isCompleted()) {
            FlowState continueWithFlow = null;
            boolean verifyValues = nextFlowLifecycleState.isVerifyValues();
            FlowValidationResult flowValidationResult = passivate(verifyValues, FlowStepDirection.inPlace);

            if (verifyValues) {
                FlowValidationException.valid(this, flowValidationResult);
                saveChanges();
            }

            this.setFlowLifecycleState(nextFlowLifecycleState);

            boolean success = false;
            try {
                // getting continueWithFlow should use FlowLauncher more correctly.
                continueWithFlow = finishFlowActivities(verifyValues);
                success = true;
            } finally {
                this.setCurrentActivityByName(null);
                clearCache();
                if (!success) {
                    getFlowManagement().dropFlowState(this);
                }
            }
            // pass on the return flow to the continuation flow.
            // need to set before starting continuation flow because continuation flow may run to completion.
            // HACK : seems like the continueFlow should have picked this up automatically
            String returnToFlow = this.getProperty(FSRETURN_TO_FLOW);
            this.setProperty(FSRETURN_TO_FLOW, null);

            // TODO: THIS block of code should be in the FlowManagement code.
            // TODO: Put this in a FlowPropertyValueProvider !!
            // OLD note but may still be valid:
            // if continueWithFlow is not null then we do not want start
            // any other flows except continueWithFlow. Autorun flows should
            // start only if we have no flow specified by the finishingActivity. This
            // caused bad UI behavior when we used TransitionFlowActivity to start new
            // flow.
            // make sure that don't get into trouble by a finishFlow that
            // returns the current FlowState.
            if (continueWithFlow == null || continueWithFlow == this) {
                /* need to explore this more.
                   idea is that there should be some ability to copy back from the started flows.
                   but this really should be under the control of the caller. So right now best mechanism seems to be
                   to make caller pass in an object to be modified.
                if ( nextFlowLifecycleState == successful && isNotBlank(returnToFlow)) {
                FlowState returnFlow = getFlowManagement().getFlowState(returnToFlow);
                Map exportedMap = this.getExportedValuesMap().getAsFlattenedStringMap();
                returnFlow.setAllProperties(exportedMap);
                }
                 */
                pageName = getFlowManagement().completeFlowState(this, false, nextFlowLifecycleState);
            } else {
                if (isNotBlank(returnToFlow)) {
                    continueWithFlow.setProperty(FSRETURN_TO_FLOW, returnToFlow);
                }
                pageName = getFlowManagement().completeFlowState(this, true, nextFlowLifecycleState);
                if (!continueWithFlow.isActive()) {
                    pageName = continueWithFlow.begin();
                } else if (!continueWithFlow.isCompleted()) {
                    pageName = continueWithFlow.resume();
                }
                String continueWithFlowLookup;
                if (continueWithFlow.isCompleted()) {
                    // the flow that was continued with immediately finished.
                    // find out what the next continue flow is ... shouldn't this be in a while loop???
                    // or passed over to the FlowManagement code for handling??
                    continueWithFlowLookup = continueWithFlow.getProperty(FSCONTINUE_WITH_FLOW);
                } else {
                    continueWithFlowLookup = continueWithFlow.getLookupKey();
                }
                // save back to "this" so that if the current flowState is in turn part of a chain that the callers
                // will find the correct continue flow state.
                setProperty(FSCONTINUE_WITH_FLOW, continueWithFlowLookup);

            }
        }
        // if afterPage is already set then don't lose that information.
        if (pageName != null) {
            setAfterPage(pageName);
        }
        return pageName;
    }

    @Override
    public FlowValidationResult getFullFlowValidationResult(FlowActivityPhase flowActivityPhase,
            FlowStepDirection flowStepDirection) {
        FlowValidationResult flowValidationResult = new ReportAllValidationResult();
        // TODO : need to account for properties that earlier activities will create the property required by a later property.
        // we should look for PropertyUsage.create (and equivalents )
        for (FlowActivity flowActivity : this.getActivities()) {
            FlowValidationResult flowActivityValidationResult = flowActivity
                    .getFlowValidationResult(flowActivityPhase, flowStepDirection);
            flowValidationResult.merge(flowActivityValidationResult);
        }
        return flowValidationResult;
    }

    /**
     * @see org.amplafi.flow.FlowState#getFinishFlowValidationResult()
     */
    @Override
    public FlowValidationResult getFinishFlowValidationResult() {
        FlowValidationResult flowValidationResult = getCurrentActivityFlowValidationResult();
        if (flowValidationResult == null || flowValidationResult.isValid()) {
            flowValidationResult = getFullFlowValidationResult(FlowActivityPhase.finish, FlowStepDirection.forward);
            if (flowValidationResult == null || flowValidationResult.isValid()) {
                flowValidationResult = getFullFlowValidationResult(FlowActivityPhase.saveChanges,
                        FlowStepDirection.forward);
            }
        }
        return flowValidationResult;
    }

    /**
     * @param possibleReferencedState
     * @return true if this flowState references possibleReferencedState
     */
    @Override
    public boolean isReferencing(FlowState possibleReferencedState) {
        if (this == possibleReferencedState) {
            // can't reference self.
            return false;
        } else {
            String possibleReferencedLookupKey = possibleReferencedState.getLookupKey();
            return possibleReferencedLookupKey.equals(this.getProperty(FSCONTINUE_WITH_FLOW))
                    || possibleReferencedLookupKey.equals(this.getProperty(FSRETURN_TO_FLOW));
        }
    }

    @Override
    public FlowValidationResult passivate(boolean verifyValues, FlowStepDirection flowStepDirection) {
        FlowActivityImplementor currentActivity = getCurrentActivity();
        if (currentActivity != null) {
            currentActivity.refresh();
            return currentActivity.passivate(verifyValues, flowStepDirection);
        }
        return null;
    }

    /**
     * @see org.amplafi.flow.FlowState#getCurrentPage()
     */
    @Override
    public String getCurrentPage() {
        if (isCompleted()) {
            return this.getAfterPage();
        }
        String pageName = null;
        if (isActive()) {
            FlowActivity flowActivity = getCurrentActivity();
            pageName = flowActivity.getProperty(FSPAGE_NAME);
        }
        if (isBlank(pageName)) {
            pageName = getProperty(FSPAGE_NAME);
            if (isBlank(pageName)) {
                pageName = this.getFlow().getPageName();
            }
        }
        return pageName;
    }

    @Override
    public void setCurrentPage(String page) {
        setProperty(FSPAGE_NAME, page);
    }

    // TODO: merge? with activeFlowLabel?
    /**
     * @return the text for a link to activate this FlowState
     */
    public String getLinkTitle() {
        String linkTitle = getProperty(FSLINK_TEXT);
        if (isBlank(linkTitle)) {
            linkTitle = this.getFlow().getLinkTitle();
        }
        return linkTitle;
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T extends FlowActivity> T getActivity(int activityIndex) {
        T flowActivity = (T) this.getFlow().getActivity(activityIndex);
        return resolveActivity(flowActivity);
    }

    /**
     * All accesses to a {@link FlowActivity} should occur through this method.
     * This allows {@link FlowState} implementations to a chance to add in any
     * objects needed to access other parts of the service (database transactions for example).
     * @param <T>
     * @see FlowManagement#wireDependencies(Object)
     * @param flowActivity
     * @return flowActivity
     */
    public <T extends FlowPropertyProvider> T resolveActivity(T flowActivity) {
        getFlowManagement().wireDependencies(flowActivity);
        return flowActivity;
    }

    /**
     * @see org.amplafi.flow.FlowState#getActivity(java.lang.String)
     */
    @SuppressWarnings("unchecked")
    @Override
    public <T extends FlowActivity> T getActivity(String activityName) {
        // HACK we need to set up a map.
        if (activityName != null) {
            for (FlowActivity flowActivity : this.getFlow().getActivities()) {
                if (flowActivity.isNamed(activityName)) {
                    return (T) resolveActivity(flowActivity);
                }
            }
        }
        return null;
    }

    /**
     * @see org.amplafi.flow.FlowState#getLookupKey()
     */
    @Override
    public String getLookupKey() {
        return lookupKey;
    }

    @Override
    public boolean hasLookupKey(Object key) {
        if (key == null) {
            return false;
        } else {
            return getLookupKey().equals(key.toString());
        }
    }

    /**
     * @see org.amplafi.flow.FlowState#getCurrentActivity()
     */
    @SuppressWarnings("unchecked")
    @Override
    public <T extends FlowActivity> T getCurrentActivity() {
        return (T) getActivity(this.getCurrentActivityIndex());
    }

    public FlowActivityImplementor getCurrentFlowActivityImplementor() {
        return (FlowActivityImplementor) getCurrentActivity();
    }

    /**
     * Use selectActivity to change the current activity.
     *
     * @param currentActivity The currentActivity to set.
     */
    private void setCurrentActivityIndex(int currentActivity) {
        if (currentActivity >= 0 && currentActivity < size()) {
            this.currentActivityIndex = currentActivity;
            this.currentActivityByName = getCurrentActivity().getFlowPropertyProviderName();
        } else {
            // required to match the iterator definition.
            throw new NoSuchElementException(currentActivity + ": incorrect index for " + this.activeFlowLabel);
        }
    }

    /**
     * @see org.amplafi.flow.FlowState#setCurrentActivityByName(java.lang.String)
     */
    @Override
    public void setCurrentActivityByName(String currentActivityByName) {
        this.currentActivityByName = currentActivityByName;
        this.currentActivityIndex = null;
    }

    /**
     * @see org.amplafi.flow.FlowState#getCurrentActivityByName()
     */
    @Override
    public String getCurrentActivityByName() {
        return this.currentActivityByName;
    }

    /**
     * @see org.amplafi.flow.FlowState#size()
     */
    @Override
    public int size() {
        if (!isEmpty(getActivities())) {
            return getActivities().size();
        } else {
            return 0;
        }
    }

    /**
     * @see org.amplafi.flow.FlowState#getCurrentActivityIndex()
     */
    @Override
    public int getCurrentActivityIndex() {
        if (currentActivityIndex == null) {
            currentActivityIndex = -1;
            if (isNotBlank(currentActivityByName)) {
                int i = 0;
                for (FlowActivity flowActivity : this.getFlow().getActivities()) {
                    if (currentActivityByName.equals(flowActivity.getFlowPropertyProviderName())) {
                        currentActivityIndex = i;
                        break;
                    } else {
                        i++;
                    }
                }
            }
        }
        return currentActivityIndex;
    }

    @Override
    public String getRawProperty(String key) {
        return getRawProperty((FlowActivity) null, key);
    }

    @Override
    public String getRawProperty(String namespace, String key) {
        return ObjectUtils.toString(getFlowValuesMap().get(namespace, key), null);
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T> T getPropertyWithDefinition(FlowPropertyProvider flowPropertyProvider,
            FlowPropertyDefinitionImplementor propertyDefinition) {
        T result = (T) getCached(propertyDefinition, flowPropertyProvider);
        if (result == null) {
            getFlowManagement().wireDependencies(propertyDefinition);
            String value = getRawProperty(flowPropertyProvider, propertyDefinition);
            result = (T) propertyDefinition.deserialize(flowPropertyProvider, value);
            if (result == null && propertyDefinition.isAutoCreate()) {
                result = (T) propertyDefinition.getDefaultObject(flowPropertyProvider);

                if (!propertyDefinition.isCacheOnly()) {
                    // so the flowState has the generated value.
                    // this will make visible to json exporting.
                    // also triggers FlowPropertyValueChangeListeners on the initial set.
                    setPropertyWithDefinition(flowPropertyProvider, propertyDefinition, result);
                }
            }
            setCached(propertyDefinition, flowPropertyProvider, result);
        }
        return result;
    }

    @Override
    public <T> T getPropertyWithDefinition(FlowPropertyDefinition flowPropertyDefinition) {
        return this.getPropertyWithDefinition(null, (FlowPropertyDefinitionImplementor) flowPropertyDefinition);
    }

    @Override
    public <T> void setPropertyWithDefinition(FlowPropertyProvider flowPropertyProvider,
            FlowPropertyDefinitionImplementor propertyDefinition, T value) {
        Object actual;
        String stringValue = null;
        getFlowManagement().wireDependencies(propertyDefinition);
        if (value instanceof String && propertyDefinition.getDataClass() != String.class) {
            // handle case for when initializing from string values.
            // or some other raw format.
            stringValue = (String) value;
            actual = propertyDefinition.deserialize(flowPropertyProvider, stringValue);
        } else {
            actual = value;
        }
        boolean cacheValue = !(actual instanceof String);
        if (!propertyDefinition.isCacheOnly()) {
            if (stringValue == null) {
                stringValue = propertyDefinition.serialize(actual);
            }
            cacheValue &= this.setRawProperty(flowPropertyProvider, propertyDefinition, stringValue);
        }
        if (cacheValue) {
            // HACK FPD can't currently parse AmpEntites to actual objects.
            this.setCached(propertyDefinition, flowPropertyProvider, actual);
        }

    }

    /**
     * @param flowPropertyProvider
     * @param flowPropertyDefinition
     * @param value
     * @return true if the value has changed.
     */
    protected boolean setRawProperty(FlowPropertyProvider flowPropertyProvider,
            FlowPropertyDefinition flowPropertyDefinition, String value) {
        String namespace = ((FlowPropertyDefinitionImplementor) flowPropertyDefinition).getNamespaceKey(this,
                flowPropertyProvider);
        String key = flowPropertyDefinition.getName();
        String oldValue = getRawProperty(namespace, key);
        String newValue = value;
        if (!StringUtils.equals(newValue, oldValue)) {
            List<FlowPropertyValueChangeListener> flowPropertyValueChangeListeners = flowPropertyDefinition
                    .getFlowPropertyValueChangeListeners();
            if (isNotEmpty(flowPropertyValueChangeListeners)) {
                for (FlowPropertyValueChangeListener flowPropertyValueChangeListener : flowPropertyValueChangeListeners) {
                    this.getFlowManagement().wireDependencies(flowPropertyValueChangeListener);
                    newValue = flowPropertyValueChangeListener.propertyChange(flowPropertyProvider, namespace,
                            flowPropertyDefinition, newValue, oldValue);
                }
            }
            if (flowPropertyProvider instanceof FlowPropertyValueChangeListener) {
                newValue = ((FlowPropertyValueChangeListener) flowPropertyProvider).propertyChange(
                        flowPropertyProvider, namespace, flowPropertyDefinition, newValue, oldValue);
            }

            FlowActivityImplementor activity = getActivity(namespace);
            if (activity == flowPropertyProvider || !(activity instanceof FlowPropertyValueChangeListener)) {
                activity = getCurrentFlowActivityImplementor();
            }
            if (activity instanceof FlowPropertyValueChangeListener && activity != flowPropertyProvider) {
                newValue = ((FlowPropertyValueChangeListener) activity).propertyChange(flowPropertyProvider,
                        namespace, flowPropertyDefinition, newValue, oldValue);
            }

            for (FlowPropertyValueChangeListener flowPropertyValueChangeListener : this.globalFlowPropertyValueChangeListeners) {
                newValue = flowPropertyValueChangeListener.propertyChange(flowPropertyProvider, namespace,
                        flowPropertyDefinition, newValue, oldValue);
            }
            put(namespace, key, newValue);
            return true;
        } else {
            return false;
        }
    }

    /**
     * @param key
     * @param value
     * @param namespace
     */
    private void put(String namespace, String key, String value) {
        getFlowValuesMap().put(namespace, key, value);
        // in other way wrong cached value returns in next get request
        setCached(namespace, key, null);
    }

    protected void remove(String namespace, String key) {
        getFlowValuesMap().remove(namespace, key);
        // in other way wrong cached value returns in next get request
        setCached(namespace, key, null);
    }

    /**
     * @see org.amplafi.flow.FlowState#hasProperty(java.lang.String)
     */
    @Override
    public boolean hasProperty(String key) {
        return getFlowValuesMap().containsKey(key);
    }

    /**
     * @see org.amplafi.flow.FlowState#getActivities()
     */
    @SuppressWarnings("unchecked")
    @Override
    public List<FlowActivityImplementor> getActivities() {
        return getFlow().getActivities();
    }

    /**
     * @see org.amplafi.flow.FlowState#getVisibleActivities()
     */
    @Override
    public List<FlowActivity> getVisibleActivities() {
        return getFlow().getVisibleActivities();
    }

    /**
     * @see org.amplafi.flow.FlowState#isFinishable()
     */
    @Override
    public boolean isFinishable() {
        if (!isCompleted()) {
            FlowActivity currentActivity = this.getCurrentActivity();
            // may not have been started
            if ((currentActivity != null && currentActivity.isFinishingActivity()) || !hasVisibleNext()) {
                // explicitly able to finish.
                // or last visible step, which must always be able to finish.
                return true;
            } else {
                // all remaining activities claim they have valid data.
                // this enables a user to go back to a previous step and still finish the flow.
                // FlowActivities that have content that is required to be viewed (Terms of Service )
                // should have a state flag so that the flow can not be finished until the ToS is viewed.
                return getFinishFlowValidationResult().isValid();
            }
        } else {
            // if it is already completed then the flow is not finishable (it already is finished)
            // but may need to indicate that this is not an error as well.
            return false;
        }
    }

    @Override
    public void clearCache() {
        if (this.cachedValues != null) {
            this.cachedValues.clear();
            this.cachedValues = null;
        }
    }

    /**
     * @see org.amplafi.flow.FlowState#getFlowTitle()
     */
    @Override
    public String getFlowTitle() {
        String flowTitle = getProperty(FSTITLE_TEXT);
        if (isBlank(flowTitle)) {
            flowTitle = this.getFlow().getFlowTitle();
        }
        if (isBlank(flowTitle)) {
            flowTitle = getLinkTitle();
        }
        return flowTitle;
    }

    @Override
    public synchronized void setCached(String namespace, String key, Object value) {
        if (cachedValues == null) {
            if (value == null) {
                // nothing to cache and no cached values.
                return;
            }
            cachedValues = new MultiKeyMap();
            flowManagement.registerForCacheClearing();
        }
        if (value == null) {
            cachedValues.remove(namespace, key);
        } else {
            cachedValues.put(namespace, key, value);
        }
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T> T getCached(String namespace, String key) {
        if (cachedValues != null) {
            T value = (T) cachedValues.get(namespace, key);
            return value;
        } else {
            return null;
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T getCached(FlowPropertyDefinitionImplementor flowPropertyDefinition,
            FlowPropertyProvider flowPropertyProvider) {
        String namespace = flowPropertyDefinition.getNamespaceKey(this, flowPropertyProvider);
        return (T) getCached(namespace, flowPropertyDefinition.getName());
    }

    @Override
    public void setCached(FlowPropertyDefinitionImplementor flowPropertyDefinition,
            FlowPropertyProvider flowPropertyProvider, Object value) {
        String namespace = flowPropertyDefinition.getNamespaceKey(this, flowPropertyProvider);
        setCached(namespace, flowPropertyDefinition.getName(), value);
    }

    @Override
    public void setFlowManagement(FlowManagement flowManagement) {
        this.flowManagement = flowManagement;
    }

    /**
     * @see org.amplafi.flow.FlowState#getFlowManagement()
     */
    @Override
    public FlowManagement getFlowManagement() {
        return flowManagement;
    }

    public void setFlowTypeName(String flowTypeName) {
        this.flowTypeName = flowTypeName;
        this.flow = null;
    }

    /**
     * @see org.amplafi.flow.FlowState#getFlowTypeName()
     */
    @Override
    public String getFlowTypeName() {
        return flowTypeName;
    }

    /**
     * @see org.amplafi.flow.FlowState#getFlow()
     */
    @Override
    public synchronized FlowImplementor getFlow() {
        if (this.flow == null && getFlowTypeName() != null) {
            this.flow = this.getFlowManagement().getInstanceFromDefinition(getFlowTypeName());
            if (this.flow == null) {
                throw new IllegalArgumentException(getFlowTypeName() + ": no such flow definition");
            }
            this.flow.setFlowState(this);
        }
        return flow;
    }

    /**
     * @see org.amplafi.flow.FlowState#iterator()
     */
    @Override
    public Iterator<FlowActivity> iterator() {
        return this;
    }

    /**
     * @see org.amplafi.flow.FlowState#next()
     */
    @Override
    public FlowActivity next() {
        if (hasNext()) {
            return this.selectActivity(nextIndex(), true);
        } else {
            finishFlow();
            return null;
        }

    }

    /**
     * @see org.amplafi.flow.FlowState#previous()
     */
    @Override
    public FlowActivity previous() {
        return this.selectActivity(previousIndex(), false);
    }

    /**
     *
     * @see java.util.ListIterator#add(java.lang.Object)
     */
    @Override
    public void add(FlowActivity e) {
        throw new UnsupportedOperationException("cannot add FlowActivities");
    }

    /**
     * @see org.amplafi.flow.FlowState#hasVisibleNext()
     */
    @Override
    public boolean hasVisibleNext() {
        if (!hasNext()) {
            return false;
        }
        int count = getActivities().size();
        for (int i = getCurrentActivityIndex() + 1; i < count; i++) {
            if (!getActivity(i).isInvisible()) {
                return true;
            }
        }
        return false;
    }

    /**
     * @see org.amplafi.flow.FlowState#hasVisiblePrevious()
     */
    @Override
    public boolean hasVisiblePrevious() {
        if (!hasPrevious()) {
            return false;
        }
        for (int i = getCurrentActivityIndex() - 1; i >= 0; i--) {
            if (!getActivity(i).isInvisible()) {
                return true;
            }
        }
        return false;
    }

    /**
     * @see org.amplafi.flow.FlowState#hasNext()
     */
    @Override
    public boolean hasNext() {
        if (isCompleted()) {
            return false;
        } else {
            //        } else if ( getCurrentActivity().getFlowValidationResult().isValid()) {
            int count = getActivities().size();
            return this.getCurrentActivityIndex() < count - 1;
            //        } else {
            //            // TODO -- this seems bad because hasNext() seems like it should be constant.
            //            return false;
        }
    }

    /**
     * @see org.amplafi.flow.FlowState#hasPrevious()
     */
    @Override
    public boolean hasPrevious() {
        if (isCompleted()) {
            return false;
        } else {
            return this.getCurrentActivityIndex() > 0;
        }
    }

    /**
     * @see org.amplafi.flow.FlowState#nextIndex()
     */
    @Override
    public int nextIndex() {
        return this.getCurrentActivityIndex() + 1;
    }

    /**
     * @see org.amplafi.flow.FlowState#previousIndex()
     */
    @Override
    public int previousIndex() {
        return this.getCurrentActivityIndex() - 1;
    }

    /**
     * @see org.amplafi.flow.FlowState#remove()
     */
    @Override
    public void remove() {
        throw new UnsupportedOperationException("TODO: Auto generated");
    }

    @Override
    public void set(FlowActivity e) {
        throw new UnsupportedOperationException("TODO: Auto generated");
    }

    @Override
    public FlowValidationResult getCurrentActivityFlowValidationResult(FlowActivityPhase flowActivityPhase,
            FlowStepDirection flowStepDirection) {
        FlowActivity currentActivity = this.getCurrentActivity();
        if (currentActivity == null) {
            return null;
        } else {
            if (FlowActivityPhase.advance == flowActivityPhase && flowStepDirection == FlowStepDirection.forward) {
                // TODO temp hack
                return currentActivity.getFlowValidationResult();
            } else {
                return currentActivity.getFlowValidationResult(flowActivityPhase, flowStepDirection);
            }
        }
    }

    /**
     * @see org.amplafi.flow.FlowState#getCurrentActivityFlowValidationResult()
     */
    @Override
    public FlowValidationResult getCurrentActivityFlowValidationResult() {
        return this.getCurrentActivityFlowValidationResult(FlowActivityPhase.advance, FlowStepDirection.forward);
    }

    @Override
    public Map<String, FlowValidationResult> getFlowValidationResults(FlowActivityPhase flowActivityPhase,
            FlowStepDirection flowStepDirection) {
        Map<String, FlowValidationResult> result = new LinkedHashMap<String, FlowValidationResult>();
        for (FlowActivity activity : this.getActivities()) {
            FlowValidationResult flowValidationResult = activity.getFlowValidationResult(flowActivityPhase,
                    flowStepDirection);
            if (!flowValidationResult.isValid()) {
                result.put(activity.getFlowPropertyProviderName(), flowValidationResult);
            }
        }
        return result;
    }

    @Override
    public void setAfterPage(String afterPage) {
        this.setProperty(FSAFTER_PAGE, afterPage);
    }

    /**
     * @see org.amplafi.flow.FlowState#getAfterPage()
     */
    @Override
    public String getAfterPage() {
        // if (this.flowLifecycleState == canceled) {
        // return null;
        // }
        String page = getProperty(FSAFTER_PAGE, String.class);
        if (isNotBlank(page)) {
            return page;
        }
        page = getProperty(FSDEFAULT_AFTER_PAGE, String.class);
        if (isNotBlank(page)) {
            return page;
        } else {
            return flow == null ? null : flow.getDefaultAfterPage();
        }
    }

    /**
     * @see org.amplafi.flow.FlowState#isUpdatePossible()
     */
    @Override
    public boolean isUpdatePossible() {
        return isNotBlank(getUpdateText());
    }

    /**
     * @see org.amplafi.flow.FlowState#getUpdateText()
     */
    @Override
    public String getUpdateText() {
        return this.getProperty(FAUPDATE_TEXT, String.class);
    }

    /**
     * @see org.amplafi.flow.FlowState#getCancelText()
     */
    @Override
    public String getCancelText() {
        return this.getProperty(FSCANCEL_TEXT, String.class);
    }

    @Override
    public void setCancelText(String cancelText) {
        this.setProperty(FSCANCEL_TEXT, cancelText);
    }

    /**
     * @see org.amplafi.flow.FlowState#getFinishText()
     */
    @Override
    public String getFinishText() {
        return this.getProperty(FSFINISH_TEXT, String.class);
    }

    @Override
    public void setFinishText(String finishText) {
        this.setProperty(FSFINISH_TEXT, finishText);
    }

    /**
     * @see org.amplafi.flow.FlowState#setFinishKey(java.lang.String)
     */
    @Override
    public void setFinishKey(String type) {
        this.setProperty(FSALT_FINISHED, type);
    }

    /**
     * @see org.amplafi.flow.FlowState#getFinishKey()
     */
    @Override
    public String getFinishKey() {
        return this.getProperty(FSALT_FINISHED, String.class);
    }

    @Override
    public void setFlowLifecycleState(FlowStateLifecycle flowStateLifecycle) {
        if (this.flowStateLifecycle != flowStateLifecycle) {
            FlowStateLifecycle previousFlowLifecycleState = this.flowStateLifecycle;
            this.flowStateLifecycle = STATE_CHECKER.checkAllowed(this.flowStateLifecycle, flowStateLifecycle);
            this.getFlowManagement().lifecycleChange(this, previousFlowLifecycleState);
        }
    }

    /**
     * @see org.amplafi.flow.FlowState#getFlowStateLifecycle()
     */
    @Override
    public FlowStateLifecycle getFlowStateLifecycle() {
        return this.flowStateLifecycle;
    }

    /**
     * @see org.amplafi.flow.FlowState#isCompleted()
     */
    @Override
    public boolean isCompleted() {
        return this.flowStateLifecycle != null && this.flowStateLifecycle.isTerminalState();
    }

    /**
     * TODO: note that there exists an issue: if a property for the current FA is PropertyUsage.initialize,
     * because PropertyUsage.initialize does not clear out the old values but just ignores them.
     */
    @Override
    public boolean isPropertySet(String key) {
        // HACK : too problematic need better way to ask if a FlowPropertyValueProvider can actually return a value.
        //        FlowPropertyDefinition flowPropertyDefinition = getFlowPropertyDefinitionWithCreate(key, null, null);
        //        if ( !flowPropertyDefinition.isDefaultObjectAvailable(this)) {
        return isPropertyValueSet(key);
        //        } else {
        //            return true;
        //        }
    }

    /**
     * @see org.amplafi.flow.flowproperty.FlowPropertyProviderWithValues#isPropertyValueSet(java.lang.String)
     */
    @Override
    public boolean isPropertyValueSet(String key) {
        return getRawProperty(key) != null;
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T> T getProperty(String key) {
        return (T) getProperty(key, null);
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T> T getProperty(String key, Class<? extends T> expected) {
        if (isActive()) {
            FlowActivity currentActivity = getCurrentActivity();
            return currentActivity.getProperty(key, expected);
        } else {
            FlowPropertyDefinitionImplementor flowPropertyDefinition = getFlowPropertyDefinitionWithCreate(key,
                    expected, null);
            return (T) getPropertyWithDefinition(this, flowPropertyDefinition);
        }
    }

    @Override
    public <T> T getProperty(Class<? extends T> expected) {
        return getProperty(FlowPropertyDefinitionBuilder.toPropertyName(expected), expected);
    }

    /**
     * @see org.amplafi.flow.FlowState#isActive()
     */
    @Override
    public boolean isActive() {
        // TODO | HACK Seems like we should be looking at FlowLifecycleState here not the index range.
        return this.getCurrentActivityIndex() >= 0 && getCurrentActivityIndex() < size();
    }

    /**
     * @see org.amplafi.flow.flowproperty.FlowPropertyProvider#getFlowPropertyProviderFullName()
     */
    @Override
    public String getFlowPropertyProviderFullName() {
        return getFlowTypeName() + "." + this.getFlowPropertyProviderName();
    }

    /**
     * @see org.amplafi.flow.flowproperty.FlowPropertyProvider#getFlowPropertyProviderName()
     */
    @Override
    public String getFlowPropertyProviderName() {
        return getLookupKey();
    }

    /**
     * @see org.amplafi.flow.flowproperty.FlowPropertyProvider#getPropertyDefinitions()
     */
    @Override
    public Map<String, FlowPropertyDefinition> getPropertyDefinitions() {
        return this.getFlow().getPropertyDefinitions();
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T extends FlowPropertyDefinition> T getFlowPropertyDefinition(String key) {
        T flowPropertyDefinition = null;
        if (this.getFlow() != null) {
            flowPropertyDefinition = (T) this.getFlow().getFlowPropertyDefinition(key);
        }
        if (flowPropertyDefinition == null && this.getFlowManagement() != null) {
            // (may not be assigned to a flowManagement any more -- historical FlowState )
            FlowPropertyDefinitionBuilder flowPropertyDefinitionBuilder = this.getFlowManagement()
                    .getFactoryFlowPropertyDefinitionBuilder(key, null);
            if (flowPropertyDefinitionBuilder != null) {
                flowPropertyDefinition = (T) flowPropertyDefinitionBuilder
                        .toFlowPropertyDefinition(getFlowManagement().getFlowTranslatorResolver());
            }
        }
        return flowPropertyDefinition;
    }

    /**
     * @see org.amplafi.flow.FlowState#setAllProperties(java.util.Map)
     */
    @Override
    public void setAllProperties(Map<?, ?> exportedMap) {
        for (Map.Entry<String, ?> entry : NotNullIterator.<Map.Entry<String, ?>>newNotNullIterator(exportedMap)) {
            Object value = entry.getValue();
            String key = entry.getKey();
            setProperty(key, value);
        }
    }

    /**
     * @see org.amplafi.flow.FlowState#setProperty(java.lang.String,
     *      java.lang.Object)
     */
    @SuppressWarnings("unchecked")
    @Override
    public <T> void setProperty(String key, T value) {
        if (isActive()) {
            FlowActivity currentActivity = getCurrentActivity();
            currentActivity.setProperty(key, value);
        } else {
            Class<T> expected = (Class<T>) (value == null ? null : value.getClass());
            FlowPropertyDefinitionImplementor flowPropertyDefinition = getFlowPropertyDefinitionWithCreate(key,
                    expected, value);
            setPropertyWithDefinition(null, flowPropertyDefinition, value);
        }
    }

    @SuppressWarnings("unchecked")
    private <T, FP extends FlowPropertyDefinition> FP getFlowPropertyDefinitionWithCreate(String key,
            Class<T> expected, T value) {
        FP flowPropertyDefinition = (FP) getFlowPropertyDefinition(key);

        if (flowPropertyDefinition == null) {
            flowPropertyDefinition = (FP) getFlowManagement().createFlowPropertyDefinition(getFlow(), key, expected,
                    value);
        }
        return flowPropertyDefinition;
    }

    /**
     * @see org.amplafi.flow.FlowState#setDefaultAfterPage(java.lang.String)
     */
    @Override
    public void setDefaultAfterPage(String pageName) {
        this.setProperty(FSDEFAULT_AFTER_PAGE, pageName);
    }

    /**
     * @see org.amplafi.flow.FlowState#getDefaultAfterPage()
     */
    @Override
    public String getDefaultAfterPage() {
        String property = this.getProperty(FSDEFAULT_AFTER_PAGE);
        return property == null ? this.getFlow().getDefaultAfterPage() : property;
    }

    /**
     * @see org.amplafi.flow.FlowState#isNotCurrentAllowed()
     */
    @Override
    public boolean isNotCurrentAllowed() {
        return this.getFlow().isNotCurrentAllowed();
    }

    /**
     * @see org.amplafi.flow.FlowState#getFlowValuesMap()
     */
    @Override
    public FlowValuesMap getFlowValuesMap() {
        if (this.flowValuesMap == null) {
            this.flowValuesMap = new DefaultFlowValuesMap();
        }
        return this.flowValuesMap;
    }

    @Override
    public void setFlowValuesMap(FlowValuesMap flowValuesMap) {
        this.flowValuesMap = flowValuesMap;
    }

    public Log getLog() {
        // TODO handle historical FlowStates ( no FlowManagement )
        if (getFlowManagement() == null) {
            return null;
        } else {
            return getFlowManagement().getLog();
        }
    }

    protected String getRawProperty(FlowPropertyProvider flowPropertyProvider, String key) {
        FlowPropertyDefinition propertyDefinition = getFlowPropertyDefinitionWithCreate(key, null, null);
        return getRawProperty(flowPropertyProvider, propertyDefinition);
    }

    @Override
    public String getRawProperty(FlowPropertyProvider flowPropertyProvider,
            FlowPropertyDefinition propertyDefinition) {
        String key = propertyDefinition.getName();
        String namespace = ((FlowPropertyDefinitionImplementor) propertyDefinition).getNamespaceKey(this,
                flowPropertyProvider);
        String value;
        if (getFlowValuesMap().containsKey(namespace, key)) {
            // A flow may set a value to null that is *not* copied out to the global namespace (yet or depending on PropertyUsage never)
            // this is a reasonable use case, so allow for the (namespace,key) to have a null value.
            value = getRawProperty(namespace, key);
        } else if (propertyDefinition.getPropertyUsage().isExternallySettable()) {
            //Property is externally settable so trying default namespace..
            value = getRawProperty(NamespaceMapKey.NO_NAMESPACE, key);
        } else {
            value = null;
        }
        return value;
    }

    /**
     * Get an object from the database.  If the object does not exist in the database a value of null will be returned.
     *
     * This uses a Hibernate get() call rather than a load() call to prevent us from getting errors if an entity can't be found
     * by the ID passed in.  This exception gets thrown whenever an object field is first accessed which can be well above the data
     * access code.
     * @param <T>
     * @param <K>
     *
     * @param clazz The class of the object to load
     * @param entityId The id of the object to load
     * @return The loaded object or null if it doesn't exist
     */
    public <T, K> T load(Class<? extends T> clazz, K entityId) {
        // We need to use get() to load entities as opposed to load() in case we try to get an entity via an id that doesn't pair
        // to a record in the database.  If we can figure out a way to validate the record actually exists then we can
        // switch this to use load() and not incur the up-front overhead.
        return getFlowManagement().getFlowTx().get(clazz, entityId, true);
    }

    /**
     * @see org.amplafi.flow.FlowStateProvider#getFlowState()
     */
    @SuppressWarnings("unchecked")
    @Override
    public <FS extends FlowState> FS getFlowState() {
        return (FS) this;
    }

    protected void warn(String message) {
        Log log = getLog();
        if (log != null) {
            log.warn(message);
        }
    }

    @Override
    public String toString() {
        return this.lookupKey + " [type:" + this.flowTypeName + "]; current Activity=" + this.getCurrentActivity()
                + "; flowStateMap=" + this.flowValuesMap;
    }

    @Override
    public boolean isPersisted() {
        return false;
    }

    protected class FlowActivityIterator implements Iterator<FlowActivityImplementor> {

        private FlowStepDirection flowStepDirection;
        private int next;
        // based on the flowStepDirection. if true, then there another FlowActivity in the same direction as the current flowActivity
        private boolean canContinue;
        // if true, currentActivity indicated that it has finished processing and the FlowState should immediately advanced. Used primarily for invisible FlowActivities.
        // true by default to handle 0 FA flows.
        private boolean lastFlowActivityActivateAutoFinished = true;

        FlowActivityIterator(int next) {
            // used to help determine if the flow is not altering which FlowActivity is current. ( refresh case )
            int originalIndex = getCurrentActivityIndex();
            FlowStepDirection flowStepDirection = FlowStepDirection.get(originalIndex, next);
            this.flowStepDirection = flowStepDirection;
            this.next = next;
            this.canContinue = FlowStateImpl.this.size() > 0;
        }

        public boolean isTimeToFinish() {
            return isLastFlowActivityActivateAutoFinished() && getFlowStepDirection() == FlowStepDirection.forward
                    && !isCanContinue();
        }

        public void activate() {
            FlowActivityImplementor currentActivity = next();

            lastFlowActivityActivateAutoFinished = activateFlowActivity(currentActivity, flowStepDirection);
        }

        @Override
        public boolean hasNext() {
            return lastFlowActivityActivateAutoFinished && canContinue;
        }

        @Override
        public FlowActivityImplementor next() {
            setCurrentActivityIndex(next);

            FlowActivityImplementor currentActivity = FlowStateImpl.this.getCurrentActivity();
            switch (flowStepDirection) {
            case forward:
                next = FlowStateImpl.this.nextIndex();
                canContinue = FlowStateImpl.this.hasNext();
                break;
            case backward:
                next = FlowStateImpl.this.previousIndex();
                canContinue = FlowStateImpl.this.hasPrevious();
                break;
            default:
                canContinue = false;
                break;
            }
            return currentActivity;
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }

        public boolean isCanContinue() {
            return canContinue;
        }

        public boolean isLastFlowActivityActivateAutoFinished() {
            return lastFlowActivityActivateAutoFinished;
        }

        public FlowStepDirection getFlowStepDirection() {
            return flowStepDirection;
        }
    }
}