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