    001    // Copyright 2006, 2007, 2008, 2010, 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.base;
    016
    017    import org.apache.tapestry5.*;
    018    import org.apache.tapestry5.annotations.*;
    019    import org.apache.tapestry5.corelib.mixins.DiscardBody;
    020    import org.apache.tapestry5.corelib.mixins.RenderDisabled;
    021    import org.apache.tapestry5.corelib.mixins.RenderInformals;
    022    import org.apache.tapestry5.internal.BeanValidationContext;
    023    import org.apache.tapestry5.internal.InternalComponentResources;
    024    import org.apache.tapestry5.ioc.annotations.Inject;
    025    import org.apache.tapestry5.services.ComponentDefaultProvider;
    026    import org.apache.tapestry5.services.Environment;
    027    import org.apache.tapestry5.services.FormSupport;
    028    import org.apache.tapestry5.services.javascript.JavaScriptSupport;
    029
    030    import java.io.Serializable;
    031
    032    /**
    033     * Provides initialization of the clientId and elementName properties. In addition, adds the {@link RenderInformals},
    034     * {@link RenderDisabled} and {@link DiscardBody} mixins.
    035     */
    036    @SupportsInformalParameters
    037    public abstract class AbstractField implements Field
    038    {
    039        /**
    040         * The user presentable label for the field. If not provided, a reasonable label is generated from the component's
    041         * id, first by looking for a message key named "id-label" (substituting the component's actual id), then by
    042         * converting the actual id to a presentable string (for example, "userId" to "User Id").
    043         */
    044        @Parameter(defaultPrefix = BindingConstants.LITERAL)
    045        private String label;
    046
    047        /**
    048         * If true, then the field will render out with a disabled attribute
    049         * (to turn off client-side behavior). When the form is submitted, the
    050         * bound value is evaluated again and, if true, the field's value is
    051         * ignored (not even validated) and the component's events are not fired.
    052         */
    053        @Parameter("false")
    054        private boolean disabled;
    055
    056        @SuppressWarnings("unused")
    057        @Mixin
    058        private DiscardBody discardBody;
    059
    060        @Environmental
    061        private ValidationDecorator decorator;
    062
    063        @Inject
    064        private Environment environment;
    065
    066        static class Setup implements ComponentAction<AbstractField>, Serializable
    067        {
    068            private static final long serialVersionUID = 2690270808212097020L;
    069
    070            private final String controlName;
    071
    072            public Setup(String controlName)
    073            {
    074                this.controlName = controlName;
    075            }
    076
    077            public void execute(AbstractField component)
    078            {
    079                component.setupControlName(controlName);
    080            }
    081
    082            @Override
    083            public String toString()
    084            {
    085                return String.format("AbstractField.Setup[%s]", controlName);
    086            }
    087        }
    088
    089        static class ProcessSubmission implements ComponentAction<AbstractField>, Serializable
    090        {
    091            private static final long serialVersionUID = -4346426414137434418L;
    092
    093            public void execute(AbstractField component)
    094            {
    095                component.processSubmission();
    096            }
    097
    098            @Override
    099            public String toString()
    100            {
    101                return "AbstractField.ProcessSubmission";
    102            }
    103        }
    104
    105        /**
    106         * Used a shared instance for all types of fields, for efficiency.
    107         */
    108        private static final ProcessSubmission PROCESS_SUBMISSION_ACTION = new ProcessSubmission();
    109
    110        /**
    111         * The id used to generate a page-unique client-side identifier for the component. If a component renders multiple
    112         * times, a suffix will be appended to the to id to ensure uniqueness. The uniqued value may be accessed via the
    113         * {@link #getClientId() clientId property}.
    114         */
    115        @Parameter(value = "prop:componentResources.id", defaultPrefix = BindingConstants.LITERAL)
    116        private String clientId;
    117
    118        private String assignedClientId;
    119
    120        private String controlName;
    121
    122        @Environmental(false)
    123        private FormSupport formSupport;
    124
    125        @Environmental
    126        private JavaScriptSupport jsSupport;
    127
    128        @Inject
    129        private ComponentResources resources;
    130
    131        @Inject
    132        private ComponentDefaultProvider defaultProvider;
    133
    134        final String defaultLabel()
    135        {
    136            return defaultProvider.defaultLabel(resources);
    137        }
    138
    139        public final String getLabel()
    140        {
    141            return label;
    142        }
    143
    144        @SetupRender
    145        final void setup()
    146        {
    147            // By default, use the component id as the (base) client id. If the clientid
    148            // parameter is bound, then that is the value to use.
    149
    150            String id = clientId;
    151
    152            // Often, these controlName and clientId will end up as the same value. There are many
    153            // exceptions, including a form that renders inside a loop, or a form inside a component
    154            // that is used multiple times.
    155
    156            if (formSupport == null)
    157                throw new RuntimeException(String.format("Component %s must be enclosed by a Form component.",
    158                        resources.getCompleteId()));
    159
    160            assignedClientId = jsSupport.allocateClientId(id);
    161            String controlName = formSupport.allocateControlName(id);
    162
    163            formSupport.storeAndExecute(this, new Setup(controlName));
    164            formSupport.store(this, PROCESS_SUBMISSION_ACTION);
    165        }
    166
    167        public final String getClientId()
    168        {
    169            return assignedClientId;
    170        }
    171
    172        public final String getControlName()
    173        {
    174            return controlName;
    175        }
    176
    177        public final boolean isDisabled()
    178        {
    179            return disabled;
    180        }
    181
    182        /**
    183         * Invoked from within a ComponentCommand callback, to restore the component's elementName.
    184         */
    185        private void setupControlName(String controlName)
    186        {
    187            this.controlName = controlName;
    188        }
    189
    190        private void processSubmission()
    191        {
    192            if (!disabled)
    193                processSubmission(controlName);
    194        }
    195
    196        /**
    197         * Method implemented by subclasses to actually do the work of processing the submission of the form. The element's
    198         * controlName property will already have been set. This method is only invoked if the field is <strong>not
    199         * {@link #isDisabled() disabled}</strong>.
    200         *
    201         * @param controlName the control name of the rendered element (used to find the correct parameter in the request)
    202         */
    203        protected abstract void processSubmission(String controlName);
    204
    205        /**
    206         * Allows the validation decorator to write markup before the field itself writes markup.
    207         */
    208        @BeginRender
    209        final void beforeDecorator()
    210        {
    211            decorator.beforeField(this);
    212        }
    213
    214        /**
    215         * Allows the validation decorator to write markup after the field has written all of its markup.
    216         */
    217        @AfterRender
    218        final void afterDecorator()
    219        {
    220            decorator.afterField(this);
    221        }
    222
    223        /**
    224         * Invoked from subclasses after they have written their tag and (where appropriate) their informal parameters
    225         * <em>and</em> have allowed their {@link Validator} to write markup as well.
    226         */
    227        protected final void decorateInsideField()
    228        {
    229            decorator.insideField(this);
    230        }
    231
    232        protected final void setDecorator(ValidationDecorator decorator)
    233        {
    234            this.decorator = decorator;
    235        }
    236
    237        protected final void setFormSupport(FormSupport formSupport)
    238        {
    239            this.formSupport = formSupport;
    240        }
    241
    242        /**
    243         * Returns false; most components do not support declarative validation.
    244         */
    245        public boolean isRequired()
    246        {
    247            return false;
    248        }
    249
    250        protected void putPropertyNameIntoBeanValidationContext(String parameterName)
    251        {
    252            String propertyName = ((InternalComponentResources) resources).getPropertyName(parameterName);
    253
    254            BeanValidationContext beanValidationContext = environment.peek(BeanValidationContext.class);
    255
    256            if (beanValidationContext == null)
    257                return;
    258
    259            // If field is inside BeanEditForm, then property is already set
    260            if (beanValidationContext.getCurrentProperty() == null)
    261            {
    262                beanValidationContext.setCurrentProperty(propertyName);
    263            }
    264        }
    265
    266        protected void removePropertyNameFromBeanValidationContext()
    267        {
    268            BeanValidationContext beanValidationContext = environment.peek(BeanValidationContext.class);
    269
    270            if (beanValidationContext == null)
    271                return;
    272
    273            beanValidationContext.setCurrentProperty(null);
    274        }
    275    }