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.List;
021
022import org.apache.wicket.markup.ComponentTag;
023import org.apache.wicket.markup.MarkupStream;
024import org.apache.wicket.model.IModel;
025import org.apache.wicket.model.util.ListModel;
026import org.apache.wicket.util.convert.IConverter;
027import org.apache.wicket.util.string.AppendingStringBuffer;
028import org.apache.wicket.util.string.Strings;
029
030
031/**
032 * Abstract base class for all choice (html select) options.
033 * <p>
034 *     This component uses String concatenation to keep its memory footprint light.
035 *     Use Select, SelectOptions and SelectOption from wicket-extensions for more
036 *     sophisticated needs.
037 * </p>
038 * 
039 * @author Jonathan Locke
040 * @author Eelco Hillenius
041 * @author Johan Compagner
042 * 
043 * @param <T>
044 *            The model object type
045 * 
046 * @param <E>
047 *            class of a single element in the choices list
048 */
049public abstract class AbstractChoice<T, E> extends FormComponent<T>
050{
051        private static final long serialVersionUID = 1L;
052
053        /**
054         * An enumeration of possible positions of the label for a choice
055         */
056        public enum LabelPosition
057        {
058                /**
059                 * will render the label before the choice
060                 */
061                BEFORE {
062                        @Override
063                        void before(AppendingStringBuffer buffer, String idAttr, StringBuilder extraLabelAttributes, CharSequence renderValue)
064                        {
065                                buffer.append("<label for=\"")
066                                .append(Strings.escapeMarkup(idAttr))
067                                .append('"')
068                                .append(extraLabelAttributes)
069                                .append('>')
070                                .append(renderValue)
071                                .append("</label>");
072                        }
073                },
074
075                /**
076                 * will render the label after the choice
077                 */
078                AFTER {
079                        @Override
080                        void after(AppendingStringBuffer buffer, String idAttr, StringBuilder extraLabelAttributes, CharSequence renderValue)
081                        {
082                                buffer.append("<label for=\"")
083                                .append(Strings.escapeMarkup(idAttr))
084                                .append('"')
085                                .append(extraLabelAttributes)
086                                .append('>')
087                                .append(renderValue)
088                                .append("</label>");
089                        }
090                },
091
092                /**
093                 * render the label around and the text will be before the the choice
094                 */
095                WRAP_BEFORE {
096                        @Override
097                        void before(AppendingStringBuffer buffer, String idAttr, StringBuilder extraLabelAttributes, CharSequence renderValue)
098                        {
099                                buffer.append("<label")
100                                .append(extraLabelAttributes)
101                                .append('>')
102                                .append(renderValue)
103                                .append(' ');
104                        }
105                        
106                        @Override
107                        void after(AppendingStringBuffer buffer, String idAttr, StringBuilder extraLabelAttributes, CharSequence renderValue)
108                        {
109                                buffer.append("</label>");
110                        }
111                },
112
113                /**
114                 * render the label around and the text will be after the the choice
115                 */
116                WRAP_AFTER {
117                        @Override
118                        void before(AppendingStringBuffer buffer, String idAttr, StringBuilder extraLabelAttributes, CharSequence renderValue)
119                        {
120                                buffer.append("<label")
121                                .append(extraLabelAttributes)
122                                .append('>');
123                        }
124                        
125                        @Override
126                        void after(AppendingStringBuffer buffer, String idAttr, StringBuilder extraLabelAttributes, CharSequence renderValue)
127                        {
128                                buffer.append(' ')
129                                .append(renderValue)
130                                .append("</label>");
131                        }
132                };
133                
134                void before(AppendingStringBuffer buffer, String idAttr, StringBuilder extraLabelAttributes, CharSequence renderValue) {
135                }
136                
137                void after(AppendingStringBuffer buffer, String idAttr, StringBuilder extraLabelAttributes, CharSequence renderValue) {
138                }
139        }
140
141        /** The list of objects. */
142        private IModel<? extends List<? extends E>> choices;
143
144        /** The renderer used to generate display/id values for the objects. */
145        private IChoiceRenderer<? super E> renderer;
146
147        /**
148         * Constructor.
149         * 
150         * @param id
151         *            See Component
152         */
153        public AbstractChoice(final String id)
154        {
155                this(id, new ListModel<>(new ArrayList<E>()), new ChoiceRenderer<E>());
156        }
157
158        /**
159         * Constructor.
160         * 
161         * @param id
162         *            See Component
163         * @param choices
164         *            The collection of choices in the dropdown
165         */
166        public AbstractChoice(final String id, final List<? extends E> choices)
167        {
168                this(id, new ListModel<>(choices), new ChoiceRenderer<E>());
169        }
170
171        /**
172         * Constructor.
173         * 
174         * @param id
175         *            See Component
176         * @param renderer
177         *            The rendering engine
178         * @param choices
179         *            The collection of choices in the dropdown
180         */
181        public AbstractChoice(final String id, final List<? extends E> choices,
182                final IChoiceRenderer<? super E> renderer)
183        {
184                this(id, new ListModel<>(choices), renderer);
185        }
186
187        /**
188         * Constructor.
189         * 
190         * @param id
191         *            See Component
192         * @param model
193         *            See Component
194         * @param choices
195         *            The collection of choices in the dropdown
196         */
197        public AbstractChoice(final String id, IModel<T> model, final List<? extends E> choices)
198        {
199                this(id, model, new ListModel<>(choices), new ChoiceRenderer<>());
200        }
201
202        /**
203         * Constructor.
204         * 
205         * @param id
206         *            See Component
207         * @param model
208         *            See Component
209         * @param choices
210         *            The drop down choices
211         * @param renderer
212         *            The rendering engine
213         */
214        public AbstractChoice(final String id, IModel<T> model, final List<? extends E> choices,
215                final IChoiceRenderer<? super E> renderer)
216        {
217                this(id, model, new ListModel<>(choices), renderer);
218        }
219
220        /**
221         * Constructor.
222         * 
223         * @param id
224         *            See Component
225         * @param choices
226         *            The collection of choices in the dropdown
227         */
228        public AbstractChoice(final String id, final IModel<? extends List<? extends E>> choices)
229        {
230                this(id, choices, new ChoiceRenderer<E>());
231        }
232
233        /**
234         * Constructor.
235         * 
236         * @param id
237         *            See Component
238         * @param renderer
239         *            The rendering engine
240         * @param choices
241         *            The collection of choices in the dropdown
242         */
243        public AbstractChoice(final String id, final IModel<? extends List<? extends E>> choices,
244                final IChoiceRenderer<? super E> renderer)
245        {
246                super(id);
247                this.choices = wrap(choices);
248                setChoiceRenderer(renderer);
249        }
250
251        /**
252         * Constructor.
253         * 
254         * @param id
255         *            See Component
256         * @param model
257         *            See Component
258         * @param choices
259         *            The collection of choices in the dropdown
260         */
261        public AbstractChoice(final String id, IModel<T> model,
262                final IModel<? extends List<? extends E>> choices)
263        {
264                this(id, model, choices, new ChoiceRenderer<>());
265        }
266
267        /**
268         * Constructor.
269         * 
270         * @param id
271         *            See Component
272         * @param model
273         *            See Component
274         * @param renderer
275         *            The rendering engine
276         * @param choices
277         *            The drop down choices
278         */
279        public AbstractChoice(final String id, IModel<T> model,
280                final IModel<? extends List<? extends E>> choices, final IChoiceRenderer<? super E> renderer)
281        {
282                super(id, model);
283                this.choices = wrap(choices);
284                setChoiceRenderer(renderer);
285        }
286
287        /**
288         * @return The collection of object that this choice has
289         */
290        public final List<? extends E> getChoices()
291        {
292                IModel<? extends List<? extends E>> choicesModel = getChoicesModel();
293                List<? extends E> choices = (choicesModel != null) ? choicesModel.getObject() : null;
294                if (choices == null)
295                {
296                        throw new NullPointerException(
297                                "List of choices is null - Was the supplied 'Choices' model empty?");
298                }
299                return choices;
300        }
301
302        /**
303         * @return The model with the choices for this component
304         */
305        public IModel<? extends List<? extends  E>> getChoicesModel()
306        {
307                return this.choices;
308        }
309
310        /**
311         * Sets the list of choices
312         * 
313         * @param choices
314         *            model representing the list of choices
315         * @return this for chaining
316         */
317        public final AbstractChoice<T, E> setChoices(IModel<? extends List<? extends E>> choices)
318        {
319                if (this.choices != null && this.choices != choices)
320                {
321                        if (isVersioned())
322                        {
323                                addStateChange();
324                        }
325                }
326                this.choices = wrap(choices);
327                return this;
328        }
329
330        /**
331         * Sets the list of choices.
332         * 
333         * @param choices
334         *            the list of choices
335         * @return this for chaining
336         */
337        public final AbstractChoice<T, E> setChoices(List<? extends E> choices)
338        {
339                if ((this.choices != null))
340                {
341                        if (isVersioned())
342                        {
343                                addStateChange();
344                        }
345                }
346                this.choices = new ListModel<>(choices);
347                return this;
348        }
349
350        /**
351         * @return The IChoiceRenderer used for rendering the data objects
352         */
353        public final IChoiceRenderer<? super E> getChoiceRenderer()
354        {
355                return renderer;
356        }
357
358        /**
359         * Set the choice renderer to be used.
360         * 
361         * @param renderer
362         *              The IChoiceRenderer used for rendering the data objects
363         * @return this for chaining
364         */
365        public final AbstractChoice<T, E> setChoiceRenderer(IChoiceRenderer<? super E> renderer)
366        {
367                if (renderer == null)
368                {
369                        renderer = new ChoiceRenderer<>();
370                }
371                this.renderer = renderer;
372                return this;
373        }
374
375        @Override
376        protected void detachModel()
377        {
378                super.detachModel();
379
380                if (choices != null)
381                {
382                        choices.detach();
383                }
384        }
385
386        /**
387         * Get a default choice to be rendered additionally to the choices available in the model.
388         * 
389         * @param selectedValue
390         *            The currently selected value
391         * @return Any default choice, such as "Choose One", depending on the subclass
392         * @see #setChoices(IModel)
393         */
394        protected CharSequence getDefaultChoice(final String selectedValue)
395        {
396                return "";
397        }
398
399        /**
400         * Gets whether the given value represents the current selection.
401         * 
402         * @param object
403         *            The object to check
404         * @param index
405         *            The index in the choices collection this object is in.
406         * @param selected
407         *            The currently selected string value
408         * @return Whether the given value represents the current selection
409         */
410        protected abstract boolean isSelected(final E object, int index, String selected);
411
412        /**
413         * Gets whether the given value is disabled. This default implementation always returns false.
414         * 
415         * @param object
416         *            The object to check
417         * @param index
418         *            The index in the choices collection this object is in.
419         * @param selected
420         *            The currently selected string value
421         * @return Whether the given value represents the current selection
422         */
423        protected boolean isDisabled(final E object, int index, String selected)
424        {
425                return false;
426        }
427
428        /**
429         * Handle the container's body.
430         * 
431         * @param markupStream
432         *            The markup stream
433         * @param openTag
434         *            The open tag for the body
435         */
436        @Override
437        public void onComponentTagBody(final MarkupStream markupStream, final ComponentTag openTag)
438        {
439                List<? extends E> choices = getChoices();
440                final AppendingStringBuffer buffer = new AppendingStringBuffer((choices.size() * 50) + 16);
441                final String selectedValue = getValue();
442
443                // Append default option
444                buffer.append(getDefaultChoice(selectedValue));
445
446                for (int index = 0; index < choices.size(); index++)
447                {
448                        final E choice = choices.get(index);
449                        appendOptionHtml(buffer, choice, index, selectedValue);
450                }
451
452                buffer.append('\n');
453                replaceComponentTagBody(markupStream, openTag, buffer);
454        }
455
456        /**
457         * Generates and appends html for a single choice into the provided buffer
458         * 
459         * @param buffer
460         *            Appending string buffer that will have the generated html appended
461         * @param choice
462         *            Choice object
463         * @param index
464         *            The index of this option
465         * @param selected
466         *            The currently selected string value
467         */
468        protected void appendOptionHtml(AppendingStringBuffer buffer, E choice, int index, String selected)
469        {
470                CharSequence renderValue = renderValue(choice);
471
472                buffer.append("\n<option ");
473                setOptionAttributes(buffer, choice, index, selected);
474                buffer.append('>');
475                buffer.append(renderValue);
476                buffer.append("</option>");
477        }
478
479        @SuppressWarnings({ "rawtypes", "unchecked" })
480        CharSequence renderValue(E choice)
481        {
482                Object objectValue = renderer.getDisplayValue(choice);
483                Class<?> objectClass = (objectValue == null ? null : objectValue.getClass());
484
485                String displayValue = "";
486                if (objectClass != null && objectClass != String.class)
487                {
488                        IConverter converter = getConverter(objectClass);
489                        displayValue = converter.convertToString(objectValue, getLocale());
490                }
491                else if (objectValue != null)
492                {
493                        displayValue = objectValue.toString();
494                }
495                
496                if (localizeDisplayValues())
497                {
498                        String localized = getLocalizer().getString(getId() + "." + displayValue, this, "");
499                        if (Strings.isEmpty(localized)) {
500                                localized = getLocalizer().getString(displayValue, this, displayValue);
501                        }
502                        displayValue = localized;
503                }
504                
505                if (getEscapeModelStrings())
506                {
507                        return escapeOptionHtml(displayValue);
508                }
509                
510                return displayValue;
511        }
512
513        /**
514         * Sets the attributes of a single choice into the provided buffer.
515         *
516         * @param buffer
517         *            Appending string buffer that will have the generated html appended
518         * @param choice
519         *            Choice object
520         * @param index
521         *            The index of this option
522         * @param selected
523         *            The currently selected string value
524         */
525        protected void setOptionAttributes(AppendingStringBuffer buffer, E choice, int index, String selected)
526        {
527                if (isSelected(choice, index, selected))
528                {
529                        buffer.append("selected=\"selected\" ");
530                }
531
532                if (isDisabled(choice, index, selected))
533                {
534                        buffer.append("disabled=\"disabled\" ");
535                }
536
537                buffer.append("value=\"");
538                buffer.append(Strings.escapeMarkup(renderer.getIdValue(choice, index)));
539                buffer.append('"');
540        }
541
542        /**
543         * Method to override if you want special escaping of the options html.
544         * 
545         * @param displayValue
546         * @return The escaped display value
547         */
548        protected CharSequence escapeOptionHtml(String displayValue)
549        {
550                return Strings.escapeMarkup(displayValue);
551        }
552
553        /**
554         * Override this method if you want to localize the display values of the generated options. By
555         * default false is returned so that the display values of options are not tested if they have a
556         * i18n key.
557         * 
558         * @return true If you want to localize the display values, default == false
559         */
560        protected boolean localizeDisplayValues()
561        {
562                return false;
563        }
564
565        @Override
566        public final FormComponent<T> setType(Class<?> type)
567        {
568                throw new UnsupportedOperationException(
569                        "This class does not support type-conversion because it is performed "
570                                + "exclusively by the IChoiceRenderer assigned to this component");
571        }
572        
573        @Override
574        protected void onDetach()
575        {
576                renderer.detach();
577                
578                super.onDetach();
579        };
580}