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
015 package org.apache.tapestry5.internal.services;
016
017 import org.apache.tapestry5.Link;
018 import org.apache.tapestry5.ioc.ScopeConstants;
019 import org.apache.tapestry5.ioc.annotations.Scope;
020 import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
021 import org.apache.tapestry5.ioc.internal.util.InternalUtils;
022 import org.apache.tapestry5.services.ClientDataEncoder;
023 import org.apache.tapestry5.services.ClientDataSink;
024 import org.apache.tapestry5.services.PersistentFieldChange;
025 import org.apache.tapestry5.services.Request;
026
027 import java.io.ObjectInputStream;
028 import java.io.ObjectOutputStream;
029 import java.io.Serializable;
030 import java.util.Collection;
031 import java.util.Collections;
032 import 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)
040 public 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 }