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.markup.html.form;
018
019import java.util.ArrayList;
020import java.util.Collection;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.StringTokenizer;
025
026import org.apache.wicket.MetaDataKey;
027import org.apache.wicket.markup.ComponentTag;
028import org.apache.wicket.model.IModel;
029import org.apache.wicket.util.convert.ConversionException;
030import org.apache.wicket.util.string.AppendingStringBuffer;
031import org.apache.wicket.util.string.Strings;
032
033
034/**
035 * A multiple choice list component.
036 * 
037 * @author Jonathan Locke
038 * @author Johan Compagner
039 * @author Martijn Dashorst
040 * 
041 * @param <T>
042 *            The model object type
043 */
044public class ListMultipleChoice<T> extends AbstractChoice<Collection<T>, T>
045{
046        private static final long serialVersionUID = 1L;
047
048        /** Meta key for the retain disabled flag */
049        static MetaDataKey<Boolean> RETAIN_DISABLED_META_KEY = new MetaDataKey<>()
050        {
051                private static final long serialVersionUID = 1L;
052        };
053
054        /** The default maximum number of rows to display. */
055        private static final int DEFAULT_MAX_ROWS = 8;
056
057        /** The maximum number of rows to display. */
058        private int maxRows = DEFAULT_MAX_ROWS;
059
060        /**
061         * @see org.apache.wicket.markup.html.form.AbstractChoice#AbstractChoice(String)
062         */
063        public ListMultipleChoice(final String id)
064        {
065                super(id);
066        }
067
068        /**
069         * @see org.apache.wicket.markup.html.form.AbstractChoice#AbstractChoice(String, List)
070         */
071        public ListMultipleChoice(final String id, final List<? extends T> choices)
072        {
073                super(id, choices);
074        }
075
076        /**
077         * Creates a multiple choice list with a maximum number of visible rows.
078         * 
079         * @param id
080         *            component id
081         * @param choices
082         *            list of choices
083         * @param maxRows
084         *            the maximum number of visible rows.
085         * @see org.apache.wicket.markup.html.form.AbstractChoice#AbstractChoice(String, List)
086         */
087        public ListMultipleChoice(final String id, final List<? extends T> choices, final int maxRows)
088        {
089                super(id, choices);
090                this.maxRows = maxRows;
091        }
092
093        /**
094         * @see org.apache.wicket.markup.html.form.AbstractChoice#AbstractChoice(String,
095         *      List,IChoiceRenderer)
096         */
097        public ListMultipleChoice(final String id, final List<? extends T> choices,
098                final IChoiceRenderer<? super T> renderer)
099        {
100                super(id, choices, renderer);
101        }
102
103        /**
104         * @see org.apache.wicket.markup.html.form.AbstractChoice#AbstractChoice(String, IModel, List)
105         * 
106         * @param id
107         * @param object
108         * @param choices
109         */
110        @SuppressWarnings("unchecked")
111        public ListMultipleChoice(final String id, IModel<? extends Collection<T>> object,
112                final List<? extends T> choices)
113        {
114                super(id, (IModel<Collection<T>>)object, choices);
115        }
116
117        /**
118         * @see org.apache.wicket.markup.html.form.AbstractChoice#AbstractChoice(String, IModel,
119         *      List,IChoiceRenderer)
120         * 
121         * @param id
122         * @param object
123         * @param choices
124         * @param renderer
125         */
126        @SuppressWarnings("unchecked")
127        public ListMultipleChoice(final String id, IModel<? extends Collection<T>> object,
128                final List<? extends T> choices, final IChoiceRenderer<? super T> renderer)
129        {
130                super(id, (IModel<Collection<T>>)object, choices, renderer);
131        }
132
133        /**
134         * @see org.apache.wicket.markup.html.form.AbstractChoice#AbstractChoice(String, IModel)
135         */
136        public ListMultipleChoice(String id, IModel<? extends List<? extends T>> choices)
137        {
138                super(id, choices);
139        }
140
141        /**
142         * @see org.apache.wicket.markup.html.form.AbstractChoice#AbstractChoice(String, IModel,IModel)
143         * 
144         * @param id
145         * @param model
146         * @param choices
147         */
148        @SuppressWarnings("unchecked")
149        public ListMultipleChoice(String id, IModel<? extends Collection<T>> model,
150                IModel<? extends List<? extends T>> choices)
151        {
152                super(id, (IModel<Collection<T>>)model, choices);
153        }
154
155        /**
156         * @see org.apache.wicket.markup.html.form.AbstractChoice#AbstractChoice(String,
157         *      IModel,IChoiceRenderer)
158         */
159        public ListMultipleChoice(String id, IModel<? extends List<? extends T>> choices,
160                IChoiceRenderer<? super T> renderer)
161        {
162                super(id, choices, renderer);
163        }
164
165
166        /**
167         * @see org.apache.wicket.markup.html.form.AbstractChoice#AbstractChoice(String, IModel,
168         *      IModel,IChoiceRenderer)
169         * 
170         * @param id
171         * @param model
172         * @param choices
173         * @param renderer
174         */
175        @SuppressWarnings("unchecked")
176        public ListMultipleChoice(String id, IModel<? extends Collection<T>> model,
177                IModel<? extends List<? extends T>> choices, IChoiceRenderer<? super T> renderer)
178        {
179                super(id, (IModel<Collection<T>>)model, choices, renderer);
180        }
181
182        /**
183         * Sets the number of visible rows in the listbox.
184         * 
185         * @param maxRows
186         *            the number of visible rows
187         * @return this
188         */
189        public final ListMultipleChoice<T> setMaxRows(final int maxRows)
190        {
191                this.maxRows = maxRows;
192                return this;
193        }
194
195        /**
196         * @see FormComponent#getModelValue()
197         */
198        @Override
199        public final String getModelValue()
200        {
201                final AppendingStringBuffer buffer = new AppendingStringBuffer();
202
203                final Collection<T> selectedValues = getModelObject();
204                if (selectedValues != null)
205                {
206                        final List<? extends T> choices = getChoices();
207                        for (T object : selectedValues)
208                        {
209                                if (buffer.length() > 0)
210                                {
211                                        buffer.append(VALUE_SEPARATOR);
212                                }
213                                int index = choices.indexOf(object);
214                                buffer.append(getChoiceRenderer().getIdValue(object, index));
215                        }
216                }
217
218                return buffer.toString();
219        }
220
221        /**
222         * @see org.apache.wicket.markup.html.form.AbstractChoice#isSelected(Object,int, String)
223         */
224        @Override
225        protected final boolean isSelected(T choice, int index, String selected)
226        {
227                // Have a value at all?
228                if (selected != null)
229                {
230                        String idValue = getChoiceRenderer().getIdValue(choice, index);
231                        // Loop through ids
232                        for (final StringTokenizer tokenizer = new StringTokenizer(selected, VALUE_SEPARATOR); tokenizer.hasMoreTokens();)
233                        {
234                                final String id = tokenizer.nextToken();
235                                if (id.equals(idValue))
236                                {
237                                        return true;
238                                }
239                        }
240                }
241                return false;
242        }
243
244        /**
245         * @see org.apache.wicket.Component#onComponentTag(ComponentTag)
246         */
247        @Override
248        protected void onComponentTag(final ComponentTag tag)
249        {
250                super.onComponentTag(tag);
251                tag.put("multiple", "multiple");
252
253                if (!tag.getAttributes().containsKey("size"))
254                {
255                        tag.put("size", Math.min(maxRows, getChoices().size()));
256                }
257        }
258
259        /**
260         * @see org.apache.wicket.markup.html.form.FormComponent#convertValue(String[])
261         */
262        @Override
263        protected Collection<T> convertValue(String[] ids) throws ConversionException
264        {
265                if (ids != null && ids.length > 0 && !Strings.isEmpty(ids[0]))
266                {
267                        return convertChoiceIdsToChoices(ids);
268                }
269                else
270                {
271                        ArrayList<T> result = new ArrayList<>();
272                        addRetainedDisabled(result);
273                        return result;
274                }
275        }
276
277        /**
278         * Converts submitted choice ids to choice objects.
279         * 
280         * @param ids
281         *            choice ids. this array is nonnull and always contains at least one id.
282         * @return list of choices.
283         */
284        protected List<T> convertChoiceIdsToChoices(String[] ids)
285        {
286                ArrayList<T> selectedValues = new ArrayList<>();
287
288                // If one or more ids is selected
289                if (ids != null && ids.length > 0 && !Strings.isEmpty(ids[0]))
290                {
291                        // Get values that could be selected
292                        final Map<String, T> choiceIds2choiceValues = createChoicesIdsMap();
293
294                        // Loop through selected indices
295                        for (String id : ids)
296                        {
297                                if (choiceIds2choiceValues.containsKey(id))
298                                {
299                                        selectedValues.add(choiceIds2choiceValues.get(id));
300                                }
301                        }
302                }
303                addRetainedDisabled(selectedValues);
304
305                return selectedValues;
306
307        }
308
309        /**
310         * Creates a map of choice IDs to choice values. This map can be used to speed up lookups e.g.
311         * in {@link #convertChoiceIdsToChoices(String[])}. <strong>Do not store the result of this
312         * method.</strong> The choices list can change between requests so this map <em>must</em> be
313         * regenerated.
314         * 
315         * @return a map.
316         */
317        private Map<String, T> createChoicesIdsMap()
318        {
319                final List<? extends T> choices = getChoices();
320
321                final Map<String, T> choiceIds2choiceValues = new HashMap<String, T>(choices.size(), 1);
322
323                for (int index = 0; index < choices.size(); index++)
324                {
325                        // Get next choice
326                        final T choice = choices.get(index);
327                        choiceIds2choiceValues.put(getChoiceRenderer().getIdValue(choice, index), choice);
328                }
329                return choiceIds2choiceValues;
330        }
331
332        private void addRetainedDisabled(ArrayList<T> selectedValues)
333        {
334                if (isRetainDisabledSelected())
335                {
336                        Collection<T> unchangedModel = getModelObject();
337                        String selected;
338                        {
339                                StringBuilder builder = new StringBuilder();
340                                for (T t : unchangedModel)
341                                {
342                                        builder.append(t);
343                                        builder.append(';');
344                                }
345                                selected = builder.toString();
346                        }
347                        List<? extends T> choices = getChoices();
348                        for (int i = 0; i < choices.size(); i++)
349                        {
350                                final T choice = choices.get(i);
351                                if (isDisabled(choice, i, selected))
352                                {
353                                        if (unchangedModel.contains(choice))
354                                        {
355                                                if (!selectedValues.contains(choice))
356                                                {
357                                                        selectedValues.add(choice);
358                                                }
359                                        }
360                                }
361                        }
362                }
363        }
364
365        /**
366         * See {@link FormComponent#updateCollectionModel(FormComponent)} for details on how the model
367         * is updated.
368         */
369        @Override
370        public void updateModel()
371        {
372                FormComponent.updateCollectionModel(this);
373        }
374
375        /**
376         * If true, choices that were selected in the model but disabled in rendering will be retained
377         * in the model after a form submit. Example: Choices are [1, 2, 3, 4]. Model collection is [2,
378         * 4]. In rendering, choices 2 and 3 are disabled ({@link #isDisabled(Object, int, String)}).
379         * That means that four checkboxes are rendered, checkboxes 2 and 4 are checked, but 2 and 3 are
380         * not clickable. User checks 1 and unchecks 4. If this flag is off, the model will be updated
381         * to [1]. This is because the browser does not re-submit a disabled checked checkbox: it only
382         * submits [1]. Therefore Wicket will only see the [1] and update the model accordingly. If you
383         * set this flag to true, Wicket will check the model before updating to find choices that were
384         * selected but disabled. These choices will then be retained, leading to a new model value of
385         * [1, 2] as (probably) expected by the user. Note that this will lead to additional calls to
386         * {@link #isDisabled(Object, int, String)}.
387         * 
388         * @return flag
389         */
390        public boolean isRetainDisabledSelected()
391        {
392                Boolean flag = getMetaData(RETAIN_DISABLED_META_KEY);
393                return (flag != null && flag);
394        }
395
396        /**
397         * @param retain
398         *            flag
399         * @return this
400         * @see #isRetainDisabledSelected()
401         */
402        public ListMultipleChoice<T> setRetainDisabledSelected(boolean retain)
403        {
404                setMetaData(RETAIN_DISABLED_META_KEY, (retain) ? true : null);
405                return this;
406        }
407}