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.components;
014
015import org.apache.tapestry5.*;
016import org.apache.tapestry5.annotations.Environmental;
017import org.apache.tapestry5.annotations.Parameter;
018import org.apache.tapestry5.annotations.SupportsInformalParameters;
019import org.apache.tapestry5.dom.Element;
020import org.apache.tapestry5.http.services.Request;
021import org.apache.tapestry5.ioc.annotations.Inject;
022import org.apache.tapestry5.ioc.internal.util.InternalUtils;
023import org.apache.tapestry5.services.ComponentDefaultProvider;
024import org.apache.tapestry5.services.FormSupport;
025import org.apache.tapestry5.services.javascript.JavaScriptSupport;
026
027/**
028 * Used to record a page property as a value into the form. The value is encoded
029 * when the form is rendered, then decoded after the form is submitted back to
030 * the server, and the "value" parameter updated.
031 *
032 * The encoding and decoding is done via a {@link org.apache.tapestry5.ValueEncoder},
033 * therefore you must either bind the "encoder" parameter to a ValueEncoder or
034 * use an entity type for the "value" parameter for which Tapestry can provide a
035 * ValueEncoder automatically.
036 * 
037 * @tapestrydoc
038 * @since 5.1.0.2
039 */
040@SupportsInformalParameters
041public class Hidden implements ClientElement
042{
043    /**
044     * The value to read (when rendering) or update (when the form is submitted).
045     */
046    @Parameter(required = true, autoconnect = true, principal = true)
047    private Object value;
048
049    /**
050     * Defines how nulls on the server side, or sent from the client side, are treated. The selected strategy may
051     * replace the nulls with some other value. The default strategy leaves nulls alone.  Another built-in strategy,
052     * zero, replaces nulls with the value 0.
053     */
054    @Parameter(defaultPrefix = BindingConstants.NULLFIELDSTRATEGY, value = "default")
055    private NullFieldStrategy nulls;
056
057    /**
058     * A ValueEncoder used to convert the server-side object provided by the
059     * "value" parameter into a unique client-side string (typically an ID) and
060     * back. Note: this parameter may be OMITTED if Tapestry is configured to
061     * provide a ValueEncoder automatically for the type of property bound to
062     * the "value" parameter. 
063     */
064    @Parameter(required = true)
065    private ValueEncoder encoder;
066
067    private String clientId;
068
069    private String controlName;
070
071    private Element hiddenInputElement;
072
073    @Environmental(false)
074    private FormSupport formSupport;
075
076    @Environmental
077    private JavaScriptSupport jsSupport;
078
079    @Inject
080    private ComponentResources resources;
081
082    @Inject
083    private ComponentDefaultProvider defaultProvider;
084
085    @Inject
086    private Request request;
087
088    ValueEncoder defaultEncoder()
089    {
090        return defaultProvider.defaultValueEncoder("value", resources);
091    }
092
093    static class ProcessSubmission implements ComponentAction<Hidden>
094    {
095        private final String controlName;
096
097        public ProcessSubmission(String controlName)
098        {
099            this.controlName = controlName;
100        }
101
102        public void execute(Hidden component)
103        {
104            component.processSubmission(controlName);
105        }
106    }
107
108    boolean beginRender(MarkupWriter writer)
109    {
110        if (formSupport == null)
111        {
112            throw new RuntimeException("The Hidden component must be enclosed by a Form component.");
113        }
114
115        controlName = formSupport.allocateControlName(resources.getId());
116
117        clientId = null;
118
119        formSupport.store(this, new ProcessSubmission(controlName));
120
121        Object toEncode = value == null ? nulls.replaceToClient() : value;
122
123        String encoded = toEncode == null ? "" : encoder.toClient(toEncode);
124
125        hiddenInputElement = writer.element("input", "type", "hidden", "name", controlName, "value", encoded);
126
127        resources.renderInformalParameters(writer);
128
129        writer.end();
130
131        return false;
132    }
133
134    private void processSubmission(String controlName)
135    {
136        String encoded = request.getParameter(controlName);
137
138        String toDecode = InternalUtils.isBlank(encoded) ? nulls.replaceFromClient() : encoded;
139
140        Object decoded = toDecode == null ? null : encoder.toValue(toDecode);
141
142        value = decoded;
143    }
144
145    public String getClientId()
146    {
147        if (clientId == null)
148        {
149            clientId = jsSupport.allocateClientId(resources);
150            hiddenInputElement.forceAttributes("id", clientId);
151        }
152
153        return clientId;
154    }
155
156    public String getControlName()
157    {
158        return controlName;
159    }
160}