| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package org.apache.tapestry5.corelib.components; |
| |
| import org.apache.tapestry5.*; |
| import org.apache.tapestry5.annotations.*; |
| import org.apache.tapestry5.corelib.base.AbstractField; |
| import org.apache.tapestry5.corelib.data.BlankOption; |
| import org.apache.tapestry5.corelib.data.SecureOption; |
| import org.apache.tapestry5.corelib.mixins.RenderDisabled; |
| import org.apache.tapestry5.internal.AbstractEventContext; |
| import org.apache.tapestry5.internal.InternalComponentResources; |
| import org.apache.tapestry5.internal.TapestryInternalUtils; |
| import org.apache.tapestry5.internal.util.CaptureResultCallback; |
| import org.apache.tapestry5.internal.util.SelectModelRenderer; |
| import org.apache.tapestry5.ioc.Messages; |
| import org.apache.tapestry5.ioc.annotations.Inject; |
| import org.apache.tapestry5.ioc.internal.util.InternalUtils; |
| import org.apache.tapestry5.ioc.services.TypeCoercer; |
| import org.apache.tapestry5.services.*; |
| import org.apache.tapestry5.services.javascript.JavaScriptSupport; |
| import org.apache.tapestry5.util.EnumSelectModel; |
| |
| import java.util.Collections; |
| import java.util.List; |
| |
| /** |
| * Select an item from a list of values, using an [X]HTML <select> element on the client side. Any validation |
| * decorations will go around the entire <select> element. |
| * |
| * A core part of this component is the {@link ValueEncoder} (the encoder parameter) that is used to convert between |
| * server-side values and unique client-side strings. In some cases, a {@link ValueEncoder} can be generated automatically from |
| * the type of the value parameter. The {@link ValueEncoderSource} service provides an encoder in these situations; it |
| * can be overridden by binding the encoder parameter, or extended by contributing a {@link ValueEncoderFactory} into the |
| * service's configuration. |
| * |
| * @tapestrydoc |
| */ |
| @Events( |
| {EventConstants.VALIDATE, EventConstants.VALUE_CHANGED + " when 'zone' parameter is bound"}) |
| public class Select extends AbstractField |
| { |
| public static final String CHANGE_EVENT = "change"; |
| |
| private class Renderer extends SelectModelRenderer |
| { |
| |
| public Renderer(MarkupWriter writer) |
| { |
| super(writer, encoder, raw); |
| } |
| |
| @Override |
| protected boolean isOptionSelected(OptionModel optionModel, String clientValue) |
| { |
| return isSelected(clientValue); |
| } |
| } |
| |
| /** |
| * A ValueEncoder used to convert the server-side object provided by the |
| * "value" parameter into a unique client-side string (typically an ID) and |
| * back. Note: this parameter may be OMITTED if Tapestry is configured to |
| * provide a ValueEncoder automatically for the type of property bound to |
| * the "value" parameter. |
| * |
| * @see ValueEncoderSource |
| */ |
| @Parameter |
| private ValueEncoder encoder; |
| |
| /** |
| * Controls whether the submitted value is validated to be one of the values in |
| * the {@link SelectModel}. If "never", then no such validation is performed, |
| * theoretically allowing a selection to be made that was not presented to |
| * the user. Note that an "always" value here requires the SelectModel to |
| * still exist (or be created again) when the form is submitted, whereas a |
| * "never" value does not. Defaults to "auto", which causes the validation |
| * to occur only if the SelectModel is present (not null) when the form is |
| * submitted. |
| * |
| * @since 5.4 |
| */ |
| @Parameter(value = BindingConstants.SYMBOL + ":" + ComponentParameterConstants.VALIDATE_WITH_MODEL, defaultPrefix = BindingConstants.LITERAL) |
| private SecureOption secure; |
| |
| /** |
| * If true, then the provided {@link org.apache.tapestry5.SelectModel} labels will be written raw (no escaping of |
| * embedded HTML entities); it becomes the callers responsibility to escape any such entities. |
| * |
| * @since 5.4 |
| */ |
| @Parameter(value = "false") |
| private boolean raw; |
| |
| /** |
| * The model used to identify the option groups and options to be presented to the user. This can be generated |
| * automatically for Enum types. |
| */ |
| @Parameter(required = true, allowNull = false) |
| private SelectModel model; |
| |
| /** |
| * Controls whether an additional blank option is provided. The blank option precedes all other options and is never |
| * selected. The value for the blank option is always the empty string, the label may be the blank string; the |
| * label is from the blankLabel parameter (and is often also the empty string). |
| */ |
| @Parameter(value = "auto", defaultPrefix = BindingConstants.LITERAL) |
| private BlankOption blankOption; |
| |
| /** |
| * The label to use for the blank option, if rendered. If not specified, the container's message catalog is |
| * searched for a key, <code><em>id</em>-blanklabel</code>. |
| */ |
| @Parameter(defaultPrefix = BindingConstants.LITERAL) |
| private String blankLabel; |
| |
| @Inject |
| private Request request; |
| |
| @Environmental |
| private ValidationTracker tracker; |
| |
| /** |
| * Performs input validation on the value supplied by the user in the form submission. |
| */ |
| @Parameter(defaultPrefix = BindingConstants.VALIDATE) |
| private FieldValidator<Object> validate; |
| |
| /** |
| * The value to read or update. |
| */ |
| @Parameter(required = true, principal = true, autoconnect = true) |
| private Object value; |
| |
| /** |
| * Binding the zone parameter will cause any change of Select's value to be handled as an Ajax request that updates |
| * the |
| * indicated zone. The component will trigger the event {@link EventConstants#VALUE_CHANGED} to inform its |
| * container that Select's value has changed. |
| * |
| * @since 5.2.0 |
| */ |
| @Parameter(defaultPrefix = BindingConstants.LITERAL) |
| private String zone; |
| |
| /** |
| * The context for the "valueChanged" event triggered by this component (optional parameter). |
| * This list of values will be converted into strings and included in |
| * the URI. The strings will be coerced back to whatever their values are and made available to event handler |
| * methods. The first parameter of the context passed to "valueChanged" event handlers will |
| * still be the selected value chosen by the user, so the context passed through this parameter |
| * will be added from the second position on. |
| * |
| * @since 5.4 |
| */ |
| @Parameter |
| private Object[] context; |
| |
| @Inject |
| private FieldValidationSupport fieldValidationSupport; |
| |
| @Environmental |
| private FormSupport formSupport; |
| |
| @Inject |
| private JavaScriptSupport javascriptSupport; |
| |
| @Inject |
| private TypeCoercer typeCoercer; |
| |
| @SuppressWarnings("unused") |
| @Mixin |
| private RenderDisabled renderDisabled; |
| |
| private String selectedClientValue; |
| |
| private boolean isSelected(String clientValue) |
| { |
| return TapestryInternalUtils.isEqual(clientValue, selectedClientValue); |
| } |
| |
| @SuppressWarnings( |
| {"unchecked"}) |
| @Override |
| protected void processSubmission(String controlName) |
| { |
| String submittedValue = request.getParameter(controlName); |
| |
| tracker.recordInput(this, submittedValue); |
| |
| Object selectedValue; |
| |
| try |
| { |
| selectedValue = toValue(submittedValue); |
| } catch (ValidationException ex) |
| { |
| // Really, this will just be the logic related to the new (in 5.4) secure |
| // parameter: |
| |
| tracker.recordError(this, ex.getMessage()); |
| return; |
| } |
| |
| putPropertyNameIntoBeanValidationContext("value"); |
| |
| try |
| { |
| fieldValidationSupport.validate(selectedValue, resources, validate); |
| |
| value = selectedValue; |
| } catch (ValidationException ex) |
| { |
| tracker.recordError(this, ex.getMessage()); |
| } |
| |
| removePropertyNameFromBeanValidationContext(); |
| } |
| |
| void afterRender(MarkupWriter writer) |
| { |
| writer.end(); |
| } |
| |
| void beginRender(MarkupWriter writer) |
| { |
| writer.element("select", |
| "name", getControlName(), |
| "id", getClientId(), |
| "class", cssClass); |
| |
| putPropertyNameIntoBeanValidationContext("value"); |
| |
| validate.render(writer); |
| |
| removePropertyNameFromBeanValidationContext(); |
| |
| resources.renderInformalParameters(writer); |
| |
| decorateInsideField(); |
| |
| // Disabled is via a mixin |
| |
| if (this.zone != null) |
| { |
| javaScriptSupport.require("t5/core/select"); |
| |
| Link link = resources.createEventLink(CHANGE_EVENT, context); |
| |
| writer.attributes( |
| "data-update-zone", zone, |
| "data-update-url", link); |
| } |
| } |
| |
| Object onChange(final EventContext context, |
| @RequestParameter(value = "t:selectvalue", allowBlank = true) final String selectValue) |
| throws ValidationException |
| { |
| final Object newValue = toValue(selectValue); |
| |
| CaptureResultCallback<Object> callback = new CaptureResultCallback<Object>(); |
| |
| |
| EventContext newContext = new AbstractEventContext() { |
| |
| @Override |
| public int getCount() { |
| return context.getCount() + 1; |
| } |
| |
| @Override |
| public <T> T get(Class<T> desiredType, int index) { |
| if (index == 0) |
| { |
| return typeCoercer.coerce(newValue, desiredType); |
| } |
| return context.get(desiredType, index-1); |
| } |
| }; |
| |
| this.resources.triggerContextEvent(EventConstants.VALUE_CHANGED, newContext, callback); |
| |
| this.value = newValue; |
| |
| return callback.getResult(); |
| } |
| |
| protected Object toValue(String submittedValue) throws ValidationException |
| { |
| if (InternalUtils.isBlank(submittedValue)) |
| { |
| return null; |
| } |
| |
| // can we skip the check for the value being in the model? |
| |
| SelectModel selectModel = typeCoercer.coerce(((InternalComponentResources) resources) |
| .getBinding("model").get(), SelectModel.class); |
| if (secure == SecureOption.NEVER || (secure == SecureOption.AUTO && selectModel == null)) |
| { |
| return encoder.toValue(submittedValue); |
| } |
| |
| // for entity types the SelectModel may be unintentionally null when the form is submitted |
| if (selectModel == null) |
| { |
| throw new ValidationException("Model is null when validating submitted option." + |
| " To fix: persist the SeletModel or recreate it upon form submission," + |
| " or change the 'secure' parameter."); |
| } |
| |
| return findValueInModel(submittedValue); |
| } |
| |
| private Object findValueInModel(String submittedValue) throws ValidationException |
| { |
| |
| Object asSubmitted = encoder.toValue(submittedValue); |
| |
| // The visitor would be nice if it had the option to abort the visit |
| // early. |
| |
| if (findInOptions(model.getOptions(), asSubmitted)) |
| { |
| return asSubmitted; |
| } |
| |
| if (model.getOptionGroups() != null) |
| { |
| for (OptionGroupModel og : model.getOptionGroups()) |
| { |
| if (findInOptions(og.getOptions(), asSubmitted)) |
| { |
| return asSubmitted; |
| } |
| } |
| } |
| |
| throw new ValidationException("Selected option is not listed in the model."); |
| } |
| |
| private boolean findInOptions(List<OptionModel> options, Object asSubmitted) |
| { |
| if (options == null) |
| { |
| return false; |
| } |
| |
| // See TAP5-2184: Sometimes the SelectModel option values are Strings even though the |
| // submitted value (decoded by the ValueEncoder) are another type (e.g., numeric). In that case, |
| // pass each OptionModel value through the ValueEncoder for a comparison. |
| boolean alsoCompareDecodedModelValue = !(asSubmitted instanceof String); |
| |
| for (OptionModel om : options) |
| { |
| Object modelValue = om.getValue(); |
| if (modelValue.equals(asSubmitted)) |
| { |
| return true; |
| } |
| |
| if (alsoCompareDecodedModelValue && (modelValue instanceof String)) |
| { |
| Object decodedModelValue = encoder.toValue(modelValue.toString()); |
| |
| if (decodedModelValue.equals(asSubmitted)) |
| { |
| return true; |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| private static <T> List<T> orEmpty(List<T> list) |
| { |
| if (list == null) |
| { |
| return Collections.emptyList(); |
| } |
| |
| return list; |
| } |
| |
| @SuppressWarnings("unchecked") |
| ValueEncoder defaultEncoder() |
| { |
| return defaultProvider.defaultValueEncoder("value", resources); |
| } |
| |
| @SuppressWarnings("unchecked") |
| SelectModel defaultModel() |
| { |
| Class valueType = resources.getBoundType("value"); |
| |
| if (valueType == null) |
| return null; |
| |
| if (Enum.class.isAssignableFrom(valueType)) |
| return new EnumSelectModel(valueType, resources.getContainerMessages()); |
| |
| return null; |
| } |
| |
| /** |
| * Computes a default value for the "validate" parameter using {@link FieldValidatorDefaultSource}. |
| */ |
| Binding defaultValidate() |
| { |
| return defaultProvider.defaultValidatorBinding("value", resources); |
| } |
| |
| Object defaultBlankLabel() |
| { |
| Messages containerMessages = resources.getContainerMessages(); |
| |
| String key = resources.getId() + "-blanklabel"; |
| |
| if (containerMessages.contains(key)) |
| return containerMessages.get(key); |
| |
| return null; |
| } |
| |
| /** |
| * Renders the options, including the blank option. |
| */ |
| @BeforeRenderTemplate |
| void options(MarkupWriter writer) |
| { |
| selectedClientValue = tracker.getInput(this); |
| |
| // Use the value passed up in the form submission, if available. |
| // Failing that, see if there is a current value (via the value parameter), and |
| // convert that to a client value for later comparison. |
| |
| if (selectedClientValue == null) |
| selectedClientValue = value == null ? null : encoder.toClient(value); |
| |
| if (showBlankOption()) |
| { |
| writer.element("option", "value", ""); |
| writer.write(blankLabel); |
| writer.end(); |
| } |
| |
| SelectModelVisitor renderer = new Renderer(writer); |
| |
| model.visit(renderer); |
| } |
| |
| @Override |
| public boolean isRequired() |
| { |
| return validate.isRequired(); |
| } |
| |
| private boolean showBlankOption() |
| { |
| switch (blankOption) |
| { |
| case ALWAYS: |
| return true; |
| |
| case NEVER: |
| return false; |
| |
| default: |
| return !isRequired(); |
| } |
| } |
| |
| // For testing. |
| |
| void setModel(SelectModel model) |
| { |
| this.model = model; |
| blankOption = BlankOption.NEVER; |
| } |
| |
| void setValue(Object value) |
| { |
| this.value = value; |
| } |
| |
| void setValueEncoder(ValueEncoder encoder) |
| { |
| this.encoder = encoder; |
| } |
| |
| void setValidationTracker(ValidationTracker tracker) |
| { |
| this.tracker = tracker; |
| } |
| |
| void setBlankOption(BlankOption option, String label) |
| { |
| blankOption = option; |
| blankLabel = label; |
| } |
| |
| void setRaw(boolean b) |
| { |
| raw = b; |
| } |
| } |