    001// Licensed under the Apache License, Version 2.0 (the "License");
    002// you may not use this file except in compliance with the License.
    003// You may obtain a copy of the License at
    004//
    005// http://www.apache.org/licenses/LICENSE-2.0
    006//
    007// Unless required by applicable law or agreed to in writing, software
    008// distributed under the License is distributed on an "AS IS" BASIS,
    009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    010// See the License for the specific language governing permissions and
    011// limitations under the License.
    012
    013package org.apache.tapestry5.corelib.base;
    014
    015import org.apache.commons.lang3.StringEscapeUtils;
    016import org.apache.tapestry5.*;
    017import org.apache.tapestry5.annotations.*;
    018import org.apache.tapestry5.commons.internal.util.TapestryException;
    019import org.apache.tapestry5.corelib.mixins.DiscardBody;
    020import org.apache.tapestry5.corelib.mixins.RenderInformals;
    021import org.apache.tapestry5.http.services.Request;
    022import org.apache.tapestry5.internal.BeanValidationContext;
    023import org.apache.tapestry5.internal.InternalComponentResources;
    024import org.apache.tapestry5.internal.services.FormControlNameManager;
    025import org.apache.tapestry5.ioc.annotations.Inject;
    026import org.apache.tapestry5.ioc.annotations.Symbol;
    027import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    028import org.apache.tapestry5.services.ComponentDefaultProvider;
    029import org.apache.tapestry5.services.Environment;
    030import org.apache.tapestry5.services.FormSupport;
    031import org.apache.tapestry5.services.javascript.JavaScriptSupport;
    032
    033import java.io.Serializable;
    034
    035/**
    036 * Provides initialization of the clientId and elementName properties. In addition, adds the {@link RenderInformals},
    037 * and {@link DiscardBody} mixins.
    038 *
    039 * @tapestrydoc
    040 */
    041@SuppressWarnings("deprecation")
    042@SupportsInformalParameters
    043public abstract class AbstractField implements Field
    044{
    045    /**
    046     * The user presentable label for the field. If not provided, a reasonable label is generated from the component's
    047     * id, first by looking for a message key named "id-label" (substituting the component's actual id), then by
    048     * converting the actual id to a presentable string (for example, "userId" to "User Id").
    049     */
    050    @Parameter(defaultPrefix = BindingConstants.LITERAL)
    051    protected String label;
    052
    053    /**
    054     * If true, then the field will render out with a disabled attribute
    055     * (to turn off client-side behavior). When the form is submitted, the
    056     * bound value is evaluated again and, if true, the field's value is
    057     * ignored (not even validated) and the component's events are not fired.
    058     */
    059    @Parameter("false")
    060    protected boolean disabled;
    061
    062    @SuppressWarnings("unused")
    063    @Mixin
    064    private DiscardBody discardBody;
    065
    066    @Environmental
    067    protected ValidationDecorator decorator;
    068
    069    @Inject
    070    protected Environment environment;
    071
    072    @Inject
    073    @Symbol(SymbolConstants.FORM_FIELD_CSS_CLASS)
    074    protected String cssClass;
    075
    076    static class Setup implements ComponentAction<AbstractField>, Serializable
    077    {
    078        private static final long serialVersionUID = 2690270808212097020L;
    079
    080        private final String controlName;
    081
    082        public Setup(String controlName)
    083        {
    084            this.controlName = controlName;
    085        }
    086
    087        public void execute(AbstractField component)
    088        {
    089            component.setupControlName(controlName);
    090        }
    091
    092        @Override
    093        public String toString()
    094        {
    095            return String.format("AbstractField.Setup[%s]", controlName);
    096        }
    097    }
    098
    099    static class ProcessSubmission implements ComponentAction<AbstractField>, Serializable
    100    {
    101        private static final long serialVersionUID = -4346426414137434418L;
    102
    103        public void execute(AbstractField component)
    104        {
    105            component.processSubmission();
    106        }
    107
    108        @Override
    109        public String toString()
    110        {
    111            return "AbstractField.ProcessSubmission";
    112        }
    113    }
    114
    115    /**
    116     * Used a shared instance for all types of fields, for efficiency.
    117     */
    118    private static final ProcessSubmission PROCESS_SUBMISSION_ACTION = new ProcessSubmission();
    119
    120    /**
    121     * Used to explicitly set the client-side id of the element for this component. Normally this is not
    122     * bound (or null) and {@link org.apache.tapestry5.services.javascript.JavaScriptSupport#allocateClientId(org.apache.tapestry5.ComponentResources)}
    123     * is used to generate a unique client-id based on the component's id. In some cases, when creating client-side
    124     * behaviors, it is useful to explicitly set a unique id for an element using this parameter.
    125     *
    126     * Certain values, such as "submit", "method", "reset", etc., will cause client-side conflicts and are not allowed; using such will
    127     * cause a runtime exception.
    128     */
    129    @Parameter(defaultPrefix = BindingConstants.LITERAL)
    130    private String clientId;
    131
    132    /**
    133     * A rarely used option that indicates that the actual client id should start with the clientId parameter (if non-null)
    134     * but should still pass that Id through {@link org.apache.tapestry5.services.javascript.JavaScriptSupport#allocateClientId(String)}
    135     * to generate the final id.
    136     *
    137     * An example of this are the components used inside a {@link org.apache.tapestry5.corelib.components.BeanEditor} which
    138     * will specify a clientId (based on the property name) but still require that it be unique.
    139     *
    140     * Defaults to false.
    141     *
    142     * @since 5.4
    143     */
    144    @Parameter
    145    private boolean ensureClientIdUnique;
    146
    147
    148    private String assignedClientId;
    149
    150    private String controlName;
    151
    152    @Environmental(false)
    153    protected FormSupport formSupport;
    154
    155    @Environmental
    156    protected JavaScriptSupport javaScriptSupport;
    157
    158    @Environmental
    159    protected ValidationTracker validationTracker;
    160
    161    @Inject
    162    protected ComponentResources resources;
    163
    164    @Inject
    165    protected ComponentDefaultProvider defaultProvider;
    166
    167    @Inject
    168    protected Request request;
    169
    170    @Inject
    171    protected FieldValidationSupport fieldValidationSupport;
    172
    173    @Inject
    174    private FormControlNameManager formControlNameManager;
    175
    176    final String defaultLabel()
    177    {
    178        return defaultProvider.defaultLabel(resources);
    179    }
    180
    181    public final String getLabel()
    182    {
    183        return label;
    184    }
    185
    186    @SetupRender
    187    final void setup()
    188    {
    189        // Often, these controlName and clientId will end up as the same value. There are many
    190        // exceptions, including a form that renders inside a loop, or a form inside a component
    191        // that is used multiple times.
    192
    193        if (formSupport == null)
    194            throw new RuntimeException(String.format("Component %s must be enclosed by a Form component.",
    195                    resources.getCompleteId()));
    196
    197        assignedClientId = allocateClientId();
    198
    199        String controlName = formSupport.allocateControlName(assignedClientId);
    200
    201        formSupport.storeAndExecute(this, new Setup(controlName));
    202        formSupport.store(this, PROCESS_SUBMISSION_ACTION);
    203    }
    204
    205    private String allocateClientId()
    206    {
    207        if (clientId == null)
    208        {
    209            return javaScriptSupport.allocateClientId(resources);
    210        }
    211
    212
    213        if (ensureClientIdUnique)
    214        {
    215            return javaScriptSupport.allocateClientId(clientId);
    216        } else
    217        {
    218            // See https://issues.apache.org/jira/browse/TAP5-1632
    219            // Basically, on the client, there can be a convenience lookup inside a HTMLFormElement
    220            // by id OR name; so an id of "submit" (for example) will mask the HTMLFormElement.submit()
    221            // function.
    222
    223            if (formControlNameManager.isReserved(clientId))
    224            {
    225                throw new TapestryException(String.format(
    226                        "The value '%s' for parameter clientId is not allowed as it causes a naming conflict in the client-side DOM. " +
    227                                "Select an id not in the list: %s.",
    228                        clientId,
    229                        InternalUtils.joinSorted(formControlNameManager.getReservedNames())), this, null);
    230            }
    231        }
    232
    233        return clientId;
    234    }
    235
    236    public final String getClientId()
    237    {
    238        return assignedClientId;
    239    }
    240
    241    public final String getControlName()
    242    {
    243        return controlName;
    244    }
    245
    246    public final boolean isDisabled()
    247    {
    248        return disabled;
    249    }
    250
    251    /**
    252     * Invoked from within a ComponentCommand callback, to restore the component's elementName.
    253     */
    254    private void setupControlName(String controlName)
    255    {
    256        this.controlName = controlName;
    257    }
    258
    259    private void processSubmission()
    260    {
    261        if (!disabled)
    262            processSubmission(controlName);
    263    }
    264
    265    /**
    266     * Method implemented by subclasses to actually do the work of processing the submission of the form. The element's
    267     * controlName property will already have been set. This method is only invoked if the field is <strong>not
    268     * {@link #isDisabled() disabled}</strong>.
    269     *
    270     * @param controlName
    271     *         the control name of the rendered element (used to find the correct parameter in the request)
    272     */
    273    protected abstract void processSubmission(String controlName);
    274
    275    /**
    276     * Allows the validation decorator to write markup before the field itself writes markup.
    277     */
    278    @BeginRender
    279    final void beforeDecorator()
    280    {
    281        decorator.beforeField(this);
    282    }
    283
    284    /**
    285     * Allows the validation decorator to write markup after the field has written all of its markup.
    286     * In addition, may invoke the <code>core/fields:showValidationError</code> function to present
    287     * the field's error (if it has one) to the user.
    288     */
    289    @AfterRender
    290    final void afterDecorator()
    291    {
    292        decorator.afterField(this);
    293
    294        String error = validationTracker.getError(this);
    295        error = StringEscapeUtils.escapeHtml4(error);
    296
    297        if (error != null)
    298        {
    299            javaScriptSupport.require("t5/core/fields").invoke("showValidationError").with(assignedClientId, error);
    300        }
    301    }
    302
    303    /**
    304     * Invoked from subclasses after they have written their tag and (where appropriate) their informal parameters
    305     * <em>and</em> have allowed their {@link Validator} to write markup as well.
    306     */
    307    protected final void decorateInsideField()
    308    {
    309        decorator.insideField(this);
    310    }
    311
    312    protected final void setDecorator(ValidationDecorator decorator)
    313    {
    314        this.decorator = decorator;
    315    }
    316
    317    protected final void setFormSupport(FormSupport formSupport)
    318    {
    319        this.formSupport = formSupport;
    320    }
    321
    322    /**
    323     * Returns false; most components do not support declarative validation.
    324     */
    325    public boolean isRequired()
    326    {
    327        return false;
    328    }
    329
    330    // This is set to true for some unit test.
    331    private boolean beanValidationDisabled = false;
    332
    333    protected void putPropertyNameIntoBeanValidationContext(String parameterName)
    334    {
    335        if (beanValidationDisabled)
    336        {
    337            return;
    338        }
    339
    340        String propertyName = ((InternalComponentResources) resources).getPropertyName(parameterName);
    341
    342        BeanValidationContext beanValidationContext = environment.peek(BeanValidationContext.class);
    343
    344        if (beanValidationContext == null)
    345            return;
    346
    347        // If field is inside BeanEditForm, then property is already set
    348        if (beanValidationContext.getCurrentProperty() == null)
    349        {
    350            beanValidationContext.setCurrentProperty(propertyName);
    351        }
    352    }
    353
    354    protected void removePropertyNameFromBeanValidationContext()
    355    {
    356        if (beanValidationDisabled)
    357        {
    358            return;
    359        }
    360
    361        BeanValidationContext beanValidationContext = environment.peek(BeanValidationContext.class);
    362
    363        if (beanValidationContext == null)
    364            return;
    365
    366        beanValidationContext.setCurrentProperty(null);
    367    }
    368}