001// Copyright 2007, 2008, 2009, 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
015package org.apache.tapestry5.internal.services;
016
017import org.apache.tapestry5.Link;
018import org.apache.tapestry5.ioc.ScopeConstants;
019import org.apache.tapestry5.ioc.annotations.Scope;
020import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
021import org.apache.tapestry5.ioc.internal.util.InternalUtils;
022import org.apache.tapestry5.services.ClientDataEncoder;
023import org.apache.tapestry5.services.ClientDataSink;
024import org.apache.tapestry5.services.PersistentFieldChange;
025import org.apache.tapestry5.services.Request;
026
027import java.io.ObjectInputStream;
028import java.io.ObjectOutputStream;
029import java.io.Serializable;
030import java.util.Collection;
031import java.util.Collections;
032import java.util.Map;
033
034/**
035 * Manages client-persistent values on behalf of a {@link ClientPersistentFieldStorageImpl}. Some effort is made to
036 * ensure that we don't uncessarily convert between objects and Base64 (the encoding used to record the value on the
037 * client).
038 */
039@Scope(ScopeConstants.PERTHREAD)
040public class ClientPersistentFieldStorageImpl implements ClientPersistentFieldStorage
041{
042    static final String PARAMETER_NAME = "t:state:client";
043
044    private static class Key implements Serializable
045    {
046        private static final long serialVersionUID = -2741540370081645945L;
047
048        private final String pageName;
049
050        private final String componentId;
051
052        private final String fieldName;
053
054        Key(String pageName, String componentId, String fieldName)
055        {
056            this.pageName = pageName;
057            this.componentId = componentId;
058            this.fieldName = fieldName;
059        }
060
061        public boolean matches(String pageName)
062        {
063            return this.pageName.equals(pageName);
064        }
065
066        public PersistentFieldChange toChange(Object value)
067        {
068            return new PersistentFieldChangeImpl(componentId == null ? "" : componentId,
069                                                 fieldName, value);
070        }
071
072        @Override
073        public int hashCode()
074        {
075            final int PRIME = 31;
076
077            int result = 1;
078
079            result = PRIME * result + ((componentId == null) ? 0 : componentId.hashCode());
080
081            // fieldName and pageName are never null
082
083            result = PRIME * result + fieldName.hashCode();
084            result = PRIME * result + pageName.hashCode();
085
086            return result;
087        }
088
089        @Override
090        public boolean equals(Object obj)
091        {
092            if (this == obj) return true;
093            if (obj == null) return false;
094            if (getClass() != obj.getClass()) return false;
095            final Key other = (Key) obj;
096
097            // fieldName and pageName are never null
098
099            if (!fieldName.equals(other.fieldName)) return false;
100            if (!pageName.equals(other.pageName)) return false;
101
102            if (componentId == null)
103            {
104                if (other.componentId != null) return false;
105            }
106            else if (!componentId.equals(other.componentId)) return false;
107
108            return true;
109        }
110    }
111
112    private final ClientDataEncoder clientDataEncoder;
113
114    private final Map<Key, Object> persistedValues = CollectionFactory.newMap();
115
116    private String clientData;
117
118    private boolean mapUptoDate = false;
119
120    public ClientPersistentFieldStorageImpl(Request request, ClientDataEncoder clientDataEncoder)
121    {
122        this.clientDataEncoder = clientDataEncoder;
123
124        // This, here, is the problem of TAPESTRY-2501; this call can predate
125        // the check to set the character set based on meta data of the page.
126
127        String value = request.getParameter(PARAMETER_NAME);
128
129        // MIME can encode to a '+' character; the browser converts that to a space; we convert it
130        // back.
131
132        clientData = value == null ? null : value.replace(' ', '+');
133    }
134
135    public void updateLink(Link link)
136    {
137        refreshClientData();
138
139        if (clientData != null) link.addParameter(PARAMETER_NAME, clientData);
140    }
141
142    public Collection<PersistentFieldChange> gatherFieldChanges(String pageName)
143    {
144        refreshMap();
145
146        if (persistedValues.isEmpty()) return Collections.emptyList();
147
148        Collection<PersistentFieldChange> result = CollectionFactory.newList();
149
150        for (Map.Entry<Key, Object> e : persistedValues.entrySet())
151        {
152            Key key = e.getKey();
153
154            if (key.matches(pageName)) result.add(key.toChange(e.getValue()));
155        }
156
157        return result;
158    }
159
160    public void discardChanges(String pageName)
161    {
162        refreshMap();
163
164        Collection<Key> removedKeys = CollectionFactory.newList();
165
166        for (Key key : persistedValues.keySet())
167        {
168            if (key.pageName.equals(pageName)) removedKeys.add(key);
169        }
170
171        for (Key key : removedKeys)
172        {
173            persistedValues.remove(key);
174            clientData = null;
175        }
176    }
177
178    public void postChange(String pageName, String componentId, String fieldName, Object newValue)
179    {
180        refreshMap();
181
182        Key key = new Key(pageName, componentId, fieldName);
183
184        if (newValue == null)
185            persistedValues.remove(key);
186        else
187        {
188            if (!Serializable.class.isInstance(newValue))
189                throw new IllegalArgumentException(String.format("State persisted on the client must be serializable, but %s does not implement the Serializable interface.", newValue));
190
191            persistedValues.put(key, newValue);
192        }
193
194        clientData = null;
195    }
196
197    /**
198     * Refreshes the _persistedValues map if it is not up to date.
199     */
200    @SuppressWarnings("unchecked")
201    private void refreshMap()
202    {
203        if (mapUptoDate) return;
204
205        // Parse the client data to form the map.
206
207        restoreMapFromClientData();
208
209        mapUptoDate = true;
210    }
211
212    /**
213     * Restores the _persistedValues map from the client data provided in the incoming Request.
214     */
215    private void restoreMapFromClientData()
216    {
217        persistedValues.clear();
218
219        if (clientData == null) return;
220
221        ObjectInputStream in = null;
222
223        try
224        {
225            in = clientDataEncoder.decodeClientData(clientData);
226
227            int count = in.readInt();
228
229            for (int i = 0; i < count; i++)
230            {
231                Key key = (Key) in.readObject();
232                Object value = in.readObject();
233
234                persistedValues.put(key, value);
235            }
236        }
237        catch (Exception ex)
238        {
239            throw new RuntimeException("Serialized client state was corrupted. This may indicate that too much state is being stored, which can cause the encoded string to be truncated by the client web browser.", ex);
240        }
241        finally
242        {
243            InternalUtils.close(in);
244        }
245    }
246
247    private void refreshClientData()
248    {
249        // Client data will be null after a change to the map, or if there was no client data in the
250        // request. In any other case where the client data is non-null, it is by definition
251        // up-to date (since it is reset to null any time there's a change to the map).
252
253        if (clientData != null) return;
254
255        // Very typical: we're refreshing the client data but haven't created the map yet, and there
256        // was no value in the request. Leave it as null.
257
258        if (!mapUptoDate) return;
259
260        // Null is also appropriate when the persisted values are empty.
261
262        if (persistedValues.isEmpty()) return;
263
264        // Otherwise, time to update clientData from persistedValues
265
266        ClientDataSink sink = clientDataEncoder.createSink();
267
268        ObjectOutputStream os = sink.getObjectOutputStream();
269
270        try
271        {
272            os.writeInt(persistedValues.size());
273
274            for (Map.Entry<Key, Object> e : persistedValues.entrySet())
275            {
276                os.writeObject(e.getKey());
277                os.writeObject(e.getValue());
278            }
279        }
280        catch (Exception ex)
281        {
282            throw new RuntimeException(ex.getMessage(), ex);
283        }
284        finally
285        {
286            InternalUtils.close(os);
287        }
288
289        clientData = sink.getClientData();
290    }
291}