001/**
002 * Copyright 2005-2014 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.krad.uif.element;
017
018import org.apache.commons.collections.CollectionUtils;
019import org.apache.commons.lang3.StringUtils;
020import org.kuali.rice.krad.datadictionary.parse.BeanTag;
021import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute;
022import org.kuali.rice.krad.messages.MessageService;
023import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
024import org.kuali.rice.krad.uif.CssConstants;
025import org.kuali.rice.krad.uif.UifConstants;
026import org.kuali.rice.krad.uif.util.LifecycleElement;
027
028import java.util.ArrayList;
029import java.util.Collection;
030import java.util.Iterator;
031import java.util.LinkedHashMap;
032import java.util.List;
033import java.util.Map;
034
035/**
036 * Element which shows a visual progress bar alongside a set of steps, to be used by wizard or multi-step
037 * processes, which reflects the current progress based on value of currentStep.
038 *
039 * @author Kuali Rice Team (rice.collab@kuali.org)
040 */
041@BeanTag(name = "stepProgressBar-bean", parent = "Uif-StepProgressBar")
042public class StepProgressBar extends ProgressBar {
043    private static final long serialVersionUID = 1053164737424481519L;
044
045    private Map<String, String> steps;
046    private List<String> stepLabelClasses;
047    private List<String> accessibilityText;
048
049    private String currentStep;
050    private String completeStep;
051
052    private Integer verticalHeight;
053    private Integer verticalStepHeight;
054
055    public StepProgressBar() {
056        steps = new LinkedHashMap<String, String>();
057        accessibilityText = new ArrayList<String>();
058        stepLabelClasses = new ArrayList<String>();
059    }
060
061    /**
062     * Populates the stepLabelClasses, accessibilityText, segmentSizes, and segmentClasses lists based on the settings
063     * of this StepProgressBar.
064     *
065     * {@inheritDoc}
066     */
067    @Override
068    public void performFinalize(Object model, LifecycleElement parent) {
069        // If a percentageComplete value is set, use it to try to determine the current step, otherwise if currentStep
070        // is set just use that (null percentComplete value)
071        if (this.getPercentComplete() != null && currentStep == null) {
072            calculateCurrentStepFromPercentage();
073        } else if (currentStep != null) {
074            this.setPercentComplete(null);
075        }
076
077        super.performFinalize(model, parent);
078
079        MessageService messageService = KRADServiceLocatorWeb.getMessageService();
080
081        // Initializing and checking for validity of values:
082        String cssDimension = CssConstants.WIDTH;
083        if (this.isVertical()) {
084            cssDimension = CssConstants.HEIGHT;
085            this.addStyleClass(CssConstants.ProgressBar.VERTICAL_STEP_PROGRESS_BAR);
086        }
087
088        int totalSteps = steps.size();
089        if (totalSteps == 0) {
090            throw new RuntimeException(
091                    "At least one step is required for a StepProgressBar: " + this.getId() + " with parent: " + parent
092                            .getId());
093        }
094
095        boolean explicitlySetPercentages = CollectionUtils.isNotEmpty(getSegmentPercentages());
096        boolean explicitlySetClasses = CollectionUtils.isNotEmpty(this.getSegmentClasses());
097        if (explicitlySetPercentages && explicitlySetClasses && this.getSegmentClasses().size() != this
098                .getSegmentPercentages().size()) {
099            throw new RuntimeException(
100                    "If segmentPercentages are set on a StepProgressBar type, and segmentClasses are also "
101                            + "set, the lists MUST contain the same number of items");
102        }
103
104        // Populate the information used by the template based on settings of this StepProgressBar
105        populateProgressBarRenderingLists(totalSteps, cssDimension, explicitlySetPercentages, explicitlySetClasses);
106
107        // Explicitly set the vertical height for vertical cases where the verticalHeight is not set using
108        // verticalStepHeight
109        if (this.isVertical() && getVerticalHeight() == null) {
110            setVerticalHeight(getSegmentSizes().size() * verticalStepHeight);
111        }
112
113        // If the step is considered complete, set the aria attributes appropriately
114        if (currentStep != null && currentStep.equals(completeStep)) {
115            this.addAriaAttribute(UifConstants.AriaAttributes.VALUE_NOW, Integer.toString(steps.size()));
116            this.addAriaAttribute(UifConstants.AriaAttributes.VALUE_TEXT, messageService.getMessageText(
117                    "accessibility.progressBar.complete"));
118        }
119
120        // Add aria attributes
121        this.addAriaAttribute(UifConstants.AriaAttributes.VALUE_MIN, "0");
122        this.addAriaAttribute(UifConstants.AriaAttributes.VALUE_MAX, Integer.toString(totalSteps));
123    }
124
125    /**
126     * Calculate the current step based on a percentage value.
127     *
128     * @return the current step key which is at that percentage of total steps
129     */
130    private String calculateCurrentStepFromPercentage() {
131        if (getPercentComplete() == 0) {
132            return "";
133        } else if (getPercentComplete() == 100) {
134            return completeStep;
135        }
136
137        int size = steps.size();
138        double currentStep = Math.ceil(size * this.getPercentComplete());
139
140        String key = "";
141        Iterator<String> stepIterator = steps.keySet().iterator();
142        for (int step = 0; stepIterator.hasNext() && step <= currentStep; step++) {
143            key = stepIterator.next();
144        }
145
146        this.setPercentComplete(null);
147
148        return key;
149    }
150
151    /**
152     * Populate the information used by the template based on settings of this StepProgressBar by iterating of
153     * the steps and setting classes and other rendering info in list to be used by the template.
154     *
155     * @param totalSteps the total steps in this StepProgressBar
156     * @param cssDimension the css dimension property to use for bar sizes
157     * @param explicitlySetPercentages true if bar percentages were manually set
158     * @param explicitlySetClasses true if bar classes wer manually set
159     */
160    public void populateProgressBarRenderingLists(int totalSteps, String cssDimension, boolean explicitlySetPercentages,
161            boolean explicitlySetClasses) {
162        MessageService messageService = KRADServiceLocatorWeb.getMessageService();
163
164        double percentage = Math.floor(100 / totalSteps);
165        double percentTotal = 0;
166        boolean currentStepFound = false;
167
168        // Bar is considered empty if currentStep is not set or is something that does not match a key
169        // so set currentStepFound to true to force the following loop to only create "empty" bars
170        if (StringUtils.isBlank(currentStep) || (!steps.containsKey(currentStep) && !currentStep.equals(
171                completeStep))) {
172            currentStepFound = true;
173        }
174
175        Iterator<String> stepIterator = steps.keySet().iterator();
176        for (int step = 0; stepIterator.hasNext() && step <= totalSteps; step++) {
177            String stepKey = stepIterator.next();
178
179            double stepPercentage;
180
181            // Retrieve/calculate the current stepPercentage and current percentageTotal of bars being processed
182            if (explicitlySetPercentages) {
183                Integer percentageValue = getSegmentPercentages().get(step);
184                stepPercentage = percentageValue;
185                percentTotal += percentageValue;
186            } else {
187                stepPercentage = percentage;
188                percentTotal += percentage;
189            }
190
191            // if there is some missing width to make 100% due to uneven division and we are on the final iteration,
192            // give the additional percentage to the last bar
193            if (!stepIterator.hasNext() && percentTotal < 100) {
194                stepPercentage = (percentage + (100 - percentTotal));
195                percentTotal = 100;
196            }
197
198            String dimensionValue = stepPercentage + "%";
199
200            // Default bar styles and screen reader text
201            String cssClasses =
202                    CssConstants.ProgressBar.PROGRESS_BAR + " " + CssConstants.ProgressBar.SUCCESS_PROGRESS_BAR;
203            String labelCssClasses = CssConstants.ProgressBar.STEP_LABEL + " " + CssConstants.ProgressBar.COMPLETE;
204            String srText = messageService.getMessageText("accessibility.progressBar.completeStep");
205
206            // If current step, change styles and text appropriately.  When the step has already be found,
207            // the final bars are considered empty/incomplete steps
208            if (stepKey.equals(currentStep)) {
209                currentStepFound = true;
210                cssClasses = CssConstants.ProgressBar.PROGRESS_BAR + " " + CssConstants.ProgressBar.INFO_PROGRESS_BAR;
211                labelCssClasses = CssConstants.ProgressBar.STEP_LABEL + " " + CssConstants.ProgressBar.ACTIVE;
212                srText = messageService.getMessageText("accessibility.progressBar.currentStep");
213
214                // Set aria attributes for the current value
215                this.addAriaAttribute(UifConstants.AriaAttributes.VALUE_NOW, Integer.toString(step));
216                this.addAriaAttribute(UifConstants.AriaAttributes.VALUE_TEXT, srText + steps.get(stepKey));
217            } else if (currentStepFound) {
218                cssClasses = CssConstants.ProgressBar.PROGRESS_BAR + " " + CssConstants.ProgressBar.EMPTY_PROGRESS_BAR;
219                labelCssClasses = CssConstants.ProgressBar.STEP_LABEL;
220                srText = messageService.getMessageText("accessibility.progressBar.futureStep");
221            }
222
223            this.getSegmentSizes().add(cssDimension + dimensionValue);
224
225            // Don't add default classes if custom classes have been set for the bars
226            if (!explicitlySetClasses) {
227                this.getSegmentClasses().add(cssClasses);
228            }
229
230            this.getStepLabelClasses().add(labelCssClasses);
231            this.accessibilityText.add(srText);
232        }
233    }
234
235    /**
236     * The steps as key-value pairs for this StepProgressBar, where value is human-readable text.
237     *
238     * @return the map of steps for this StepProgressBar
239     */
240    @BeanTagAttribute(name = "steps", type = BeanTagAttribute.AttributeType.MAPVALUE)
241    public Map<String, String> getSteps() {
242        return steps;
243    }
244
245    /**
246     * @see StepProgressBar#getSteps()
247     */
248    public void setSteps(Map<String, String> steps) {
249        this.steps = steps;
250    }
251
252    /**
253     * The list of step values; framework only, not settable.
254     *
255     * @return the list of step values
256     */
257    public Collection<String> getStepCollection() {
258        return steps.values();
259    }
260
261    /**
262     * The list of step label css classes in order of steps shown; framework only, not settable
263     *
264     * @return the list of step label css classes
265     */
266    public List<String> getStepLabelClasses() {
267        return stepLabelClasses;
268    }
269
270    /**
271     * The list of additional screen reader only accessibility text to render per step, in order; framework only,
272     * not settable.
273     *
274     * @return the list of additional screen reader only accessibility text
275     */
276    public List<String> getAccessibilityText() {
277        return accessibilityText;
278    }
279
280    /**
281     * The current step (by key) of this progress bar to be highlighted visually as the active step.
282     *
283     * @return the current step (by key)
284     */
285    @BeanTagAttribute(name = "currentStep")
286    public String getCurrentStep() {
287        return currentStep;
288    }
289
290    /**
291     * @see StepProgressBar#getCurrentStep()
292     */
293    public void setCurrentStep(String currentStep) {
294        this.currentStep = currentStep;
295    }
296
297    /**
298     * The key that when currentStep has this value, shows the step progress bar as fully complete; this key
299     * is must not be part of the steps being shown, by default this has a value of "SUCCESS".
300     *
301     * @return the completeStep key for showing this bar as fully complete
302     */
303    @BeanTagAttribute(name = "completeStep")
304    public String getCompleteStep() {
305        return completeStep;
306    }
307
308    /**
309     * @see org.kuali.rice.krad.uif.element.StepProgressBar#getCompleteStep()
310     */
311    public void setCompleteStep(String completeStep) {
312        this.completeStep = completeStep;
313    }
314
315    /**
316     * The height (in pixels) of the progress bar portion of this component, if this is not set, verticalStepHeight
317     * is used to calculate this value; only used when vertical property is true.
318     *
319     * @return the verticalHeight of the progress bar
320     */
321    @BeanTagAttribute(name = "verticalHeight")
322    public Integer getVerticalHeight() {
323        return verticalHeight;
324    }
325
326    /**
327     * @see StepProgressBar#getVerticalHeight()
328     */
329    public void setVerticalHeight(Integer verticalHeight) {
330        this.verticalHeight = verticalHeight;
331    }
332
333    /**
334     * The height (in pixels) allocated for each step for vertical step display used to calculate verticalHeight if not
335     * set, by default this is 75.
336     *
337     * @return the vertical step height used to calculate verticalHeight
338     */
339    @BeanTagAttribute(name = "verticalStepHeight")
340    public Integer getVerticalStepHeight() {
341        return verticalStepHeight;
342    }
343
344    /**
345     * @see StepProgressBar#getVerticalStepHeight()
346     */
347    public void setVerticalStepHeight(Integer verticalStepHeight) {
348        this.verticalStepHeight = verticalStepHeight;
349    }
350}