001: // Copyright 2007 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.tapestry.internal.services;
016:
017: import static org.apache.tapestry.ioc.IOCConstants.PERTHREAD_SCOPE;
018: import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newMap;
019:
020: import java.io.ObjectInputStream;
021: import java.io.Serializable;
022: import java.util.Collection;
023: import java.util.Collections;
024: import java.util.Map;
025:
026: import org.apache.tapestry.Link;
027: import org.apache.tapestry.internal.TapestryInternalUtils;
028: import org.apache.tapestry.internal.util.Base64ObjectInputStream;
029: import org.apache.tapestry.internal.util.Base64ObjectOutputStream;
030: import org.apache.tapestry.ioc.annotations.Scope;
031: import org.apache.tapestry.ioc.internal.util.CollectionFactory;
032: import org.apache.tapestry.services.PersistentFieldChange;
033: import org.apache.tapestry.services.Request;
034:
035: /**
036: * Manages client-persistent values on behalf of a {@link ClientPersistentFieldStorageImpl}. Some
037: * effort is made to ensure that we don't uncessarily convert between objects and Base64 (the
038: * encoding used to record the value on the client).
039: */
040: @Scope(PERTHREAD_SCOPE)
041: public class ClientPersistentFieldStorageImpl implements
042: ClientPersistentFieldStorage {
043: static final String PARAMETER_NAME = "t:state:client";
044:
045: private static class Key implements Serializable {
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(final String pageName, final String componentId,
055: final String fieldName) {
056: _pageName = pageName;
057: _componentId = componentId;
058: _fieldName = fieldName;
059: }
060:
061: public boolean matches(String pageName) {
062: return _pageName.equals(pageName);
063: }
064:
065: public PersistentFieldChange toChange(Object value) {
066: return new PersistentFieldChangeImpl(
067: _componentId == null ? "" : _componentId,
068: _fieldName, value);
069: }
070:
071: @Override
072: public int hashCode() {
073: final int PRIME = 31;
074:
075: int result = 1;
076:
077: result = PRIME
078: * result
079: + ((_componentId == null) ? 0 : _componentId
080: .hashCode());
081:
082: // _fieldName and _pageName are never null
083:
084: result = PRIME * result + _fieldName.hashCode();
085: result = PRIME * result + _pageName.hashCode();
086:
087: return result;
088: }
089:
090: @Override
091: public boolean equals(Object obj) {
092: if (this == obj)
093: return true;
094: if (obj == null)
095: return false;
096: if (getClass() != obj.getClass())
097: return false;
098: final Key other = (Key) obj;
099:
100: // _fieldName and _pageName are never null
101:
102: if (!_fieldName.equals(other._fieldName))
103: return false;
104: if (!_pageName.equals(other._pageName))
105: return false;
106:
107: if (_componentId == null) {
108: if (other._componentId != null)
109: return false;
110: } else if (!_componentId.equals(other._componentId))
111: return false;
112:
113: return true;
114: }
115: }
116:
117: private final Map<Key, Object> _persistedValues = newMap();
118:
119: private String _clientData;
120:
121: private boolean _mapUptoDate = false;
122:
123: public ClientPersistentFieldStorageImpl(Request request) {
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: refreshClientData();
134:
135: if (_clientData != null)
136: link.addParameter(PARAMETER_NAME, _clientData);
137: }
138:
139: public Collection<PersistentFieldChange> gatherFieldChanges(
140: String pageName) {
141: refreshMap();
142:
143: if (_persistedValues.isEmpty())
144: return Collections.emptyList();
145:
146: Collection<PersistentFieldChange> result = CollectionFactory
147: .newList();
148:
149: for (Map.Entry<Key, Object> e : _persistedValues.entrySet()) {
150: Key key = e.getKey();
151:
152: if (key.matches(pageName))
153: result.add(key.toChange(e.getValue()));
154: }
155:
156: return result;
157: }
158:
159: public void postChange(String pageName, String componentId,
160: String fieldName, Object newValue) {
161: refreshMap();
162:
163: Key key = new Key(pageName, componentId, fieldName);
164:
165: if (newValue == null)
166: _persistedValues.remove(key);
167: else {
168: if (!Serializable.class.isInstance(newValue))
169: throw new IllegalArgumentException(ServicesMessages
170: .clientStateMustBeSerializable(newValue));
171:
172: _persistedValues.put(key, newValue);
173: }
174:
175: _clientData = null;
176: }
177:
178: @SuppressWarnings("unchecked")
179: private void refreshMap() {
180: if (_mapUptoDate)
181: return;
182:
183: // Parse the client data to form the map.
184:
185: restoreMapFromClientData();
186:
187: _mapUptoDate = true;
188: }
189:
190: private void restoreMapFromClientData() {
191: _persistedValues.clear();
192:
193: if (_clientData == null)
194: return;
195:
196: ObjectInputStream in = null;
197:
198: try {
199: in = new Base64ObjectInputStream(_clientData);
200:
201: int count = in.readInt();
202:
203: for (int i = 0; i < count; i++) {
204: Key key = (Key) in.readObject();
205: Object value = in.readObject();
206:
207: _persistedValues.put(key, value);
208: }
209: } catch (Exception ex) {
210: throw new RuntimeException(ServicesMessages
211: .corruptClientState(), ex);
212: } finally {
213: TapestryInternalUtils.close(in);
214: }
215: }
216:
217: private void refreshClientData() {
218: // Client data will be null after a change to the map, or if there was no client data in the
219: // request. In any other case where the client data is non-null, it is by definition
220: // up-to date (since it is reset to null any time there's a change to the map).
221:
222: if (_clientData != null)
223: return;
224:
225: // Very typical: we're refreshing the client data but haven't created the map yet, and there
226: // was no value in the request. Leave it as null.
227:
228: if (!_mapUptoDate)
229: return;
230:
231: // Null is also appropriate when the persisted values are empty.
232:
233: if (_persistedValues.isEmpty())
234: return;
235:
236: // Otherwise, time to update _clientData from _persistedValues
237:
238: Base64ObjectOutputStream os = null;
239:
240: try {
241: os = new Base64ObjectOutputStream();
242:
243: os.writeInt(_persistedValues.size());
244:
245: for (Map.Entry<Key, Object> e : _persistedValues.entrySet()) {
246: os.writeObject(e.getKey());
247: os.writeObject(e.getValue());
248: }
249:
250: } catch (Exception ex) {
251: throw new RuntimeException(ex.getMessage(), ex);
252: } finally {
253: TapestryInternalUtils.close(os);
254: }
255:
256: _clientData = os.toBase64();
257: }
258: }
|