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 }