001// Copyright 2007, 2008, 2009, 2011 The Apache Software Foundation
002//
003// Licensed under the Apache License, Version 2.0 (the "License");
004// you may not use this file except in compliance with the License.
005// You may obtain a copy of the License at
006//
007// http://www.apache.org/licenses/LICENSE-2.0
008//
009// Unless required by applicable law or agreed to in writing, software
010// distributed under the License is distributed on an "AS IS" BASIS,
011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012// See the License for the specific language governing permissions and
013// limitations under the License.
014
015package org.apache.tapestry5.corelib.components;
016
017import org.apache.tapestry5.*;
018import org.apache.tapestry5.annotations.Environmental;
019import org.apache.tapestry5.annotations.Events;
020import org.apache.tapestry5.annotations.Parameter;
021import org.apache.tapestry5.commons.services.TypeCoercer;
022import org.apache.tapestry5.http.services.Request;
023import org.apache.tapestry5.internal.TapestryInternalUtils;
024import org.apache.tapestry5.ioc.annotations.Inject;
025import org.apache.tapestry5.services.ComponentDefaultProvider;
026import org.apache.tapestry5.services.Environment;
027import org.apache.tapestry5.services.FormSupport;
028
029/**
030 * A wrapper component around some number of {@link Radio} components, used to organize the selection and define the
031 * property to be edited. Examples of its use are in the {@link Radio} documentation.
032 *
033 * @tapestrydoc
034 */
035@Events(EventConstants.VALIDATE)
036public class RadioGroup implements Field
037{
038    /**
039     * The property read and updated by the group as a whole.
040     */
041    @Parameter(required = true, principal = true, autoconnect = true)
042    private Object value;
043
044    /**
045     * If true, then the field will render out with a disabled attribute (to turn off client-side behavior). Further, a
046     * disabled field ignores any value in the request when the form is submitted.
047     */
048    @Parameter("false")
049    private boolean disabled;
050
051    /**
052     * The user presentable label for the field. If not provided, a reasonable label is generated from the component's
053     * id, first by looking for a message key named "id-label" (substituting the component's actual id), then by
054     * converting the actual id to a presentable string (for example, "userId" to "User Id").
055     */
056    @Parameter(defaultPrefix = BindingConstants.LITERAL)
057    private String label;
058
059    /**
060     * The id used to generate a page-unique client-side identifier for the component. If a component renders multiple
061     * times, a suffix will be appended to the to id to ensure uniqueness. The uniqued value may be accessed via the
062     * {@link #getClientId() clientId property}.
063     */
064    @Parameter(value = "prop:componentResources.id", defaultPrefix = BindingConstants.LITERAL)
065    private String clientId;
066
067    /**
068     * A ValueEncoder used to convert server-side objects (provided by the
069     * selected Radio componnent's "value" parameter) into unique client-side
070     * strings (typically IDs) and back. Note: this parameter may be OMITTED if
071     * Tapestry is configured to provide a ValueEncoder automatically for the
072     * type of property bound to the "value" parameter.
073     */
074    @Parameter(required = true, allowNull = false)
075    private ValueEncoder encoder;
076
077    /**
078     * The object that will perform input validation. The validate binding prefix is generally used to provide this
079     * object in a declarative fashion.
080     */
081    @Parameter(defaultPrefix = BindingConstants.VALIDATE)
082    @SuppressWarnings("unchecked")
083    private FieldValidator<Object> validate;
084
085    @Inject
086    private ComponentDefaultProvider defaultProvider;
087
088    @Inject
089    private ComponentResources resources;
090
091    @Environmental
092    private FormSupport formSupport;
093
094    @Inject
095    private Environment environment;
096
097    @Inject
098    private Request request;
099
100    @Inject
101    private TypeCoercer typeCoercer;
102
103    @Environmental
104    private ValidationTracker tracker;
105
106    @Inject
107    private FieldValidationSupport fieldValidationSupport;
108
109    private String controlName;
110
111    String defaultLabel()
112    {
113        return defaultProvider.defaultLabel(resources);
114    }
115
116    final ValueEncoder defaultEncoder()
117    {
118        return defaultProvider.defaultValueEncoder("value", resources);
119    }
120
121    private static class Setup implements ComponentAction<RadioGroup>
122    {
123        private static final long serialVersionUID = -7984673040135949374L;
124
125        private final String controlName;
126
127        Setup(String controlName)
128        {
129            this.controlName = controlName;
130        }
131
132        public void execute(RadioGroup component)
133        {
134            component.setup(controlName);
135        }
136
137        @Override
138        public String toString()
139        {
140            return String.format("RadioGroup.Setup[%s]", controlName);
141        }
142    }
143
144    private static final ComponentAction<RadioGroup> PROCESS_SUBMISSION = new ComponentAction<RadioGroup>()
145    {
146        private static final long serialVersionUID = -3857110108918776386L;
147
148        public void execute(RadioGroup component)
149        {
150            component.processSubmission();
151        }
152
153        @Override
154        public String toString()
155        {
156            return "RadioGroup.ProcessSubmission";
157        }
158    };
159
160    private void setup(String elementName)
161    {
162        controlName = elementName;
163    }
164
165    private void processSubmission()
166    {
167
168        if (disabled)
169        {
170            return;
171        }
172        String rawValue = request.getParameter(controlName);
173        Object convertedValue = encoder.toValue(rawValue);
174
175        tracker.recordInput(this, rawValue);
176        try
177        {
178            if (validate != null)
179                fieldValidationSupport.validate(convertedValue, resources, validate);
180        }
181        catch (ValidationException ex)
182        {
183            tracker.recordError(this, ex.getMessage());
184        }
185
186        value = convertedValue;
187    }
188
189    /**
190     * Obtains the element name for the group, and stores a {@link RadioContainer} into the {@link Environment} (so that
191     * the {@link Radio} components can find it).
192     */
193    final void setupRender()
194    {
195        ComponentAction<RadioGroup> action = new Setup(formSupport.allocateControlName(clientId));
196
197        formSupport.storeAndExecute(this, action);
198
199        String submittedValue = tracker.getInput(this);
200
201        final String selectedValue = submittedValue != null ? submittedValue : encoder.toClient(value);
202
203        final Class<?> boundType = resources.getBoundType("value");
204
205        environment.push(RadioContainer.class, new RadioContainer()
206        {
207            public String getControlName()
208            {
209                return controlName;
210            }
211
212            public boolean isDisabled()
213            {
214                return disabled;
215            }
216
217            private Object getObjectAsCorrectType(Object val)
218            {
219              if (val != null && boundType != null && !boundType.isAssignableFrom(val.getClass()))
220              {
221                  return typeCoercer.coerce(val, boundType);
222              }
223              return val;
224            }
225
226            @SuppressWarnings("unchecked")
227            public String toClient(Object value)
228            {
229                return encoder.toClient(getObjectAsCorrectType(value));
230            }
231
232            public boolean isSelected(Object value)
233            {
234                return TapestryInternalUtils.isEqual(encoder.toClient(getObjectAsCorrectType(value)), selectedValue);
235            }
236        });
237
238        formSupport.store(this, PROCESS_SUBMISSION);
239    }
240
241    /**
242     * Pops the {@link RadioContainer} off the Environment.
243     */
244    final void afterRender()
245    {
246        environment.pop(RadioContainer.class);
247    }
248
249    public String getControlName()
250    {
251        return controlName;
252    }
253
254    public String getLabel()
255    {
256        return label;
257    }
258
259    public boolean isDisabled()
260    {
261        return disabled;
262    }
263
264    /**
265     * Returns null; the radio group does not render as a tag and so doesn't have an id to share. RadioGroup implements
266     * {@link org.apache.tapestry5.Field} only so it can interact with the
267     * {@link org.apache.tapestry5.ValidationTracker}.
268     *
269     * @return null
270     */
271    public String getClientId()
272    {
273        return null;
274    }
275
276    public boolean isRequired()
277    {
278        return validate.isRequired();
279    }
280}