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.markup.html.form.select;
018
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collection;
022import java.util.List;
023
024import org.apache.wicket.WicketRuntimeException;
025import org.apache.wicket.markup.html.WebMarkupContainer;
026import org.apache.wicket.markup.html.form.FormComponent;
027import org.apache.wicket.model.IModel;
028import org.apache.wicket.util.lang.Args;
029import org.apache.wicket.util.lang.Objects;
030import org.apache.wicket.util.string.Strings;
031import org.apache.wicket.util.visit.IVisit;
032import org.apache.wicket.util.visit.IVisitor;
033
034
035/**
036 * Component that represents a <code>&lt;select&gt;</code> box. Elements are provided by one or more
037 * <code>SelectOptions</code> components in the hierarchy below the <code>Select</code> component.
038 * 
039 * Advantages to the standard choice components is that the user has a lot more control over the
040 * markup between the &lt;select&gt; tag and its children &lt;option&gt; tags: allowing for such
041 * things as &lt;optgroup&gt; tags.
042 * 
043 * <p>
044 * Example HTML:
045 * 
046 * <pre>
047 *    &lt;select wicket:id=&quot;select&quot; multiple=&quot;multiple&quot;&gt;
048 *        &lt;wicket:container wicket:id=&quot;options&quot;&gt;
049 *            &lt;option wicket:id=&quot;option&quot;&gt;Option Label&lt;/option&gt;
050 *        &lt;/wicket:container&gt;
051 *    &lt;/select&gt;
052 * </pre>
053 * 
054 * Related Java Code:
055 * 
056 * <pre>
057 * Select select = new Select(&quot;select&quot;, selectionModel);
058 * add(select);
059 * SelectOptions options = new SelectOptions(&quot;options&quot;, elements, renderer);
060 * select.add(options);
061 * </pre>
062 * 
063 * Note that you don't need to add component(s) for the &lt;option&gt; tag - they are created by
064 * SelectOptions
065 * </p>
066 * <p>
067 * <strong>Note</strong>: due to the usage of a SelectOption for each &lt;option&gt; the memory
068 * footprint of the page will grow with the number of &lt;option&gt;s you need. Consider using
069 * {@link org.apache.wicket.markup.html.form.DropDownChoice} component if it is able to fulfill your
070 * requirements.
071 * </p>
072 * 
073 * @see SelectOption
074 * @see SelectOptions
075 * @see org.apache.wicket.markup.html.form.DropDownChoice
076 * 
077 * @author Igor Vaynberg
078 * @param <T>
079 */
080public class Select<T> extends FormComponent<T>
081{
082        private static final long serialVersionUID = 1L;
083
084        /**
085         * Constructor that will create a default model collection
086         * 
087         * @param id
088         *            component id
089         */
090        public Select(final String id)
091        {
092                super(id);
093        }
094
095        /**
096         * @param id
097         * @param model
098         * @see WebMarkupContainer#WebMarkupContainer(String, IModel)
099         */
100        public Select(final String id, final IModel<T> model)
101        {
102                super(id, model);
103        }
104
105        @Override
106        protected String getModelValue()
107        {
108                final StringBuilder builder = new StringBuilder();
109
110                visitChildren(SelectOption.class, new IVisitor<SelectOption<T>, Void>()
111                {
112                        @Override
113                        public void component(SelectOption<T> option, IVisit<Void> visit)
114                        {
115                                if (isSelected(option.getDefaultModel()))
116                                {
117                                        if (builder.length() > 0)
118                                        {
119                                                builder.append(VALUE_SEPARATOR);
120                                        }
121                                        builder.append(option.getValue());
122                                }
123                        }
124                });
125
126                return builder.toString();
127        }
128
129        @Override
130        public void convertInput()
131        {
132                boolean supportsMultiple = getModelObject() instanceof Collection;
133
134                /*
135                 * + * the input contains an array of values of the selected option components unless
136                 * nothing was selected in which case the input contains null
137                 */
138                String[] values = getInputAsArray();
139
140                if ((values == null) || (values.length == 0))
141                {
142                        setConvertedInput(null);
143                        return;
144                }
145
146                if (!supportsMultiple && (values.length > 1))
147                {
148                        throw new WicketRuntimeException(
149                                "The model of Select component [" +
150                                        getPath() +
151                                        "] is not of type java.util.Collection, but more then one SelectOption component has been selected. Either remove the multiple attribute from the select tag or make the model of the Select component a collection");
152                }
153
154                List<Object> converted = new ArrayList<>(values.length);
155
156                /*
157                 * if the input is null we do not need to do anything since the model collection has already
158                 * been cleared
159                 */
160                for (int i = 0; i < values.length; i++)
161                {
162                        final String value = values[i];
163                        if (!Strings.isEmpty(value))
164                        {
165                                SelectOption<T> option = visitChildren(SelectOption.class,
166                                        new IVisitor<SelectOption<T>, SelectOption<T>>()
167                                        {
168                                                @Override
169                                                public void component(SelectOption<T> option, IVisit<SelectOption<T>> visit)
170                                                {
171                                                        if (String.valueOf(option.getValue()).equals(value))
172                                                        {
173                                                                visit.stop(option);
174                                                        }
175                                                }
176                                        });
177
178                                if (option == null)
179                                {
180                                        throw new WicketRuntimeException(
181                                                "submitted http post value [" +
182                                                        Arrays.toString(values) +
183                                                        "] for SelectOption component [" +
184                                                        getPath() +
185                                                        "] contains an illegal value [" +
186                                                        value +
187                                                        "] which does not point to a SelectOption component. Due to this the Select component cannot resolve the selected SelectOption component pointed to by the illegal value. A possible reason is that component hierarchy changed between rendering and form submission.");
188                                }
189                                converted.add(option.getDefaultModelObject());
190                        }
191                }
192
193                if (converted.isEmpty())
194                {
195                        setConvertedInput(null);
196                }
197                else if (!supportsMultiple)
198                {
199                        @SuppressWarnings("unchecked")
200                        T convertedInput = (T)converted.get(0);
201                        setConvertedInput(convertedInput);
202                }
203                else
204                {
205                        @SuppressWarnings("unchecked")
206                        T convertedInput = (T)converted;
207                        setConvertedInput(convertedInput);
208                }
209        }
210
211        /**
212         * @see FormComponent#updateModel()
213         */
214        @SuppressWarnings({ "unchecked", "rawtypes" })
215        @Override
216        public void updateModel()
217        {
218                T object = getModelObject();
219                boolean supportsMultiple = object instanceof Collection;
220
221                Object converted = getConvertedInput();
222                /*
223                 * update the model
224                 */
225                if (supportsMultiple)
226                {
227                        Collection<?> modelCollection = (Collection<?>)object;
228                        modelChanging();
229                        modelCollection.clear();
230                        if (converted != null)
231                        {
232                                modelCollection.addAll((Collection)converted);
233                        }
234                        modelChanged();
235                        // force notify of model update via setObject()
236                        setDefaultModelObject(modelCollection);
237                }
238                else
239                {
240                        setDefaultModelObject(converted);
241                }
242        }
243
244        /**
245         * Checks if the specified option is selected based on raw input
246         * 
247         * @param option
248         * @return {@code true} if the option is selected, {@code false} otherwise
249         */
250        boolean isSelected(final SelectOption<?> option)
251        {
252                Args.notNull(option, "option");
253
254                // if the raw input is specified use that, otherwise use model
255                if (hasRawInput())
256                {
257                        final String raw = getRawInput();
258                        if (!Strings.isEmpty(raw))
259                        {
260                                String[] values = raw.split(VALUE_SEPARATOR);
261                                for (int i = 0; i < values.length; i++)
262                                {
263                                        String value = values[i];
264                                        if (value.equals(option.getValue()))
265                                        {
266                                                return true;
267                                        }
268                                }
269                        }
270                        return false;
271                }
272
273                return isSelected(option.getDefaultModel());
274        }
275
276        /**
277         * Does the given model contain a selected value.
278         * 
279         * @param model
280         *            model to test on selection
281         * @return selected
282         */
283        protected boolean isSelected(IModel<?> model)
284        {
285                return compareModels(getDefaultModelObject(), model.getObject());
286        }
287
288        private boolean compareModels(final Object selected, final Object value)
289        {
290                if ((selected != null) && (selected instanceof Collection))
291                {
292                        if (value instanceof Collection)
293                        {
294                                return ((Collection<?>)selected).containsAll((Collection<?>)value);
295                        }
296                        else
297                        {
298                                return ((Collection<?>)selected).contains(value);
299                        }
300                }
301                else
302                {
303                        return Objects.equal(selected, value);
304                }
305        }
306
307}