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.List;
020
021import org.apache.wicket.Localizer;
022import org.apache.wicket.model.IModel;
023import org.apache.wicket.util.string.AppendingStringBuffer;
024import org.apache.wicket.util.string.Strings;
025
026
027/**
028 * Abstract base class for single-select choices.
029 * 
030 * @author Jonathan Locke
031 * @author Eelco Hillenius nm
032 * @author Johan Compagner
033 * 
034 * @param <T>
035 *            The model object type
036 */
037public abstract class AbstractSingleSelectChoice<T> extends AbstractChoice<T, T>
038{
039        private static final long serialVersionUID = 1L;
040
041        /** String to display when the selected value is null and nullValid is false. */
042        private static final String CHOOSE_ONE = "Choose One";
043
044        /** whether or not null will be offered as a choice once a nonnull value is saved */
045        private boolean nullValid = false;
046
047        /**
048         * Constructor.
049         * 
050         * @param id
051         *            See Component
052         */
053        public AbstractSingleSelectChoice(final String id)
054        {
055                super(id);
056        }
057
058        /**
059         * Constructor.
060         * 
061         * @param id
062         *            See Component
063         * @param choices
064         *            The collection of choices in the dropdown
065         */
066        public AbstractSingleSelectChoice(final String id, final List<? extends T> choices)
067        {
068                super(id, choices);
069        }
070
071        /**
072         * Constructor.
073         * 
074         * @param id
075         *            See Component
076         * @param renderer
077         *            The rendering engine
078         * @param choices
079         *            The collection of choices in the dropdown
080         */
081        public AbstractSingleSelectChoice(final String id, final List<? extends T> choices,
082                final IChoiceRenderer<? super T> renderer)
083        {
084                super(id, choices, renderer);
085        }
086
087        /**
088         * Constructor.
089         * 
090         * @param id
091         *            See Component
092         * @param model
093         *            See Component
094         * @param choices
095         *            The collection of choices in the dropdown
096         */
097        public AbstractSingleSelectChoice(final String id, IModel<T> model,
098                final List<? extends T> choices)
099        {
100                super(id, model, choices);
101        }
102
103        /**
104         * Constructor.
105         * 
106         * @param id
107         *            See Component
108         * @param model
109         *            See Component
110         * @param choices
111         *            The drop down choices
112         * @param renderer
113         *            The rendering engine
114         */
115        public AbstractSingleSelectChoice(final String id, IModel<T> model,
116                final List<? extends T> choices, final IChoiceRenderer<? super T> renderer)
117        {
118                super(id, model, choices, renderer);
119        }
120
121        /**
122         * Constructor.
123         * 
124         * @param id
125         *            See Component
126         * @param choices
127         *            The collection of choices in the dropdown
128         */
129        public AbstractSingleSelectChoice(String id, IModel<? extends List<? extends T>> choices)
130        {
131                super(id, choices);
132        }
133
134        /**
135         * Constructor.
136         * 
137         * @param id
138         *            See Component
139         * @param model
140         *            See Component
141         * @param choices
142         *            The drop down choices
143         */
144        public AbstractSingleSelectChoice(String id, IModel<T> model,
145                IModel<? extends List<? extends T>> choices)
146        {
147                super(id, model, choices);
148        }
149
150        /**
151         * Constructor.
152         * 
153         * @param id
154         *            See Component
155         * @param choices
156         *            The drop down choices
157         * @param renderer
158         *            The rendering engine
159         */
160        public AbstractSingleSelectChoice(String id, IModel<? extends List<? extends T>> choices,
161                IChoiceRenderer<? super T> renderer)
162        {
163                super(id, choices, renderer);
164        }
165
166        /**
167         * Constructor.
168         * 
169         * @param id
170         *            See Component
171         * @param model
172         *            See Component
173         * @param choices
174         *            The drop down choices
175         * @param renderer
176         *            The rendering engine
177         */
178        public AbstractSingleSelectChoice(String id, IModel<T> model,
179                IModel<? extends List<? extends T>> choices, IChoiceRenderer<? super T> renderer)
180        {
181                super(id, model, choices, renderer);
182        }
183
184        /**
185         * @see FormComponent#getModelValue()
186         */
187        @Override
188        public String getModelValue()
189        {
190                final T object = getModelObject();
191                if (object != null)
192                {
193                        int index = getChoices().indexOf(object);
194                        return getChoiceRenderer().getIdValue(object, index);
195                }
196                else
197                {
198                        return "";
199                }
200        }
201
202        /**
203         * Determines whether or not the null value should be included in the list of choices when the
204         * field's model value is nonnull, and whether or not the null_valid string property (e.g.
205         * "Choose One") should be displayed until a nonnull value is selected.
206         * 
207         * If set to false, then "Choose One" will be displayed when the value is null. After a value is
208         * selected, and that change is propagated to the underlying model, the user will no longer see
209         * the "Choose One" option, and there will be no way to reselect null as the value.
210         * 
211         * If set to true, the null string property (the empty string, by default) will always be
212         * displayed as an option, whether or not a nonnull value has ever been selected.
213         * 
214         * Note that this setting has no effect on validation; in order to guarantee that a value will
215         * be specified on form validation, {@link #setRequired(boolean)}. This is because even if
216         * setNullValid() is called with false, the user can fail to provide a value simply by never
217         * activating (i.e. clicking on) the component.
218         * 
219         * @return <code>true</code> when the <code>null</code> value is allowed.
220         */
221        public boolean isNullValid()
222        {
223                return nullValid;
224        }
225
226        /**
227         * Determines whether or not the null value should be included in the list of choices when the
228         * field's model value is nonnull, and whether or not the null_valid string property (e.g.
229         * "Choose One") should be displayed until a nonnull value is selected.
230         * 
231         * If set to false, then "Choose One" will be displayed when the value is null. After a value is
232         * selected, and that change is propagated to the underlying model, the user will no longer see
233         * the "Choose One" option, and there will be no way to reselect null as the value.
234         * 
235         * If set to true, the null string property (the empty string, by default) will always be
236         * displayed as an option, whether or not a nonnull value has ever been selected.
237         * 
238         * Note that this setting has no effect on validation; in order to guarantee that a value will
239         * be specified on form validation, {@link #setRequired(boolean)}. This is because even if
240         * setNullValid() is called with false, the user can fail to provide a value simply by never
241         * activating (i.e. clicking on) the component.
242         * 
243         * @param nullValid
244         *            whether null is a valid value
245         * @return this for chaining
246         */
247        public AbstractSingleSelectChoice<T> setNullValid(boolean nullValid)
248        {
249                this.nullValid = nullValid;
250                return this;
251        }
252
253        /**
254         * @see org.apache.wicket.markup.html.form.FormComponent#convertValue(String[])
255         */
256        @Override
257        protected final T convertValue(final String[] value)
258        {
259                String tmp = ((value != null) && (value.length > 0)) ? value[0] : null;
260                return convertChoiceIdToChoice(tmp);
261        }
262
263        /**
264         * Converts submitted choice id string back to choice object.
265         * 
266         * @param id
267         *            string id of one of the choice objects in the choices list. can be null.
268         * @return choice object. null if none match the specified id.
269         */
270        protected T convertChoiceIdToChoice(String id)
271        {
272                final IModel<? extends List<? extends T>> choices = getChoicesModel();
273                final IChoiceRenderer<? super T> renderer = getChoiceRenderer();
274                T object = (T) renderer.getObject(id, choices);
275                return object;
276        }
277
278        /**
279         * Asks the {@link Localizer} for the property to display for an additional default choice
280         * depending on {@link #isNullValid()}:
281         * 
282         * <ul>
283         * <li>
284         * "nullValid" if {@code null} is valid, defaulting to an empty string.</li>
285         * <li>
286         * "null" if {@code null} is not valid but no choice is selected (i.e. {@code selectedValue} is
287         * empty), defaulting to "Choose one".</li>
288         * </ul>
289         * 
290         * Otherwise no additional default choice will be returned.
291         * 
292         * @see #getNullValidKey()
293         * @see #getNullKey()
294         * @see org.apache.wicket.markup.html.form.AbstractChoice#getDefaultChoice(String)
295         */
296        @Override
297        protected CharSequence getDefaultChoice(final String selectedValue)
298        {
299                // Is null a valid selection value?
300                if (isNullValid())
301                {
302                        // Null is valid, so look up the value for it
303                        String option = getNullValidDisplayValue();
304
305                        // The <option> tag buffer
306                        final AppendingStringBuffer buffer = new AppendingStringBuffer(64 + option.length());
307
308                        // Add option tag
309                        buffer.append("\n<option");
310
311                        // If null is selected, indicate that
312                        if (selectedValue != null && selectedValue.isEmpty())
313                        {
314                                buffer.append(" selected=\"selected\"");
315                        }
316
317                        // Add body of option tag
318                        buffer.append(" value=\"\">").append(option).append("</option>");
319                        return buffer;
320                }
321                else
322                {
323                        // Null is not valid. Is it selected anyway?
324                        if (selectedValue != null && selectedValue.isEmpty())
325                        {
326                                // Force the user to pick a non-null value
327                                String option = getNullKeyDisplayValue();
328                                return "\n<option selected=\"selected\" value=\"\">" + option + "</option>";
329                        }
330                }
331                return "";
332        }
333
334        /**
335         * Returns the display value for the null value. The default behavior is to look the value up by
336         * using the key from <code>getNullValidKey()</code>.
337         *
338         * @return The value to display for null
339         */
340        protected String getNullValidDisplayValue()
341        {
342                String option = getLocalizer().getStringIgnoreSettings(getNullValidKey(), this, null, null);
343                if (Strings.isEmpty(option))
344                {
345                        option = getLocalizer().getString("nullValid", this, "");
346                }
347                return option;
348        }
349
350        /**
351         * Return the localization key for nullValid value
352         * 
353         * @return getId() + ".nullValid"
354         */
355        protected String getNullValidKey()
356        {
357                return getId() + ".nullValid";
358        }
359
360        /**
361         * Returns the display value if null is not valid but is selected. The default behavior is to
362         * look the value up by using the key from <code>getNullKey()</code>.
363         *
364         * @return The value to display if null is not value but selected, e.g. "Choose One"
365         */
366        protected String getNullKeyDisplayValue()
367        {
368                String option = getLocalizer().getStringIgnoreSettings(getNullKey(), this, null, null);
369
370                if (Strings.isEmpty(option))
371                {
372                        option = getLocalizer().getString("null", this, CHOOSE_ONE);
373                }
374                return option;
375        }
376
377        /**
378         * Return the localization key for null value
379         * 
380         * @return getId() + ".null"
381         */
382        protected String getNullKey()
383        {
384                return getId() + ".null";
385        }
386
387        /**
388         * Gets whether the given value represents the current selection.
389         * 
390         * 
391         * @param object
392         *            The object to check
393         * @param index
394         *            The index of the object in the collection
395         * @param selected
396         *            The current selected id value
397         * @return Whether the given value represents the current selection
398         */
399        @Override
400        protected boolean isSelected(final T object, int index, String selected)
401        {
402                return (selected != null) && selected.equals(getChoiceRenderer().getIdValue(object, index));
403        }
404}