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