    001// Licensed under the Apache License, Version 2.0 (the "License");
    002// you may not use this file except in compliance with the License.
    003// You may obtain a copy of the License at
    004//
    005//     http://www.apache.org/licenses/LICENSE-2.0
    006//
    007// Unless required by applicable law or agreed to in writing, software
    008// distributed under the License is distributed on an "AS IS" BASIS,
    009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    010// See the License for the specific language governing permissions and
    011// limitations under the License.
    012
    013package org.apache.tapestry5.internal.services;
    014
    015import org.apache.tapestry5.beanmodel.services.*;
    016import org.apache.tapestry5.commons.util.CollectionFactory;
    017import org.apache.tapestry5.http.Link;
    018import org.apache.tapestry5.http.services.Request;
    019import org.apache.tapestry5.http.services.SessionPersistedObjectAnalyzer;
    020import org.apache.tapestry5.ioc.ScopeConstants;
    021import org.apache.tapestry5.ioc.annotations.Scope;
    022import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    023import org.apache.tapestry5.services.ClientDataEncoder;
    024import org.apache.tapestry5.services.ClientDataSink;
    025import org.apache.tapestry5.services.PersistentFieldChange;
    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            } else if (!componentId.equals(other.componentId)) return false;
    106
    107            return true;
    108        }
    109    }
    110
    111    private final ClientDataEncoder clientDataEncoder;
    112
    113    private final SessionPersistedObjectAnalyzer analyzer;
    114
    115    private final Map<Key, Object> persistedValues = CollectionFactory.newMap();
    116
    117    private String clientData;
    118
    119    private boolean mapUptoDate = false;
    120
    121    public ClientPersistentFieldStorageImpl(Request request, ClientDataEncoder clientDataEncoder, SessionPersistedObjectAnalyzer analyzer)
    122    {
    123        this.clientDataEncoder = clientDataEncoder;
    124        this.analyzer = analyzer;
    125
    126        // This, here, is the problem of TAPESTRY-2501; this call can predate
    127        // the check to set the character set based on meta data of the page.
    128
    129        String value = request.getParameter(PARAMETER_NAME);
    130
    131        // MIME can encode to a '+' character; the browser converts that to a space; we convert it
    132        // back.
    133
    134        clientData = value == null ? null : value.replace(' ', '+');
    135    }
    136
    137    public void updateLink(Link link)
    138    {
    139        refreshClientData();
    140
    141        if (clientData != null) link.addParameter(PARAMETER_NAME, clientData);
    142    }
    143
    144    public Collection<PersistentFieldChange> gatherFieldChanges(String pageName)
    145    {
    146        refreshMap();
    147
    148        if (persistedValues.isEmpty()) return Collections.emptyList();
    149
    150        Collection<PersistentFieldChange> result = CollectionFactory.newList();
    151
    152        for (Map.Entry<Key, Object> e : persistedValues.entrySet())
    153        {
    154            Key key = e.getKey();
    155
    156            if (key.matches(pageName)) result.add(key.toChange(e.getValue()));
    157        }
    158
    159        return result;
    160    }
    161
    162    public void discardChanges(String pageName)
    163    {
    164        refreshMap();
    165
    166        Collection<Key> removedKeys = CollectionFactory.newList();
    167
    168        for (Key key : persistedValues.keySet())
    169        {
    170            if (key.pageName.equals(pageName)) removedKeys.add(key);
    171        }
    172
    173        for (Key key : removedKeys)
    174        {
    175            persistedValues.remove(key);
    176            clientData = null;
    177        }
    178    }
    179
    180    public void postChange(String pageName, String componentId, String fieldName, Object newValue)
    181    {
    182        refreshMap();
    183
    184        Key key = new Key(pageName, componentId, fieldName);
    185
    186        if (newValue == null)
    187            persistedValues.remove(key);
    188        else
    189        {
    190            if (!Serializable.class.isInstance(newValue))
    191                throw new IllegalArgumentException(String.format("State persisted on the client must be serializable, but %s does not implement the Serializable interface.", newValue));
    192
    193            persistedValues.put(key, newValue);
    194        }
    195
    196        clientData = null;
    197    }
    198
    199    /**
    200     * Refreshes the _persistedValues map if it is not up to date.
    201     */
    202    @SuppressWarnings("unchecked")
    203    private void refreshMap()
    204    {
    205        if (mapUptoDate) return;
    206
    207        // Parse the client data to form the map.
    208
    209        restoreMapFromClientData();
    210
    211        mapUptoDate = true;
    212    }
    213
    214    /**
    215     * Restores the _persistedValues map from the client data provided in the incoming Request.
    216     */
    217    private void restoreMapFromClientData()
    218    {
    219        persistedValues.clear();
    220
    221        if (clientData == null) return;
    222
    223        ObjectInputStream in = null;
    224
    225        try
    226        {
    227            in = clientDataEncoder.decodeClientData(clientData);
    228
    229            int count = in.readInt();
    230
    231            for (int i = 0; i < count; i++)
    232            {
    233                Key key = (Key) in.readObject();
    234                Object value = in.readObject();
    235
    236                persistedValues.put(key, value);
    237            }
    238        } catch (Exception ex)
    239        {
    240            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);
    241        } finally
    242        {
    243            InternalUtils.close(in);
    244        }
    245    }
    246
    247    private void refreshClientData()
    248    {
    249        // TAP5-2269: Even in the absense of a change to a persistent field, a mutable persistent object
    250        // may have changed.
    251
    252        if (clientData != null)
    253        {
    254            for (Object value : persistedValues.values())
    255            {
    256                if (analyzer.checkAndResetDirtyState(value))
    257                {
    258                    clientData = null;
    259                    break;
    260                }
    261            }
    262        }
    263
    264        // Client data will be null after a change to the map, or if there was no client data in the
    265        // request. In any other case where the client data is non-null, it is by definition
    266        // up-to date (since it is reset to null any time there's a change to the map).
    267
    268        if (clientData != null) return;
    269
    270        // Very typical: we're refreshing the client data but haven't created the map yet, and there
    271        // was no value in the request. Leave it as null.
    272
    273        if (!mapUptoDate) return;
    274
    275        // Null is also appropriate when the persisted values are empty.
    276
    277        if (persistedValues.isEmpty()) return;
    278
    279        // Otherwise, time to update clientData from persistedValues
    280
    281        ClientDataSink sink = clientDataEncoder.createSink();
    282
    283        ObjectOutputStream os = sink.getObjectOutputStream();
    284
    285        try
    286        {
    287            os.writeInt(persistedValues.size());
    288
    289            for (Map.Entry<Key, Object> e : persistedValues.entrySet())
    290            {
    291                os.writeObject(e.getKey());
    292                os.writeObject(e.getValue());
    293            }
    294        } catch (Exception ex)
    295        {
    296            throw new RuntimeException(ex.getMessage(), ex);
    297        } finally
    298        {
    299            InternalUtils.close(os);
    300        }
    301
    302        clientData = sink.getClientData();
    303    }
    304}