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    }