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