001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.extensions.wizard;
018
019import java.util.ArrayDeque;
020import java.util.ArrayList;
021import java.util.Collections;
022import java.util.Iterator;
023import java.util.List;
024
025import org.apache.wicket.util.io.IClusterable;
026
027
028/**
029 * Default implementation of {@link IWizardModel}, which models a semi-static wizard. This means
030 * that all steps should be known upfront, and added to the model on construction. Steps can be
031 * optional by using {@link ICondition}. The wizard is initialized with a wizard model through
032 * calling method {@link Wizard#init(IWizardModel)}.
033 * <p>
034 * Steps can be added to this model directly using either the {@link #add(IWizardStep) normal add
035 * method} or {@link #add(IWizardStep, ICondition) the conditional add method}.
036 * </p>
037 * 
038 * <p>
039 * <a href="https://wizard-framework.dev.java.net/">Swing Wizard Framework</a> served as a valuable
040 * source of inspiration.
041 * </p>
042 * 
043 * @author Eelco Hillenius
044 */
045public class WizardModel extends AbstractWizardModel
046{
047        /**
048         * Interface for conditional displaying of wizard steps.
049         */
050        @FunctionalInterface
051        public interface ICondition extends IClusterable
052        {
053                /**
054                 * Evaluates the current state and returns whether the step that is coupled to this
055                 * condition is available.
056                 * 
057                 * @return True if the step this condition is coupled to is available, false otherwise
058                 */
059                public boolean evaluate();
060        }
061
062        /**
063         * Condition that always evaluates true.
064         */
065        public static final ICondition TRUE = (() -> true);
066
067        private static final long serialVersionUID = 1L;
068
069        /** The currently active step. */
070        private IWizardStep activeStep;
071
072        /** Conditions with steps. */
073        private final List<ICondition> conditions = new ArrayList<>();
074
075        /** State history. */
076        private final ArrayDeque<IWizardStep> history = new ArrayDeque<>();
077
078        /** The wizard steps. */
079        private final List<IWizardStep> steps = new ArrayList<>();
080
081        /**
082         * Construct.
083         */
084        public WizardModel()
085        {
086        }
087
088        /**
089         * Adds the next step to the wizard. If the {@link WizardStep} implements {@link ICondition},
090         * then this method is equivalent to calling {@link #add(IWizardStep, ICondition) add(step,
091         * (ICondition)step)}.
092         * 
093         * @param step
094         *            the step to added.
095         */
096        public void add(final IWizardStep step)
097        {
098                if (step instanceof ICondition)
099                {
100                        add(step, (ICondition)step);
101                }
102                else
103                {
104                        add(step, TRUE);
105                }
106        }
107
108        /**
109         * Adds an optional step to the model. The step will only be displayed if the specified
110         * condition is met.
111         * 
112         * @param step
113         *            The step to add
114         * @param condition
115         *            the {@link ICondition} under which it should be included in the wizard.
116         */
117        public void add(final IWizardStep step, final ICondition condition)
118        {
119                steps.add(step);
120                conditions.add(condition);
121        }
122
123        /**
124         * Gets the current active step the wizard should display.
125         * 
126         * @return the active step.
127         */
128        @Override
129        public final IWizardStep getActiveStep()
130        {
131                return activeStep;
132        }
133
134        /**
135         * Checks if the last button should be enabled.
136         * 
137         * @return <tt>true</tt> if the last button should be enabled, <tt>false</tt> otherwise.
138         * @see IWizardModel#isLastVisible
139         */
140        @Override
141        public boolean isLastAvailable()
142        {
143                return allStepsComplete() && !isLastStep(activeStep);
144        }
145
146        /**
147         * @see org.apache.wicket.extensions.wizard.IWizardModel#isLastStep(org.apache.wicket.extensions.wizard.IWizardStep)
148         */
149        @Override
150        public boolean isLastStep(final IWizardStep step)
151        {
152                return findLastStep().equals(step);
153        }
154
155        /**
156         * Checks if the next button should be enabled.
157         * 
158         * @return <tt>true</tt> if the next button should be enabled, <tt>false</tt> otherwise.
159         */
160        @Override
161        public boolean isNextAvailable()
162        {
163                return activeStep.isComplete() && !isLastStep(activeStep);
164        }
165
166        /**
167         * Checks if the previous button should be enabled.
168         * 
169         * @return <tt>true</tt> if the previous button should be enabled, <tt>false</tt> otherwise.
170         */
171        @Override
172        public boolean isPreviousAvailable()
173        {
174                return !history.isEmpty();
175        }
176
177        /**
178         * @see org.apache.wicket.extensions.wizard.IWizardModel#last()
179         */
180        @Override
181        public void last()
182        {
183                history.push(getActiveStep());
184                IWizardStep lastStep = findLastStep();
185                setActiveStep(lastStep);
186        }
187
188        /**
189         * @see org.apache.wicket.extensions.wizard.IWizardModel#next()
190         */
191        @Override
192        public void next()
193        {
194                history.push(getActiveStep());
195                IWizardStep step = findNextVisibleStep();
196                setActiveStep(step);
197        }
198
199        /**
200         * @see org.apache.wicket.extensions.wizard.IWizardModel#previous()
201         */
202        @Override
203        public void previous()
204        {
205                IWizardStep step = history.pop();
206                setActiveStep(step);
207        }
208
209        /**
210         * @see org.apache.wicket.extensions.wizard.IWizardModel#reset()
211         */
212        @Override
213        public void reset()
214        {
215                history.clear();
216                activeStep = null;
217                
218                for (IWizardStep step : steps)
219                {
220                        step.init(this);
221                }
222
223                setActiveStep(findNextVisibleStep());
224        }
225
226        /**
227         * Sets the active step.
228         * 
229         * @param step
230         *            the new active step step.
231         */
232        public void setActiveStep(final IWizardStep step)
233        {
234                if ((activeStep != null) && (step != null) && activeStep.equals(step))
235                {
236                        return;
237                }
238
239                activeStep = step;
240
241                fireActiveStepChanged(step);
242        }
243
244        /**
245         * @see IWizardModel#stepIterator()
246         */
247        @Override
248        public Iterator<IWizardStep> stepIterator()
249        {
250                List<IWizardStep> filtered = new ArrayList<>();
251                
252                for (int s = 0; s < steps.size(); s++) {
253                        if (conditions.get(s).evaluate()) {
254                                filtered.add(steps.get(s));
255                        }
256                }
257                
258                return filtered.iterator();
259        }
260
261        /**
262         * Returns true if all the steps in the wizard return <tt>true</tt> from
263         * {@link IWizardStep#isComplete}. This is primarily used to determine if the last button can be
264         * enabled.
265         * 
266         * @return <tt>true</tt> if all the steps in the wizard are complete, <tt>false</tt> otherwise.
267         */
268        protected final boolean allStepsComplete()
269        {
270                for (IWizardStep step : steps)
271                {
272                        if (!step.isComplete())
273                        {
274                                return false;
275                        }
276                }
277
278                return true;
279        }
280
281        /**
282         * Finds the last step in this model.
283         * 
284         * @return The last step
285         */
286        protected final IWizardStep findLastStep()
287        {
288                for (int i = conditions.size() - 1; i >= 0; i--)
289                {
290                        ICondition condition = conditions.get(i);
291                        if (condition.evaluate())
292                        {
293                                return steps.get(i);
294                        }
295                }
296
297                throw new IllegalStateException("Wizard contains no visible steps");
298        }
299
300        /**
301         * Finds the next visible step based on the active step.
302         * 
303         * @return The next visible step based on the active step
304         */
305        protected final IWizardStep findNextVisibleStep()
306        {
307                int startIndex = (activeStep == null) ? 0 : steps.indexOf(activeStep) + 1;
308
309                for (int i = startIndex; i < conditions.size(); i++)
310                {
311                        ICondition condition = conditions.get(i);
312                        if (condition.evaluate())
313                        {
314                                return steps.get(i);
315                        }
316                }
317
318                throw new IllegalStateException("Wizard contains no more visible steps");
319        }
320
321        /**
322         * Gets conditions.
323         * 
324         * @return unmodifiable list of conditions
325         */
326        public List<ICondition> getConditions()
327        {
328                return Collections.unmodifiableList(conditions);
329        }
330
331}