/* * Copyright (c) 2005-2016 Vincent Vandenschrick. All rights reserved. * * This file is part of the Jspresso framework. * * Jspresso is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Jspresso is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Jspresso. If not, see <http://www.gnu.org/licenses/>. */ package org.jspresso.framework.application.frontend.action.wizard; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import org.jspresso.framework.action.IActionHandler; import org.jspresso.framework.application.frontend.action.FrontendAction; import org.jspresso.framework.application.frontend.action.ModalDialogAction; import org.jspresso.framework.binding.IValueConnector; import org.jspresso.framework.binding.model.IModelConnectorFactory; import org.jspresso.framework.model.descriptor.IComponentDescriptor; import org.jspresso.framework.model.descriptor.IModelDescriptor; import org.jspresso.framework.model.descriptor.IPropertyDescriptor; import org.jspresso.framework.model.descriptor.IScalarPropertyDescriptor; import org.jspresso.framework.util.collection.ObjectEqualityMap; import org.jspresso.framework.util.gui.Dimension; import org.jspresso.framework.util.gui.Icon; import org.jspresso.framework.util.i18n.ITranslationProvider; import org.jspresso.framework.view.IView; import org.jspresso.framework.view.action.IDisplayableAction; /** * This action implements a "wizard". It can be configured from the * simplest use-case (as for a value entering) to the most complex, multi-step * wizard. Here are a usage directions : * <ol> * <li>The generic wizard front-end action is registered in the Spring context * under the name wizardAction. So you will typically inherit the bean * definition using the parent="wizardAction" when declaring yours.</li> * <li>The goal of the wizard action is to work on a map - the wizard model - * (potentially containing other maps) that represents a hierarchical data * structure that can be used seamlessly as model for any Jspresso view. In your * case, the map will only contain 1 key-value pair (the property you want your * user to enter). When finishing the wizard, the action context will contain * the map with all the key-value pairs the user has created/modified through * the wizard steps. It will be accessible in the action context under the * ActionContextConstants.ACTION_PARAM key and will typically serve as input for * the finish chained action.</li> * <li>The wizard action is configured using chained * org.jspresso.framework.application * .frontend.action.wizard.IWizardStepDescriptor. A concrete, directly usable, * implementation of this interface is * org.jspresso.framework.application.frontend * .action.wizard.StaticWizardStepDescriptor. Each wizard step is highly * configurable (name, description, icon, ...) but its most important properties * are : * <ol> * <li>{@code viewDescriptor} : the Jspresso view descriptor to be shown in * the wizard GUI when the user enters this step. It can be arbitrarily complex * (even with master-detail like views, inner actions, constraints, security * enforcements, ...). Of course, the view descriptor needs a model descriptor. * So you must describe the wizard model as you would do for persistent entities * or components (so that step views can configure themselves). You will * typically use a BasicComponentDescriptor without name so that it is * automatically excluded from code generation. Note that your actual model * object will be a map (and not Jspresso generated java bean) but Jspresso * connectors are "smart" enough to detect the situation and work with the * hierarchy of maps as if it was a hierarchy of java beans.</li> * <li>optional {@code onEnterAction} and {@code onLeaveAction} : * actions that will respectively be executed when entering and when exiting the * wizard step.</li> * <li>optional {@code nextLabelKey} and {@code previousLabelKey} : * i18n keys for next and previous buttons if you want to change the default * ones.</li> * <li>optional {@code nextStepDescriptor} : the next wizard step. If null, * the wizard GUI will enable the finish action.</li> * </ol> * </li> * <li>The first wizard step is registered on the wizard action using the * {@code firstWizardStep} property.</li> * <li>When the user leaves the last wizard step (clicking the finish action * button), the finish action is triggered. The finish action can be registered * on the wizard action using the {@code finishAction} property. This is * typically the place where you explore the wizard map model - * {@code ACTION_PARAM} - to get back all the data the user has worked on. * Note that the finish button is entirely configured from the finish action * (label and icon).</li> * </ol> * * @author Vincent Vandenschrick * @param <E> * the actual gui component type used. * @param <F> * the actual icon type used. * @param <G> * the actual action type used. */ public class WizardAction<E, F, G> extends FrontendAction<E, F, G> { private IDisplayableAction cancelAction; private IDisplayableAction finishAction; private IWizardStepDescriptor firstWizardStep; private Integer height; private Integer width; /** * {@inheritDoc} */ @SuppressWarnings("unchecked") @Override public boolean execute(IActionHandler actionHandler, Map<String, Object> context) { IValueConnector modelConnector = getBackendController(context) .createModelConnector(ACTION_MODEL_NAME, firstWizardStep.getViewDescriptor().getModelDescriptor()); Map<String, Object> wizardModelInit = (Map<String, Object>) context .get(IWizardStepDescriptor.INITIAL_WIZARD_MODEL); Map<String, Object> wizardModel = new ObjectEqualityMap<>(); if (wizardModelInit != null) { wizardModel.putAll(wizardModelInit); } completeInitialWizardModel(wizardModel, context); modelConnector.setConnectorValue(wizardModel); displayWizardStep(firstWizardStep, modelConnector, actionHandler, context, false); return super.execute(actionHandler, context); } /** * Configures the action that will be executed whenever the user cancels the * wizard. * * @param cancelAction * the cancelAction to set. */ public void setCancelAction(IDisplayableAction cancelAction) { this.cancelAction = cancelAction; } /** * Configures the action that will be executed whenever the user validates the * wizard. * * @param finishAction * the finishAction to set. */ public void setFinishAction(IDisplayableAction finishAction) { this.finishAction = finishAction; } /** * Configures the first wizard step to display. * * @param firstWizardStep * the firstWizardStep to set. */ public void setFirstWizardStep(IWizardStepDescriptor firstWizardStep) { this.firstWizardStep = firstWizardStep; } /** * Configures explicitly the height of the wizard dialog. It prevents the * dialog from resizing dynamically depending on the displayed wizard step. * * @param height * the height to set. */ public void setHeight(Integer height) { this.height = height; } /** * Sets the modelConnectorFactory. * * @param modelConnectorFactory * the modelConnectorFactory to set. * @deprecated model connector is now created by the backend controller. */ @SuppressWarnings({"EmptyMethod", "UnusedParameters"}) @Deprecated public void setModelConnectorFactory( IModelConnectorFactory modelConnectorFactory) { // this.modelConnectorFactory = modelConnectorFactory; } /** * Configures explicitly the width of the wizard dialog. It prevents the * dialog from resizing dynamically depending on the displayed wizard step. * * @param width * the width to set. */ public void setWidth(Integer width) { this.width = width; } /** * Creates (and initializes) the wizard model. * * @param initialWizardModel * the initial wizard model. * @param context * the action context. */ @SuppressWarnings({"EmptyMethod", "UnusedParameters"}) protected void completeInitialWizardModel( Map<String, Object> initialWizardModel, Map<String, Object> context) { // No-op by default. } private G createCancelAction(IWizardStepDescriptor wizardStep, IActionHandler actionHandler, IView<E> view, Locale locale, Map<String, Object> context) { IDisplayableAction cancelActionAdapter = new CancelAction(wizardStep, cancelAction); G cancelGAction = getActionFactory(context).createAction( cancelActionAdapter, actionHandler, view, locale); return cancelGAction; } private G createFinishAction(IWizardStepDescriptor wizardStep, IActionHandler actionHandler, IView<E> view, Locale locale, Map<String, Object> context) { IDisplayableAction finishActionAdapter = new FinishAction(wizardStep, finishAction); G finishGAction = getActionFactory(context).createAction( finishActionAdapter, actionHandler, view, locale); if (wizardStep.canFinish(context)) { getActionFactory(context).setActionEnabled(finishGAction, true); } else { getActionFactory(context).setActionEnabled(finishGAction, false); } return finishGAction; } private G createNextAction(IWizardStepDescriptor wizardStep, IActionHandler actionHandler, IView<E> view, ITranslationProvider translationProvider, Locale locale, IValueConnector modelConnector, Map<String, Object> context) { NextAction nextAction = new NextAction(wizardStep, modelConnector); nextAction .setIconImageURL(getIconFactory(context).getForwardIconImageURL()); G nextGAction = getActionFactory(context).createAction(nextAction, actionHandler, view, locale); if (wizardStep.getNextStepDescriptor(context) != null) { getActionFactory(context).setActionEnabled(nextGAction, true); } else { getActionFactory(context).setActionEnabled(nextGAction, false); } if (wizardStep.getNextLabelKey() != null) { getActionFactory(context).setActionName( nextGAction, translationProvider.getTranslation(wizardStep.getNextLabelKey(), locale)); } else { getActionFactory(context).setActionName( nextGAction, translationProvider.getTranslation( IWizardStepDescriptor.DEFAULT_NEXT_KEY, locale)); } return nextGAction; } private G createPreviousAction(IWizardStepDescriptor wizardStep, IActionHandler actionHandler, IView<E> view, ITranslationProvider translationProvider, Locale locale, IValueConnector modelConnector, Map<String, Object> context) { PreviousAction previousAction = new PreviousAction(wizardStep, modelConnector); previousAction.setIconImageURL(getIconFactory(context) .getBackwardIconImageURL()); G previousGAction = getActionFactory(context).createAction(previousAction, actionHandler, view, locale); if (wizardStep.getPreviousStepDescriptor(context) != null) { getActionFactory(context).setActionEnabled(previousGAction, true); } else { getActionFactory(context).setActionEnabled(previousGAction, false); } if (wizardStep.getPreviousLabelKey() != null) { getActionFactory(context).setActionName( previousGAction, translationProvider.getTranslation(wizardStep.getPreviousLabelKey(), locale)); } else { getActionFactory(context).setActionName( previousGAction, translationProvider.getTranslation( IWizardStepDescriptor.DEFAULT_PREVIOUS_KEY, locale)); } return previousGAction; } private List<G> createWizardStepActions(IWizardStepDescriptor wizardStep, IView<E> view, IActionHandler actionHandler, ITranslationProvider translationProvider, Locale locale, IValueConnector modelConnector, Map<String, Object> context) { List<G> wizardStepActions = new ArrayList<>(); G previousGAction = createPreviousAction(wizardStep, actionHandler, view, translationProvider, locale, modelConnector, context); G nextGAction = createNextAction(wizardStep, actionHandler, view, translationProvider, locale, modelConnector, context); G cancelGAction = createCancelAction(wizardStep, actionHandler, view, locale, context); G finishGAction = createFinishAction(wizardStep, actionHandler, view, locale, context); wizardStepActions.add(previousGAction); wizardStepActions.add(nextGAction); wizardStepActions.add(finishGAction); wizardStepActions.add(cancelGAction); return wizardStepActions; } private void displayWizardStep(IWizardStepDescriptor wizardStep, IValueConnector modelConnector, IActionHandler actionHandler, Map<String, Object> context, boolean reuseCurrent) { ITranslationProvider translationProvider = getTranslationProvider(context); Locale locale = getLocale(context); IView<E> view = getViewFactory(context).createView( wizardStep.getViewDescriptor(), actionHandler, getLocale(context)); IModelDescriptor modelDescriptor = wizardStep.getViewDescriptor() .getModelDescriptor(); if (modelDescriptor instanceof IComponentDescriptor<?>) { for (IPropertyDescriptor propertyDescriptor : ((IComponentDescriptor<?>) modelDescriptor) .getPropertyDescriptors()) { if (propertyDescriptor instanceof IScalarPropertyDescriptor && ((IScalarPropertyDescriptor) propertyDescriptor) .getDefaultValue() != null) { Map<String, Object> wizardModel = modelConnector .getConnectorValue(); if (!wizardModel.containsKey(propertyDescriptor.getName())) { wizardModel.put(propertyDescriptor.getName(), ((IScalarPropertyDescriptor) propertyDescriptor) .getDefaultValue()); } } } } getMvcBinder(context).bind(view.getConnector(), modelConnector); String title = getI18nName(translationProvider, locale) + " - " + wizardStep.getI18nName(translationProvider, locale); Dimension dialogSize = getDialogSize(context); getController(context).displayModalDialog( view.getPeer(), createWizardStepActions(wizardStep, view, actionHandler, translationProvider, locale, modelConnector, context), title, getSourceComponent(context), context, dialogSize, reuseCurrent); // We must update the context context.putAll(getActionFactory(context).createActionContext(actionHandler, view, view.getConnector(), getActionCommand(context), getActionWidget(context))); } private Dimension getDialogSize(Map<String, Object> context) { Dimension dialogSize = (Dimension) context .get(ModalDialogAction.DIALOG_SIZE); if (width != null && height != null) { dialogSize = new Dimension(width, height); } return dialogSize; } private class CancelAction extends FrontendAction<E, F, G> { @SuppressWarnings("unused") private final IWizardStepDescriptor wizardStep; private final IDisplayableAction wrappedCancelAction; public CancelAction(IWizardStepDescriptor wizardStep, IDisplayableAction wrappedCancelAction) { this.wizardStep = wizardStep; this.wrappedCancelAction = wrappedCancelAction; } @Override public boolean execute(IActionHandler actionHandler, Map<String, Object> context) { actionHandler.execute(wrappedCancelAction, context); return super.execute(actionHandler, context); } /** * {@inheritDoc} */ @Override public String getDescription() { return wrappedCancelAction.getDescription(); } /** * {@inheritDoc} */ @Override public Icon getIcon() { return wrappedCancelAction.getIcon(); } /** * {@inheritDoc} */ @Override public String getName() { return wrappedCancelAction.getName(); } } private class FinishAction extends FrontendAction<E, F, G> { private final IWizardStepDescriptor wizardStep; private final IDisplayableAction wrappedFinishAction; public FinishAction(IWizardStepDescriptor wizardStep, IDisplayableAction wrappedFinishAction) { this.wizardStep = wizardStep; this.wrappedFinishAction = wrappedFinishAction; } @Override public boolean execute(IActionHandler actionHandler, Map<String, Object> context) { if (wizardStep.getOnLeaveAction() == null || actionHandler.execute(wizardStep.getOnLeaveAction(), context)) { setActionParameter(getViewConnector(context).getConnectorValue(), context); actionHandler.execute(wrappedFinishAction, context); } return super.execute(actionHandler, context); } /** * {@inheritDoc} */ @Override public String getDescription() { return wrappedFinishAction.getDescription(); } /** * {@inheritDoc} */ @Override public Icon getIcon() { return wrappedFinishAction.getIcon(); } /** * {@inheritDoc} */ @Override public String getName() { return wrappedFinishAction.getName(); } } private class NextAction extends FrontendAction<E, F, G> { private final IValueConnector modelConnector; private final IWizardStepDescriptor wizardStep; public NextAction(IWizardStepDescriptor wizardStep, IValueConnector modelConnector) { this.wizardStep = wizardStep; this.modelConnector = modelConnector; } @Override public boolean execute(IActionHandler actionHandler, Map<String, Object> context) { if (wizardStep.getOnLeaveAction() == null || actionHandler.execute(wizardStep.getOnLeaveAction(), context)) { IWizardStepDescriptor nextWizardStep = wizardStep .getNextStepDescriptor(context); displayWizardStep(nextWizardStep, modelConnector, actionHandler, context, true); if (nextWizardStep.getOnEnterAction() != null) { actionHandler.execute(nextWizardStep.getOnEnterAction(), context); } } return super.execute(actionHandler, context); } } private class PreviousAction extends FrontendAction<E, F, G> { private final IValueConnector modelConnector; private final IWizardStepDescriptor wizardStep; public PreviousAction(IWizardStepDescriptor wizardStep, IValueConnector modelConnector) { this.wizardStep = wizardStep; this.modelConnector = modelConnector; } @Override public boolean execute(IActionHandler actionHandler, Map<String, Object> context) { IWizardStepDescriptor previousWizardStep = wizardStep .getPreviousStepDescriptor(context); displayWizardStep(previousWizardStep, modelConnector, actionHandler, context, true); return super.execute(actionHandler, context); } } }