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
015 package org.apache.tapestry5.corelib.components;
016
017 import org.apache.tapestry5.*;
018 import org.apache.tapestry5.annotations.Environmental;
019 import org.apache.tapestry5.annotations.Events;
020 import org.apache.tapestry5.annotations.Parameter;
021 import org.apache.tapestry5.internal.TapestryInternalUtils;
022 import org.apache.tapestry5.ioc.annotations.Inject;
023 import org.apache.tapestry5.services.ComponentDefaultProvider;
024 import org.apache.tapestry5.services.Environment;
025 import org.apache.tapestry5.services.FormSupport;
026 import 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)
035 public 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 }