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.beanmodel.services.*; 016import org.apache.tapestry5.commons.util.CollectionFactory; 017import org.apache.tapestry5.http.Link; 018import org.apache.tapestry5.http.services.Request; 019import org.apache.tapestry5.http.services.SessionPersistedObjectAnalyzer; 020import org.apache.tapestry5.ioc.ScopeConstants; 021import org.apache.tapestry5.ioc.annotations.Scope; 022import org.apache.tapestry5.ioc.internal.util.InternalUtils; 023import org.apache.tapestry5.services.ClientDataEncoder; 024import org.apache.tapestry5.services.ClientDataSink; 025import org.apache.tapestry5.services.PersistentFieldChange; 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 } else if (!componentId.equals(other.componentId)) return false; 106 107 return true; 108 } 109 } 110 111 private final ClientDataEncoder clientDataEncoder; 112 113 private final SessionPersistedObjectAnalyzer analyzer; 114 115 private final Map<Key, Object> persistedValues = CollectionFactory.newMap(); 116 117 private String clientData; 118 119 private boolean mapUptoDate = false; 120 121 public ClientPersistentFieldStorageImpl(Request request, ClientDataEncoder clientDataEncoder, SessionPersistedObjectAnalyzer analyzer) 122 { 123 this.clientDataEncoder = clientDataEncoder; 124 this.analyzer = analyzer; 125 126 // This, here, is the problem of TAPESTRY-2501; this call can predate 127 // the check to set the character set based on meta data of the page. 128 129 String value = request.getParameter(PARAMETER_NAME); 130 131 // MIME can encode to a '+' character; the browser converts that to a space; we convert it 132 // back. 133 134 clientData = value == null ? null : value.replace(' ', '+'); 135 } 136 137 public void updateLink(Link link) 138 { 139 refreshClientData(); 140 141 if (clientData != null) link.addParameter(PARAMETER_NAME, clientData); 142 } 143 144 public Collection<PersistentFieldChange> gatherFieldChanges(String pageName) 145 { 146 refreshMap(); 147 148 if (persistedValues.isEmpty()) return Collections.emptyList(); 149 150 Collection<PersistentFieldChange> result = CollectionFactory.newList(); 151 152 for (Map.Entry<Key, Object> e : persistedValues.entrySet()) 153 { 154 Key key = e.getKey(); 155 156 if (key.matches(pageName)) result.add(key.toChange(e.getValue())); 157 } 158 159 return result; 160 } 161 162 public void discardChanges(String pageName) 163 { 164 refreshMap(); 165 166 Collection<Key> removedKeys = CollectionFactory.newList(); 167 168 for (Key key : persistedValues.keySet()) 169 { 170 if (key.pageName.equals(pageName)) removedKeys.add(key); 171 } 172 173 for (Key key : removedKeys) 174 { 175 persistedValues.remove(key); 176 clientData = null; 177 } 178 } 179 180 public void postChange(String pageName, String componentId, String fieldName, Object newValue) 181 { 182 refreshMap(); 183 184 Key key = new Key(pageName, componentId, fieldName); 185 186 if (newValue == null) 187 persistedValues.remove(key); 188 else 189 { 190 if (!Serializable.class.isInstance(newValue)) 191 throw new IllegalArgumentException(String.format("State persisted on the client must be serializable, but %s does not implement the Serializable interface.", newValue)); 192 193 persistedValues.put(key, newValue); 194 } 195 196 clientData = null; 197 } 198 199 /** 200 * Refreshes the _persistedValues map if it is not up to date. 201 */ 202 @SuppressWarnings("unchecked") 203 private void refreshMap() 204 { 205 if (mapUptoDate) return; 206 207 // Parse the client data to form the map. 208 209 restoreMapFromClientData(); 210 211 mapUptoDate = true; 212 } 213 214 /** 215 * Restores the _persistedValues map from the client data provided in the incoming Request. 216 */ 217 private void restoreMapFromClientData() 218 { 219 persistedValues.clear(); 220 221 if (clientData == null) return; 222 223 ObjectInputStream in = null; 224 225 try 226 { 227 in = clientDataEncoder.decodeClientData(clientData); 228 229 int count = in.readInt(); 230 231 for (int i = 0; i < count; i++) 232 { 233 Key key = (Key) in.readObject(); 234 Object value = in.readObject(); 235 236 persistedValues.put(key, value); 237 } 238 } catch (Exception ex) 239 { 240 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); 241 } finally 242 { 243 InternalUtils.close(in); 244 } 245 } 246 247 private void refreshClientData() 248 { 249 // TAP5-2269: Even in the absense of a change to a persistent field, a mutable persistent object 250 // may have changed. 251 252 if (clientData != null) 253 { 254 for (Object value : persistedValues.values()) 255 { 256 if (analyzer.checkAndResetDirtyState(value)) 257 { 258 clientData = null; 259 break; 260 } 261 } 262 } 263 264 // Client data will be null after a change to the map, or if there was no client data in the 265 // request. In any other case where the client data is non-null, it is by definition 266 // up-to date (since it is reset to null any time there's a change to the map). 267 268 if (clientData != null) return; 269 270 // Very typical: we're refreshing the client data but haven't created the map yet, and there 271 // was no value in the request. Leave it as null. 272 273 if (!mapUptoDate) return; 274 275 // Null is also appropriate when the persisted values are empty. 276 277 if (persistedValues.isEmpty()) return; 278 279 // Otherwise, time to update clientData from persistedValues 280 281 ClientDataSink sink = clientDataEncoder.createSink(); 282 283 ObjectOutputStream os = sink.getObjectOutputStream(); 284 285 try 286 { 287 os.writeInt(persistedValues.size()); 288 289 for (Map.Entry<Key, Object> e : persistedValues.entrySet()) 290 { 291 os.writeObject(e.getKey()); 292 os.writeObject(e.getValue()); 293 } 294 } catch (Exception ex) 295 { 296 throw new RuntimeException(ex.getMessage(), ex); 297 } finally 298 { 299 InternalUtils.close(os); 300 } 301 302 clientData = sink.getClientData(); 303 } 304}