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    }