    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.commons.services.TypeCoercer;
    022import org.apache.tapestry5.http.services.Request;
    023import org.apache.tapestry5.internal.TapestryInternalUtils;
    024import org.apache.tapestry5.ioc.annotations.Inject;
    025import org.apache.tapestry5.services.ComponentDefaultProvider;
    026import org.apache.tapestry5.services.Environment;
    027import org.apache.tapestry5.services.FormSupport;
    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}