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