    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    }