    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    }