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.ioc.services.TypeCoercer; 024import org.apache.tapestry5.services.ComponentDefaultProvider; 025import org.apache.tapestry5.services.Environment; 026import org.apache.tapestry5.services.FormSupport; 027import org.apache.tapestry5.services.Request; 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}