001// Copyright 2007, 2008, 2009 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(ServicesMessages
190                        .clientStateMustBeSerializable(newValue));
191
192            persistedValues.put(key, newValue);
193        }
194
195        clientData = null;
196    }
197
198    /**
199     * Refreshes the _persistedValues map if it is not up to date.
200     */
201    @SuppressWarnings("unchecked")
202    private void refreshMap()
203    {
204        if (mapUptoDate) return;
205
206        // Parse the client data to form the map.
207
208        restoreMapFromClientData();
209
210        mapUptoDate = true;
211    }
212
213    /**
214     * Restores the _persistedValues map from the client data provided in the incoming Request.
215     */
216    private void restoreMapFromClientData()
217    {
218        persistedValues.clear();
219
220        if (clientData == null) return;
221
222        ObjectInputStream in = null;
223
224        try
225        {
226            in = clientDataEncoder.decodeClientData(clientData);
227
228            int count = in.readInt();
229
230            for (int i = 0; i < count; i++)
231            {
232                Key key = (Key) in.readObject();
233                Object value = in.readObject();
234
235                persistedValues.put(key, value);
236            }
237        }
238        catch (Exception ex)
239        {
240            throw new RuntimeException(ServicesMessages.corruptClientState(), ex);
241        }
242        finally
243        {
244            InternalUtils.close(in);
245        }
246    }
247
248    private void refreshClientData()
249    {
250        // Client data will be null after a change to the map, or if there was no client data in the
251        // request. In any other case where the client data is non-null, it is by definition
252        // up-to date (since it is reset to null any time there's a change to the map).
253
254        if (clientData != null) return;
255
256        // Very typical: we're refreshing the client data but haven't created the map yet, and there
257        // was no value in the request. Leave it as null.
258
259        if (!mapUptoDate) return;
260
261        // Null is also appropriate when the persisted values are empty.
262
263        if (persistedValues.isEmpty()) return;
264
265        // Otherwise, time to update clientData from persistedValues
266
267        ClientDataSink sink = clientDataEncoder.createSink();
268
269        ObjectOutputStream os = sink.getObjectOutputStream();
270
271        try
272        {
273            os.writeInt(persistedValues.size());
274
275            for (Map.Entry<Key, Object> e : persistedValues.entrySet())
276            {
277                os.writeObject(e.getKey());
278                os.writeObject(e.getValue());
279            }
280        }
281        catch (Exception ex)
282        {
283            throw new RuntimeException(ex.getMessage(), ex);
284        }
285        finally
286        {
287            InternalUtils.close(os);
288        }
289
290        clientData = sink.getClientData();
291    }
292}