001// Licensed under the Apache License, Version 2.0 (the "License"); 002// you may not use this file except in compliance with the License. 003// You may obtain a copy of the License at 004// 005// http://www.apache.org/licenses/LICENSE-2.0 006// 007// Unless required by applicable law or agreed to in writing, software 008// distributed under the License is distributed on an "AS IS" BASIS, 009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 010// See the License for the specific language governing permissions and 011// limitations under the License. 012 013package org.apache.tapestry5.corelib.components; 014 015import org.apache.tapestry5.*; 016import org.apache.tapestry5.annotations.*; 017import org.apache.tapestry5.corelib.base.AbstractField; 018import org.apache.tapestry5.corelib.data.BlankOption; 019import org.apache.tapestry5.corelib.data.SecureOption; 020import org.apache.tapestry5.corelib.mixins.RenderDisabled; 021import org.apache.tapestry5.internal.TapestryInternalUtils; 022import org.apache.tapestry5.internal.util.CaptureResultCallback; 023import org.apache.tapestry5.internal.util.SelectModelRenderer; 024import org.apache.tapestry5.ioc.Messages; 025import org.apache.tapestry5.ioc.annotations.Inject; 026import org.apache.tapestry5.ioc.internal.util.InternalUtils; 027import org.apache.tapestry5.services.*; 028import org.apache.tapestry5.services.javascript.JavaScriptSupport; 029import org.apache.tapestry5.util.EnumSelectModel; 030 031import java.util.Collections; 032import java.util.List; 033 034/** 035 * Select an item from a list of values, using an [X]HTML <select> element on the client side. Any validation 036 * decorations will go around the entire <select> element. 037 * 038 * A core part of this component is the {@link ValueEncoder} (the encoder parameter) that is used to convert between 039 * server-side values and unique client-side strings. In some cases, a {@link ValueEncoder} can be generated automatically from 040 * the type of the value parameter. The {@link ValueEncoderSource} service provides an encoder in these situations; it 041 * can be overridden by binding the encoder parameter, or extended by contributing a {@link ValueEncoderFactory} into the 042 * service's configuration. 043 * 044 * @tapestrydoc 045 */ 046@Events( 047 {EventConstants.VALIDATE, EventConstants.VALUE_CHANGED + " when 'zone' parameter is bound"}) 048public class Select extends AbstractField 049{ 050 public static final String CHANGE_EVENT = "change"; 051 052 private class Renderer extends SelectModelRenderer 053 { 054 055 public Renderer(MarkupWriter writer) 056 { 057 super(writer, encoder, raw); 058 } 059 060 @Override 061 protected boolean isOptionSelected(OptionModel optionModel, String clientValue) 062 { 063 return isSelected(clientValue); 064 } 065 } 066 067 /** 068 * A ValueEncoder used to convert the server-side object provided by the 069 * "value" parameter into a unique client-side string (typically an ID) and 070 * back. Note: this parameter may be OMITTED if Tapestry is configured to 071 * provide a ValueEncoder automatically for the type of property bound to 072 * the "value" parameter. 073 * 074 * @see ValueEncoderSource 075 */ 076 @Parameter 077 private ValueEncoder encoder; 078 079 /** 080 * Controls whether the submitted value is validated to be one of the values in 081 * the {@link SelectModel}. If "never", then no such validation is performed, 082 * theoretically allowing a selection to be made that was not presented to 083 * the user. Note that an "always" value here requires the SelectModel to 084 * still exist (or be created again) when the form is submitted, whereas a 085 * "never" value does not. Defaults to "auto", which causes the validation 086 * to occur only if the SelectModel is present (not null) when the form is 087 * submitted. 088 * 089 * @since 5.4 090 */ 091 @Parameter(value = BindingConstants.SYMBOL + ":" + ComponentParameterConstants.VALIDATE_WITH_MODEL, defaultPrefix = BindingConstants.LITERAL) 092 private SecureOption secure; 093 094 /** 095 * If true, then the provided {@link org.apache.tapestry5.SelectModel} labels will be written raw (no escaping of 096 * embedded HTML entities); it becomes the callers responsibility to escape any such entities. 097 * 098 * @since 5.4 099 */ 100 @Parameter(value = "false") 101 private boolean raw; 102 103 /** 104 * The model used to identify the option groups and options to be presented to the user. This can be generated 105 * automatically for Enum types. 106 */ 107 @Parameter(required = true, allowNull = false) 108 private SelectModel model; 109 110 /** 111 * Controls whether an additional blank option is provided. The blank option precedes all other options and is never 112 * selected. The value for the blank option is always the empty string, the label may be the blank string; the 113 * label is from the blankLabel parameter (and is often also the empty string). 114 */ 115 @Parameter(value = "auto", defaultPrefix = BindingConstants.LITERAL) 116 private BlankOption blankOption; 117 118 /** 119 * The label to use for the blank option, if rendered. If not specified, the container's message catalog is 120 * searched for a key, <code><em>id</em>-blanklabel</code>. 121 */ 122 @Parameter(defaultPrefix = BindingConstants.LITERAL) 123 private String blankLabel; 124 125 @Inject 126 private Request request; 127 128 @Environmental 129 private ValidationTracker tracker; 130 131 /** 132 * Performs input validation on the value supplied by the user in the form submission. 133 */ 134 @Parameter(defaultPrefix = BindingConstants.VALIDATE) 135 private FieldValidator<Object> validate; 136 137 /** 138 * The value to read or update. 139 */ 140 @Parameter(required = true, principal = true, autoconnect = true) 141 private Object value; 142 143 /** 144 * Binding the zone parameter will cause any change of Select's value to be handled as an Ajax request that updates 145 * the 146 * indicated zone. The component will trigger the event {@link EventConstants#VALUE_CHANGED} to inform its 147 * container that Select's value has changed. 148 * 149 * @since 5.2.0 150 */ 151 @Parameter(defaultPrefix = BindingConstants.LITERAL) 152 private String zone; 153 154 /** 155 * The context for the "valueChanged" event triggered by this component (optional parameter). 156 * This list of values will be converted into strings and included in 157 * the URI. The strings will be coerced back to whatever their values are and made available to event handler 158 * methods. The first parameter of the context passed to "valueChanged" event handlers will 159 * still be the selected value chosen by the user, so the context passed through this parameter 160 * will be added from the second position on. 161 * 162 * @since 5.4 163 */ 164 @Parameter 165 private Object[] context; 166 167 @Inject 168 private FieldValidationSupport fieldValidationSupport; 169 170 @Environmental 171 private FormSupport formSupport; 172 173 @Inject 174 private JavaScriptSupport javascriptSupport; 175 176 @SuppressWarnings("unused") 177 @Mixin 178 private RenderDisabled renderDisabled; 179 180 private String selectedClientValue; 181 182 private boolean isSelected(String clientValue) 183 { 184 return TapestryInternalUtils.isEqual(clientValue, selectedClientValue); 185 } 186 187 @SuppressWarnings( 188 {"unchecked"}) 189 @Override 190 protected void processSubmission(String controlName) 191 { 192 String submittedValue = request.getParameter(controlName); 193 194 tracker.recordInput(this, submittedValue); 195 196 Object selectedValue; 197 198 try 199 { 200 selectedValue = toValue(submittedValue); 201 } catch (ValidationException ex) 202 { 203 // Really, this will just be the logic related to the new (in 5.4) secure 204 // parameter: 205 206 tracker.recordError(this, ex.getMessage()); 207 return; 208 } 209 210 putPropertyNameIntoBeanValidationContext("value"); 211 212 try 213 { 214 fieldValidationSupport.validate(selectedValue, resources, validate); 215 216 value = selectedValue; 217 } catch (ValidationException ex) 218 { 219 tracker.recordError(this, ex.getMessage()); 220 } 221 222 removePropertyNameFromBeanValidationContext(); 223 } 224 225 void afterRender(MarkupWriter writer) 226 { 227 writer.end(); 228 } 229 230 void beginRender(MarkupWriter writer) 231 { 232 writer.element("select", 233 "name", getControlName(), 234 "id", getClientId(), 235 "class", cssClass); 236 237 putPropertyNameIntoBeanValidationContext("value"); 238 239 validate.render(writer); 240 241 removePropertyNameFromBeanValidationContext(); 242 243 resources.renderInformalParameters(writer); 244 245 decorateInsideField(); 246 247 // Disabled is via a mixin 248 249 if (this.zone != null) 250 { 251 javaScriptSupport.require("t5/core/select"); 252 253 Link link = resources.createEventLink(CHANGE_EVENT, context); 254 255 writer.attributes( 256 "data-update-zone", zone, 257 "data-update-url", link); 258 } 259 } 260 261 Object onChange(final List<Object> context, 262 @RequestParameter(value = "t:selectvalue", allowBlank = true) final String selectValue) 263 throws ValidationException 264 { 265 final Object newValue = toValue(selectValue); 266 267 CaptureResultCallback<Object> callback = new CaptureResultCallback<Object>(); 268 269 Object[] newContext = new Object[context.size() + 1]; 270 newContext[0] = newValue; 271 for (int i = 1; i < newContext.length; i++) 272 { 273 newContext[i] = context.get(i - 1); 274 } 275 276 277 this.resources.triggerEvent(EventConstants.VALUE_CHANGED, newContext, callback); 278 279 this.value = newValue; 280 281 return callback.getResult(); 282 } 283 284 protected Object toValue(String submittedValue) throws ValidationException 285 { 286 if (InternalUtils.isBlank(submittedValue)) 287 { 288 return null; 289 } 290 291 // can we skip the check for the value being in the model? 292 if (secure == SecureOption.NEVER || (secure == SecureOption.AUTO && model == null)) 293 { 294 return encoder.toValue(submittedValue); 295 } 296 297 // for entity types the SelectModel may be unintentionally null when the form is submitted 298 if (model == null) 299 { 300 throw new ValidationException("Model is null when validating submitted option." + 301 " To fix: persist the SeletModel or recreate it upon form submission," + 302 " or change the 'secure' parameter."); 303 } 304 305 return findValueInModel(submittedValue); 306 } 307 308 private Object findValueInModel(String submittedValue) throws ValidationException 309 { 310 311 Object asSubmitted = encoder.toValue(submittedValue); 312 313 // The visitor would be nice if it had the option to abort the visit 314 // early. 315 316 if (findInOptions(model.getOptions(), asSubmitted)) 317 { 318 return asSubmitted; 319 } 320 321 if (model.getOptionGroups() != null) 322 { 323 for (OptionGroupModel og : model.getOptionGroups()) 324 { 325 if (findInOptions(og.getOptions(), asSubmitted)) 326 { 327 return asSubmitted; 328 } 329 } 330 } 331 332 throw new ValidationException("Selected option is not listed in the model."); 333 } 334 335 private boolean findInOptions(List<OptionModel> options, Object asSubmitted) 336 { 337 if (options == null) 338 { 339 return false; 340 } 341 342 // See TAP5-2184: Sometimes the SelectModel option values are Strings even though the 343 // submitted value (decoded by the ValueEncoder) are another type (e.g., numeric). In that case, 344 // pass each OptionModel value through the ValueEncoder for a comparison. 345 boolean alsoCompareDecodedModelValue = !(asSubmitted instanceof String); 346 347 for (OptionModel om : options) 348 { 349 Object modelValue = om.getValue(); 350 if (modelValue.equals(asSubmitted)) 351 { 352 return true; 353 } 354 355 if (alsoCompareDecodedModelValue && (modelValue instanceof String)) 356 { 357 Object decodedModelValue = encoder.toValue(modelValue.toString()); 358 359 if (decodedModelValue.equals(asSubmitted)) 360 { 361 return true; 362 } 363 } 364 } 365 366 return false; 367 } 368 369 private static <T> List<T> orEmpty(List<T> list) 370 { 371 if (list == null) 372 { 373 return Collections.emptyList(); 374 } 375 376 return list; 377 } 378 379 @SuppressWarnings("unchecked") 380 ValueEncoder defaultEncoder() 381 { 382 return defaultProvider.defaultValueEncoder("value", resources); 383 } 384 385 @SuppressWarnings("unchecked") 386 SelectModel defaultModel() 387 { 388 Class valueType = resources.getBoundType("value"); 389 390 if (valueType == null) 391 return null; 392 393 if (Enum.class.isAssignableFrom(valueType)) 394 return new EnumSelectModel(valueType, resources.getContainerMessages()); 395 396 return null; 397 } 398 399 /** 400 * Computes a default value for the "validate" parameter using {@link FieldValidatorDefaultSource}. 401 */ 402 Binding defaultValidate() 403 { 404 return defaultProvider.defaultValidatorBinding("value", resources); 405 } 406 407 Object defaultBlankLabel() 408 { 409 Messages containerMessages = resources.getContainerMessages(); 410 411 String key = resources.getId() + "-blanklabel"; 412 413 if (containerMessages.contains(key)) 414 return containerMessages.get(key); 415 416 return null; 417 } 418 419 /** 420 * Renders the options, including the blank option. 421 */ 422 @BeforeRenderTemplate 423 void options(MarkupWriter writer) 424 { 425 selectedClientValue = tracker.getInput(this); 426 427 // Use the value passed up in the form submission, if available. 428 // Failing that, see if there is a current value (via the value parameter), and 429 // convert that to a client value for later comparison. 430 431 if (selectedClientValue == null) 432 selectedClientValue = value == null ? null : encoder.toClient(value); 433 434 if (showBlankOption()) 435 { 436 writer.element("option", "value", ""); 437 writer.write(blankLabel); 438 writer.end(); 439 } 440 441 SelectModelVisitor renderer = new Renderer(writer); 442 443 model.visit(renderer); 444 } 445 446 @Override 447 public boolean isRequired() 448 { 449 return validate.isRequired(); 450 } 451 452 private boolean showBlankOption() 453 { 454 switch (blankOption) 455 { 456 case ALWAYS: 457 return true; 458 459 case NEVER: 460 return false; 461 462 default: 463 return !isRequired(); 464 } 465 } 466 467 // For testing. 468 469 void setModel(SelectModel model) 470 { 471 this.model = model; 472 blankOption = BlankOption.NEVER; 473 } 474 475 void setValue(Object value) 476 { 477 this.value = value; 478 } 479 480 void setValueEncoder(ValueEncoder encoder) 481 { 482 this.encoder = encoder; 483 } 484 485 void setValidationTracker(ValidationTracker tracker) 486 { 487 this.tracker = tracker; 488 } 489 490 void setBlankOption(BlankOption option, String label) 491 { 492 blankOption = option; 493 blankLabel = label; 494 } 495 496 void setRaw(boolean b) 497 { 498 raw = b; 499 } 500}