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}