You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tapestry.apache.org by hl...@apache.org on 2007/05/17 23:48:45 UTC
svn commit: r539131 - in /tapestry/tapestry5/trunk/tapestry-core/src:
main/java/org/apache/tapestry/corelib/components/
main/java/org/apache/tapestry/internal/services/
main/java/org/apache/tapestry/internal/structure/
main/java/org/apache/tapestry/int...
Author: hlship
Date: Thu May 17 14:48:44 2007
New Revision: 539131
URL: http://svn.apache.org/viewvc?view=rev&rev=539131
Log:
TAPESTRY-1356: Implement client-side field persistence
Added:
tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/ClientPersistentFieldStorage.java
tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/ClientPersistentFieldStorageImpl.java
tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/ClientPersistentFieldStrategy.java
tapestry/tapestry5/trunk/tapestry-core/src/test/app1/WEB-INF/ClientPersistenceDemo.html
tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry/integration/app1/pages/ClientPersistenceDemo.java
tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry/internal/services/ClientPersistentFieldStorageImplTest.java
Modified:
tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/corelib/components/Form.java
tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/ComponentInvocation.java
tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/InternalModule.java
tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/PersistentFieldChangeImpl.java
tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/ServicesMessages.java
tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/structure/InternalComponentResourcesImpl.java
tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/structure/StructureMessages.java
tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/test/InternalBaseTestCase.java
tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/services/TapestryModule.java
tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/test/TapestryTestCase.java
tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry/internal/services/ServicesStrings.properties
tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry/internal/structure/StructureStrings.properties
tapestry/tapestry5/trunk/tapestry-core/src/site/apt/guide/persist.apt
tapestry/tapestry5/trunk/tapestry-core/src/test/app1/WEB-INF/Start.html
tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry/integration/IntegrationTests.java
Modified: tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/corelib/components/Form.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/corelib/components/Form.java?view=diff&rev=539131&r1=539130&r2=539131
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/corelib/components/Form.java (original)
+++ tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/corelib/components/Form.java Thu May 17 14:48:44 2007
@@ -42,6 +42,7 @@
import org.apache.tapestry.annotations.Persist;
import org.apache.tapestry.corelib.mixins.RenderInformals;
import org.apache.tapestry.dom.Element;
+import org.apache.tapestry.internal.TapestryInternalUtils;
import org.apache.tapestry.internal.services.HeartbeatImpl;
import org.apache.tapestry.internal.util.Base64ObjectInputStream;
import org.apache.tapestry.internal.util.Base64ObjectOutputStream;
@@ -334,9 +335,11 @@
String actionsBase64 = _request.getParameter(FORM_DATA);
+ ObjectInputStream ois = null;
+
try
{
- ObjectInputStream ois = new Base64ObjectInputStream(actionsBase64);
+ ois = new Base64ObjectInputStream(actionsBase64);
while (true)
{
@@ -350,11 +353,15 @@
}
catch (EOFException ex)
{
- // Expected.
+ // Expected
}
catch (Exception ex)
{
throw new RuntimeException(ex);
+ }
+ finally
+ {
+ TapestryInternalUtils.close(ois);
}
heartbeat.end();
Added: tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/ClientPersistentFieldStorage.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/ClientPersistentFieldStorage.java?view=auto&rev=539131
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/ClientPersistentFieldStorage.java (added)
+++ tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/ClientPersistentFieldStorage.java Thu May 17 14:48:44 2007
@@ -0,0 +1,33 @@
+// Copyright 2007 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry.internal.services;
+
+import org.apache.tapestry.Link;
+import org.apache.tapestry.services.PersistentFieldChange;
+import org.apache.tapestry.services.PersistentFieldStrategy;
+
+/**
+ * Describes an object that can store {@link PersistentFieldChange}s, and manage a query parameter
+ * stored into a {@link Link} to maining this data across requests.
+ */
+public interface ClientPersistentFieldStorage extends PersistentFieldStrategy
+{
+ /**
+ * Updates a link, adding a query parameter to it (if necessary) to store
+ *
+ * @param link
+ */
+ void updateLink(Link link);
+}
Added: tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/ClientPersistentFieldStorageImpl.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/ClientPersistentFieldStorageImpl.java?view=auto&rev=539131
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/ClientPersistentFieldStorageImpl.java (added)
+++ tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/ClientPersistentFieldStorageImpl.java Thu May 17 14:48:44 2007
@@ -0,0 +1,263 @@
+// Copyright 2007 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry.internal.services;
+
+import static org.apache.tapestry.ioc.IOCConstants.PERTHREAD_SCOPE;
+import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newMap;
+
+import java.io.ObjectInputStream;
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+
+import org.apache.tapestry.Link;
+import org.apache.tapestry.internal.TapestryInternalUtils;
+import org.apache.tapestry.internal.util.Base64ObjectInputStream;
+import org.apache.tapestry.internal.util.Base64ObjectOutputStream;
+import org.apache.tapestry.ioc.annotations.Scope;
+import org.apache.tapestry.ioc.internal.util.CollectionFactory;
+import org.apache.tapestry.services.PersistentFieldChange;
+import org.apache.tapestry.services.Request;
+
+/**
+ * Manages client-persistent values on behalf of a {@link ClientPersistentFieldStorageImpl}. Some
+ * effort is made to ensure that we don't uncessarily convert between objects and Base64 (the
+ * encoding used to record the value on the client).
+ */
+@Scope(PERTHREAD_SCOPE)
+public class ClientPersistentFieldStorageImpl implements ClientPersistentFieldStorage
+{
+ static final String PARAMETER_NAME = "t:state:client";
+
+ private static class Key implements Serializable
+ {
+ private static final long serialVersionUID = -2741540370081645945L;
+
+ private final String _pageName;
+
+ private final String _componentId;
+
+ private final String _fieldName;
+
+ Key(final String pageName, final String componentId, final String fieldName)
+ {
+ _pageName = pageName;
+ _componentId = componentId;
+ _fieldName = fieldName;
+ }
+
+ public boolean matches(String pageName)
+ {
+ return _pageName.equals(pageName);
+ }
+
+ public PersistentFieldChange toChange(Object value)
+ {
+ return new PersistentFieldChangeImpl(_componentId == null ? "" : _componentId,
+ _fieldName, value);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ final int PRIME = 31;
+
+ int result = 1;
+
+ result = PRIME * result + ((_componentId == null) ? 0 : _componentId.hashCode());
+
+ // _fieldName and _pageName are never null
+
+ result = PRIME * result + _fieldName.hashCode();
+ result = PRIME * result + _pageName.hashCode();
+
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (getClass() != obj.getClass()) return false;
+ final Key other = (Key) obj;
+
+ // _fieldName and _pageName are never null
+
+ if (!_fieldName.equals(other._fieldName)) return false;
+ if (!_pageName.equals(other._pageName)) return false;
+
+ if (_componentId == null)
+ {
+ if (other._componentId != null) return false;
+ }
+ else if (!_componentId.equals(other._componentId)) return false;
+
+ return true;
+ }
+ }
+
+ private final Map<Key, Object> _persistedValues = newMap();
+
+ private String _clientData;
+
+ private boolean _mapUptoDate = false;
+
+ public ClientPersistentFieldStorageImpl(Request request)
+ {
+ String value = request.getParameter(PARAMETER_NAME);
+
+ // MIME can encode to a '+' character; the browser converts that to a space; we convert it
+ // back.
+
+ _clientData = value == null ? null : value.replace(' ', '+');
+ }
+
+ public void updateLink(Link link)
+ {
+ refreshClientData();
+
+ if (_clientData != null) link.addParameter(PARAMETER_NAME, _clientData);
+ }
+
+ public Collection<PersistentFieldChange> gatherFieldChanges(String pageName)
+ {
+ refreshMap();
+
+ if (_persistedValues.isEmpty()) return Collections.emptyList();
+
+ Collection<PersistentFieldChange> result = CollectionFactory.newList();
+
+ for (Map.Entry<Key, Object> e : _persistedValues.entrySet())
+ {
+ Key key = e.getKey();
+
+ if (key.matches(pageName)) result.add(key.toChange(e.getValue()));
+ }
+
+ return result;
+ }
+
+ public void postChange(String pageName, String componentId, String fieldName, Object newValue)
+ {
+ refreshMap();
+
+ Key key = new Key(pageName, componentId, fieldName);
+
+ if (newValue == null)
+ _persistedValues.remove(key);
+ else
+ {
+ if (!Serializable.class.isInstance(newValue))
+ throw new IllegalArgumentException(ServicesMessages
+ .clientStateMustBeSerializable(newValue));
+
+ _persistedValues.put(key, newValue);
+ }
+
+ _clientData = null;
+ }
+
+ @SuppressWarnings("unchecked")
+ private void refreshMap()
+ {
+ if (_mapUptoDate) return;
+
+ // Parse the client data to form the map.
+
+ restoreMapFromClientData();
+
+ _mapUptoDate = true;
+ }
+
+ private void restoreMapFromClientData()
+ {
+ _persistedValues.clear();
+
+ if (_clientData == null) return;
+
+ ObjectInputStream in = null;
+
+ try
+ {
+ in = new Base64ObjectInputStream(_clientData);
+
+ int count = in.readInt();
+
+ for (int i = 0; i < count; i++)
+ {
+ Key key = (Key) in.readObject();
+ Object value = in.readObject();
+
+ _persistedValues.put(key, value);
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new RuntimeException(ServicesMessages.corruptClientState(), ex);
+ }
+ finally
+ {
+ TapestryInternalUtils.close(in);
+ }
+ }
+
+ private void refreshClientData()
+ {
+ // Client data will be null after a change to the map, or if there was no client data in the
+ // request. In any other case where the client data is non-null, it is by definition
+ // up-to date (since it is reset to null any time there's a change to the map).
+
+ if (_clientData != null) return;
+
+ // Very typical: we're refreshing the client data but haven't created the map yet, and there
+ // was no value in the request. Leave it as null.
+
+ if (!_mapUptoDate) return;
+
+ // Null is also appropriate when the persisted values are empty.
+
+ if (_persistedValues.isEmpty()) return;
+
+ // Otherwise, time to update _clientData from _persistedValues
+
+ Base64ObjectOutputStream os = null;
+
+ try
+ {
+ os = new Base64ObjectOutputStream();
+
+ os.writeInt(_persistedValues.size());
+
+ for (Map.Entry<Key, Object> e : _persistedValues.entrySet())
+ {
+ os.writeObject(e.getKey());
+ os.writeObject(e.getValue());
+ }
+
+ }
+ catch (Exception ex)
+ {
+ throw new RuntimeException(ex.getMessage(), ex);
+ }
+ finally
+ {
+ TapestryInternalUtils.close(os);
+ }
+
+ _clientData = os.toBase64();
+ }
+}
Added: tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/ClientPersistentFieldStrategy.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/ClientPersistentFieldStrategy.java?view=auto&rev=539131
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/ClientPersistentFieldStrategy.java (added)
+++ tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/ClientPersistentFieldStrategy.java Thu May 17 14:48:44 2007
@@ -0,0 +1,57 @@
+// Copyright 2007 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry.internal.services;
+
+import java.util.Collection;
+
+import org.apache.tapestry.Link;
+import org.apache.tapestry.services.PersistentFieldChange;
+import org.apache.tapestry.services.PersistentFieldStrategy;
+
+/**
+ * Implements simple client-persistent properties. Most of the logic is delegated to an instance of
+ * {@link ClientPersistentFieldStorage}. This division of layer allows this service to be a true
+ * singleton, and a listener to the {@link LinkFactory}, and allow per-request state to be isolated
+ * inside the other service.
+ */
+public class ClientPersistentFieldStrategy implements PersistentFieldStrategy, LinkFactoryListener
+{
+ private final ClientPersistentFieldStorage _storage;
+
+ public ClientPersistentFieldStrategy(ClientPersistentFieldStorage storage)
+ {
+ _storage = storage;
+ }
+
+ public Collection<PersistentFieldChange> gatherFieldChanges(String pageName)
+ {
+ return _storage.gatherFieldChanges(pageName);
+ }
+
+ public void postChange(String pageName, String componentId, String fieldName, Object newValue)
+ {
+ _storage.postChange(pageName, componentId, fieldName, newValue);
+ }
+
+ public void createdActionLink(Link link)
+ {
+ _storage.updateLink(link);
+ }
+
+ public void createdPageLink(Link link)
+ {
+ _storage.updateLink(link);
+ }
+}
Modified: tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/ComponentInvocation.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/ComponentInvocation.java?view=diff&rev=539131&r1=539130&r2=539131
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/ComponentInvocation.java (original)
+++ tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/ComponentInvocation.java Thu May 17 14:48:44 2007
@@ -15,6 +15,7 @@
package org.apache.tapestry.internal.services;
import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newMap;
+import static org.apache.tapestry.ioc.internal.util.Defense.notBlank;
import java.util.List;
import java.util.Map;
@@ -65,8 +66,7 @@
public String buildURI(boolean isForm)
{
String path = getPath();
- if (isForm || _parameters == null)
- return path;
+ if (isForm || _parameters == null) return path;
StringBuilder builder = new StringBuilder();
@@ -124,8 +124,10 @@
public void addParameter(String parameterName, String value)
{
- if (_parameters == null)
- _parameters = newMap();
+ notBlank(parameterName, "parameterName");
+ notBlank(value, "value");
+
+ if (_parameters == null) _parameters = newMap();
if (_parameters.containsKey(parameterName))
throw new IllegalArgumentException(ServicesMessages.parameterNameMustBeUnique(
Modified: tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/InternalModule.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/InternalModule.java?view=diff&rev=539131&r1=539130&r2=539131
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/InternalModule.java (original)
+++ tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/InternalModule.java Thu May 17 14:48:44 2007
@@ -32,10 +32,8 @@
import org.apache.tapestry.internal.bindings.LiteralBinding;
import org.apache.tapestry.internal.bindings.PropBindingFactory;
import org.apache.tapestry.internal.util.IntegerRange;
-import org.apache.tapestry.ioc.Configuration;
import org.apache.tapestry.ioc.Location;
import org.apache.tapestry.ioc.MappedConfiguration;
-import org.apache.tapestry.ioc.ObjectLocator;
import org.apache.tapestry.ioc.ObjectProvider;
import org.apache.tapestry.ioc.OrderedConfiguration;
import org.apache.tapestry.ioc.ServiceBinder;
@@ -43,14 +41,12 @@
import org.apache.tapestry.ioc.annotations.InjectService;
import org.apache.tapestry.ioc.annotations.Scope;
import org.apache.tapestry.ioc.annotations.Symbol;
-import org.apache.tapestry.ioc.internal.util.InternalUtils;
import org.apache.tapestry.ioc.services.ChainBuilder;
import org.apache.tapestry.ioc.services.ClassFactory;
import org.apache.tapestry.ioc.services.PropertyAccess;
import org.apache.tapestry.ioc.services.ThreadCleanupHub;
import org.apache.tapestry.ioc.services.ThreadLocale;
import org.apache.tapestry.ioc.services.TypeCoercer;
-import org.apache.tapestry.services.AliasContribution;
import org.apache.tapestry.services.ApplicationGlobals;
import org.apache.tapestry.services.ApplicationInitializer;
import org.apache.tapestry.services.ApplicationInitializerFilter;
@@ -61,6 +57,7 @@
import org.apache.tapestry.services.ComponentMessagesSource;
import org.apache.tapestry.services.Context;
import org.apache.tapestry.services.ObjectRenderer;
+import org.apache.tapestry.services.PersistentFieldStrategy;
import org.apache.tapestry.services.PropertyConduitSource;
import org.apache.tapestry.services.Request;
import org.apache.tapestry.services.RequestExceptionHandler;
@@ -88,22 +85,7 @@
binder.bind(RequestExceptionHandler.class, DefaultRequestExceptionHandler.class);
binder.bind(PageLinkHandler.class, PageLinkHandlerImpl.class);
binder.bind(ResourceStreamer.class, ResourceStreamerImpl.class);
- }
-
- @SuppressWarnings("unchecked")
- private static void add(Configuration<AliasContribution> configuration, ObjectLocator locator,
- Class... serviceInterfaces)
- {
- for (Class serviceInterface : serviceInterfaces)
- {
- String name = serviceInterface.getName();
- String serviceId = InternalUtils.lastTerm(name);
-
- AliasContribution contribution = AliasContribution.create(serviceInterface, locator
- .getService(serviceId, serviceInterface));
-
- configuration.add(contribution);
- }
+ binder.bind(ClientPersistentFieldStorage.class, ClientPersistentFieldStorageImpl.class);
}
public static void contributeTemplateParser(MappedConfiguration<String, URL> configuration)
@@ -534,5 +516,16 @@
checkInterval), "before:*");
configuration.add("Localization", new LocalizationFilter(localizationSetter));
+ }
+
+ public PersistentFieldStrategy buildClientPersistentFieldStrategy(LinkFactory linkFactory,
+ ServiceResources resources)
+ {
+ ClientPersistentFieldStrategy service = resources
+ .autobuild(ClientPersistentFieldStrategy.class);
+
+ linkFactory.addListener(service);
+
+ return service;
}
}
Modified: tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/PersistentFieldChangeImpl.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/PersistentFieldChangeImpl.java?view=diff&rev=539131&r1=539130&r2=539131
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/PersistentFieldChangeImpl.java (original)
+++ tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/PersistentFieldChangeImpl.java Thu May 17 14:48:44 2007
@@ -1,4 +1,4 @@
-// Copyright 2006 The Apache Software Foundation
+// Copyright 2006, 2007 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -12,42 +12,44 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package org.apache.tapestry.internal.services;
-
-import org.apache.tapestry.ioc.internal.util.Defense;
-import org.apache.tapestry.services.PersistentFieldChange;
-
-public class PersistentFieldChangeImpl implements PersistentFieldChange
-{
- private final String _componentId;
-
- private final String _fieldName;
-
- private final Object _value;
-
- public PersistentFieldChangeImpl(final String componentId, final String fieldName,
- final Object value)
- {
- Defense.notNull(componentId, "componentId");
- Defense.notBlank(fieldName, "fieldName");
-
- _componentId = componentId;
- _fieldName = fieldName;
- _value = value;
- }
-
- public String getComponentId()
- {
- return _componentId;
- }
-
- public String getFieldName()
- {
- return _fieldName;
- }
-
- public Object getValue()
- {
- return _value;
- }
-}
+package org.apache.tapestry.internal.services;
+
+import static org.apache.tapestry.ioc.internal.util.Defense.notBlank;
+import static org.apache.tapestry.ioc.internal.util.Defense.notNull;
+
+import org.apache.tapestry.services.PersistentFieldChange;
+
+public class PersistentFieldChangeImpl implements PersistentFieldChange
+{
+ private final String _componentId;
+
+ private final String _fieldName;
+
+ private final Object _value;
+
+ public PersistentFieldChangeImpl(final String componentId, final String fieldName,
+ final Object value)
+ {
+ notNull(componentId, "componentId");
+ notBlank(fieldName, "fieldName");
+
+ _componentId = componentId;
+ _fieldName = fieldName;
+ _value = value;
+ }
+
+ public String getComponentId()
+ {
+ return _componentId;
+ }
+
+ public String getFieldName()
+ {
+ return _fieldName;
+ }
+
+ public Object getValue()
+ {
+ return _value;
+ }
+}
Modified: tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/ServicesMessages.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/ServicesMessages.java?view=diff&rev=539131&r1=539130&r2=539131
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/ServicesMessages.java (original)
+++ tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/services/ServicesMessages.java Thu May 17 14:48:44 2007
@@ -378,4 +378,14 @@
{
return MESSAGES.format("field-injection-error", className, fieldName, cause);
}
+
+ static String clientStateMustBeSerializable(Object newValue)
+ {
+ return MESSAGES.format("client-state-must-be-serializable", newValue);
+ }
+
+ static String corruptClientState()
+ {
+ return MESSAGES.get("corrupt-client-state");
+ }
}
Modified: tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/structure/InternalComponentResourcesImpl.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/structure/InternalComponentResourcesImpl.java?view=diff&rev=539131&r1=539130&r2=539131
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/structure/InternalComponentResourcesImpl.java (original)
+++ tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/structure/InternalComponentResourcesImpl.java Thu May 17 14:48:44 2007
@@ -171,7 +171,17 @@
public void persistFieldChange(String fieldName, Object newValue)
{
- _element.persistFieldChange(this, fieldName, newValue);
+ try
+ {
+ _element.persistFieldChange(this, fieldName, newValue);
+ }
+ catch (Exception ex)
+ {
+ throw new TapestryException(StructureMessages.fieldPersistFailure(
+ getCompleteId(),
+ fieldName,
+ ex), getLocation(), ex);
+ }
}
public void bindParameter(String parameterName, Binding binding)
Modified: tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/structure/StructureMessages.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/structure/StructureMessages.java?view=diff&rev=539131&r1=539130&r2=539131
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/structure/StructureMessages.java (original)
+++ tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/structure/StructureMessages.java Thu May 17 14:48:44 2007
@@ -95,4 +95,9 @@
{
return MESSAGES.format("duplicate-block", component.getCompleteId(), blockId);
}
+
+ static String fieldPersistFailure(String componentId, String fieldName, Throwable cause)
+ {
+ return MESSAGES.format("field-persist-failure", componentId, fieldName, cause);
+ }
}
Modified: tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/test/InternalBaseTestCase.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/test/InternalBaseTestCase.java?view=diff&rev=539131&r1=539130&r2=539131
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/test/InternalBaseTestCase.java (original)
+++ tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/internal/test/InternalBaseTestCase.java Thu May 17 14:48:44 2007
@@ -489,11 +489,6 @@
expect(factory.createPageLink(page)).andReturn(link);
}
- protected final void train_getParameter(Request request, String elementName, String value)
- {
- expect(request.getParameter(elementName)).andReturn(value).atLeastOnce();
- }
-
protected final void train_isLoaded(InternalComponentResources resources, boolean isLoaded)
{
expect(resources.isLoaded()).andReturn(isLoaded);
Modified: tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/services/TapestryModule.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/services/TapestryModule.java?view=diff&rev=539131&r1=539130&r2=539131
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/services/TapestryModule.java (original)
+++ tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/services/TapestryModule.java Thu May 17 14:48:44 2007
@@ -256,15 +256,15 @@
* A few of the built in services overlap in terms of service interface so we make contributions
* to the Alias service to disambiguate. This ensures that a bare parameter (without an
* InjectService annotation) will chose the correct value without being further qualified.
- * <ul>
- * <li>{@link ComponentEventResultProcessor}: the master ComponentEventResultProcessor service
+ * <dl>
+ * <dt>{@link ComponentEventResultProcessor} <dd> the master ComponentEventResultProcessor service
* (rather than one of the other services that exist to handle a specific type of result)</li>
- * <li>{@link ObjectRenderer}: the master ObjectRenderer service (rather than the one of the
+ * <dt>{@link ObjectRenderer} <dd> the master ObjectRenderer service (rather than the one of the
* other services that renders a specific type of object)</li>
- * <li>{@link ClassFactory}: the <em>ComponentClassFactory</em> (which will be recreated if
+ * <dt>{@link ClassFactory} <dd> the <em>ComponentClassFactory</em> (which will be recreated if
* the component class loader is recreated, on a change to a component class)
- * <li>{@link DataTypeAnalyzer}: the <em>DefaultDataTypeAnalyzer</em> service
- * </ul>
+ * <dt>{@link DataTypeAnalyzer} <dd> the <em>DefaultDataTypeAnalyzer</em> service
+ * </dl>
*/
public static void contributeAlias(Configuration<AliasContribution> configuration,
ObjectLocator locator,
@@ -1394,15 +1394,27 @@
}
/**
- * Contributes the "session" strategy.
+ * Contributes several strategies:
+ * <dl>
+ * <dt>session
+ * <dd>Values are stored in the {@link Session}
+ * <dt>flash
+ * <dd>Values are stored in the {@link Session}, until the next request (for the page)
+ * <dt>client
+ * <dd>Values are encoded into URLs (or hidden form fields)
+ * </dl>
*/
public void contributePersistentFieldManager(
MappedConfiguration<String, PersistentFieldStrategy> configuration,
- Request request)
+ Request request,
+
+ @InjectService("ClientPersistentFieldStrategy")
+ PersistentFieldStrategy clientStrategy)
{
configuration.add("session", new SessionPersistentFieldStrategy(request));
configuration.add("flash", new FlashPersistentFieldStrategy(request));
+ configuration.add("client", clientStrategy);
}
public void contributeValidationMessagesSource(Configuration<String> configuration)
Modified: tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/test/TapestryTestCase.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/test/TapestryTestCase.java?view=diff&rev=539131&r1=539130&r2=539131
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/test/TapestryTestCase.java (original)
+++ tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry/test/TapestryTestCase.java Thu May 17 14:48:44 2007
@@ -970,4 +970,9 @@
expect(locatable.getLocation()).andReturn(location).atLeastOnce();
}
+ protected final void train_getParameter(Request request, String elementName, String value)
+ {
+ expect(request.getParameter(elementName)).andReturn(value).atLeastOnce();
+ }
+
}
Modified: tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry/internal/services/ServicesStrings.properties
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry/internal/services/ServicesStrings.properties?view=diff&rev=539131&r1=539130&r2=539131
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry/internal/services/ServicesStrings.properties (original)
+++ tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry/internal/services/ServicesStrings.properties Thu May 17 14:48:44 2007
@@ -73,4 +73,7 @@
request-exception=Processing of request failed with uncaught exception: %s
component-recursion=The template for component %s is recursive (contains another direct or indirect reference to component %<s). \
This is not supported (components may not contain themselves).
-field-injection-error=Error obtaining injected value for field %s.%s: %s
\ No newline at end of file
+field-injection-error=Error obtaining injected value for field %s.%s: %s
+client-state-must-be-serializable=State persisted on the client must be serializable, but %s does not implement the Serializable interface.
+corrupt-client-state=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.
\ No newline at end of file
Modified: tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry/internal/structure/StructureStrings.properties
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry/internal/structure/StructureStrings.properties?view=diff&rev=539131&r1=539130&r2=539131
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry/internal/structure/StructureStrings.properties (original)
+++ tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry/internal/structure/StructureStrings.properties Thu May 17 14:48:44 2007
@@ -33,3 +33,4 @@
Embedded component ids must be unique (excluding case, which is ignored).
duplicate-block=Component %s already contains a block with id '%s'. \
Block ids must be unique (excluding case, which is ignored).
+field-persist-failure=Error persisting field %s:%s: %s
Modified: tapestry/tapestry5/trunk/tapestry-core/src/site/apt/guide/persist.apt
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/site/apt/guide/persist.apt?view=diff&rev=539131&r1=539130&r2=539131
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/site/apt/guide/persist.apt (original)
+++ tapestry/tapestry5/trunk/tapestry-core/src/site/apt/guide/persist.apt Thu May 17 14:48:44 2007
@@ -33,9 +33,6 @@
The value for each field is the <strategy> used to store the field between requests.
- Currently, only the default value, "session", is supported. Other implementations, that store
- the value on the client, are forthcoming.
-
* session strategy
The session strategy stores field changes into the session; the session is created as necessary.
@@ -43,6 +40,8 @@
A suitably long session attribute name is used; it incorporates the
name of the page, the nested component id, and the name of the field.
+ Session strategy is the default strategy used unless otherwise overridden.
+
* flash strategy
The flash strategy stores information in the session as well, just for not very long. Values are
@@ -51,6 +50,23 @@
The flash is typically used to store temporary messages that should only be displayed to the user
once.
+
+* client stategy
+
+ The field is persisted onto the client; you will see an additional query parameter in each URL
+ (or an extra hidden field in each form).
+
+ Client persistence is somewhat expensive. It can bloat the size of the rendered pages by adding hundreds
+ of characters to each link. There is extra processing on each request to de-serialize the
+ values encoded into the query parameter.
+
+ Client persistence does not scale very well; as more information is stored into the query parameter, its
+ length can become problematic. In many cases, web browsers, firewalls or other servers may silently
+ truncate the URL which will break the application.
+
+ Use client persistence with care, and store a minimal amount of data. Try to store the identity (that is,
+ primary key) of an object, rather than the object itself.
+
Persistence Search
Added: tapestry/tapestry5/trunk/tapestry-core/src/test/app1/WEB-INF/ClientPersistenceDemo.html
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/test/app1/WEB-INF/ClientPersistenceDemo.html?view=auto&rev=539131
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/test/app1/WEB-INF/ClientPersistenceDemo.html (added)
+++ tapestry/tapestry5/trunk/tapestry-core/src/test/app1/WEB-INF/ClientPersistenceDemo.html Thu May 17 14:48:44 2007
@@ -0,0 +1,27 @@
+<html t:type="Border" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
+
+<h1>Client Persistence Demo</h1>
+
+
+ <p>
+ Persisted value: [${persistedValue}]
+ </p>
+
+ <p>
+ Session: [${sessionExists}]
+ </p>
+
+
+ <p>
+ <t:actionlink t:id="storeString">store string</t:actionlink>
+ </p>
+
+ <p>
+ <t:actionlink t:id="storeBad">store non-serializable</t:actionlink>
+ </p>
+
+ <p>
+ <t:pagelink page="clientpersistencedemo">refresh</t:pagelink>
+ </p>
+
+</html>
\ No newline at end of file
Modified: tapestry/tapestry5/trunk/tapestry-core/src/test/app1/WEB-INF/Start.html
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/test/app1/WEB-INF/Start.html?view=diff&rev=539131&r1=539130&r2=539131
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/test/app1/WEB-INF/Start.html (original)
+++ tapestry/tapestry5/trunk/tapestry-core/src/test/app1/WEB-INF/Start.html Thu May 17 14:48:44 2007
@@ -131,6 +131,9 @@
<t:pagelink page="inheritedbindingsdemo">Inherited Bindings Demo</t:pagelink> --
Tests for components that inherit bindings from containing components
</li>
+ <li>
+ <t:pagelink page="ClientPersistenceDemo">Client Persistence Demo</t:pagelink> -- component field values persisted on the client side
+ </li>
</ul>
</td>
</tr>
Modified: tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry/integration/IntegrationTests.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry/integration/IntegrationTests.java?view=diff&rev=539131&r1=539130&r2=539131
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry/integration/IntegrationTests.java (original)
+++ tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry/integration/IntegrationTests.java Thu May 17 14:48:44 2007
@@ -913,4 +913,17 @@
"Bound: [ value: the-bound-value, bound: true ]",
"Unbound: [ value: null, bound: false ]");
}
+
+ @Test
+ public void client_persistence()
+ {
+ open(BASE_URL);
+ clickAndWait("link=Client Persistence Demo");
+
+ assertTextPresent("Persisted value: []", "Session: [false]");
+
+ clickAndWait("link=store string");
+
+ assertTextPresent("Persisted value: [A String]", "Session: [false]");
+ }
}
Added: tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry/integration/app1/pages/ClientPersistenceDemo.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry/integration/app1/pages/ClientPersistenceDemo.java?view=auto&rev=539131
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry/integration/app1/pages/ClientPersistenceDemo.java (added)
+++ tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry/integration/app1/pages/ClientPersistenceDemo.java Thu May 17 14:48:44 2007
@@ -0,0 +1,53 @@
+// Copyright 2007 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry.integration.app1.pages;
+
+import org.apache.tapestry.annotations.Inject;
+import org.apache.tapestry.annotations.Persist;
+import org.apache.tapestry.services.Request;
+
+public class ClientPersistenceDemo
+{
+ @Persist("client")
+ private Object _persistedValue;
+
+ @Inject
+ private Request _request;
+
+ public Object getPersistedValue()
+ {
+ return _persistedValue;
+ }
+
+ public boolean getSessionExists()
+ {
+ return _request.getSession(false) != null;
+ }
+
+ void onActionFromStoreString()
+ {
+ _persistedValue = "A String";
+ }
+
+ void onActionFromStoreBad()
+ {
+ _persistedValue = new Runnable()
+ {
+ public void run()
+ {
+ }
+ };
+ }
+}
Added: tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry/internal/services/ClientPersistentFieldStorageImplTest.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry/internal/services/ClientPersistentFieldStorageImplTest.java?view=auto&rev=539131
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry/internal/services/ClientPersistentFieldStorageImplTest.java (added)
+++ tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry/internal/services/ClientPersistentFieldStorageImplTest.java Thu May 17 14:48:44 2007
@@ -0,0 +1,245 @@
+// Copyright 2007 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry.internal.services;
+
+import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newList;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.isA;
+
+import java.util.List;
+
+import org.apache.tapestry.Link;
+import org.apache.tapestry.internal.util.Holder;
+import org.apache.tapestry.services.PersistentFieldChange;
+import org.apache.tapestry.services.Request;
+import org.apache.tapestry.test.TapestryTestCase;
+import org.easymock.EasyMock;
+import org.easymock.IAnswer;
+import org.testng.annotations.Test;
+
+public class ClientPersistentFieldStorageImplTest extends TapestryTestCase
+{
+ @Test
+ public void no_client_data_in_request()
+ {
+ Request request = mockRequest(null);
+ Link link = mockLink();
+
+ replay();
+
+ ClientPersistentFieldStorage storage = new ClientPersistentFieldStorageImpl(request);
+
+ // Should do nothing.
+
+ storage.updateLink(link);
+
+ verify();
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void store_and_restore_a_change()
+ {
+ Request request = mockRequest(null);
+ Link link = mockLink();
+ final Holder<String> holder = Holder.create();
+
+ String pageName = "Foo";
+ String componentId = "bar.baz";
+ String fieldName = "biff";
+ Object value = 99;
+
+ // Use an IAnswer to capture the value.
+
+ link.addParameter(eq(ClientPersistentFieldStorageImpl.PARAMETER_NAME), isA(String.class));
+ getMocksControl().andAnswer(new IAnswer<Void>()
+ {
+ public Void answer() throws Throwable
+ {
+ String base64 = (String) EasyMock.getCurrentArguments()[1];
+
+ holder.put(base64);
+
+ return null;
+ }
+ });
+
+ replay();
+
+ ClientPersistentFieldStorage storage1 = new ClientPersistentFieldStorageImpl(request);
+
+ storage1.postChange(pageName, componentId, fieldName, value);
+
+ List<PersistentFieldChange> changes1 = newList(storage1.gatherFieldChanges(pageName));
+
+ storage1.updateLink(link);
+
+ verify();
+
+ System.out.println(holder.get());
+
+ assertEquals(changes1.size(), 1);
+ PersistentFieldChange change1 = changes1.get(0);
+
+ assertEquals(change1.getComponentId(), componentId);
+ assertEquals(change1.getFieldName(), fieldName);
+ assertEquals(change1.getValue(), value);
+
+ // Now more training ...
+
+ train_getParameter(request, ClientPersistentFieldStorageImpl.PARAMETER_NAME, holder.get());
+
+ replay();
+
+ ClientPersistentFieldStorage storage2 = new ClientPersistentFieldStorageImpl(request);
+
+ List<PersistentFieldChange> changes2 = newList(storage2.gatherFieldChanges(pageName));
+
+ verify();
+
+ assertEquals(changes2.size(), 1);
+ PersistentFieldChange change2 = changes2.get(0);
+
+ assertEquals(change2.getComponentId(), componentId);
+ assertEquals(change2.getFieldName(), fieldName);
+ assertEquals(change2.getValue(), value);
+
+ assertNotSame(change1, change2);
+ }
+
+ @Test
+ public void multiple_changes()
+ {
+ Request request = mockRequest(null);
+ Link link = mockLink();
+
+ String pageName = "Foo";
+ String componentId = "bar.baz";
+
+ link.addParameter(eq(ClientPersistentFieldStorageImpl.PARAMETER_NAME), isA(String.class));
+
+ replay();
+
+ ClientPersistentFieldStorage storage = new ClientPersistentFieldStorageImpl(request);
+
+ for (int k = 0; k < 3; k++)
+ {
+ for (int i = 0; i < 20; i++)
+ {
+ // Force some cache collisions ...
+
+ storage.postChange(pageName, componentId, "field" + i, i * k);
+ }
+ }
+
+ storage.postChange(pageName, null, "field", "foo");
+ storage.postChange(pageName, null, "field", "bar");
+
+ storage.updateLink(link);
+
+ verify();
+ }
+
+ @Test
+ public void null_value_is_a_remove()
+ {
+ Request request = mockRequest(null);
+ Link link = mockLink();
+
+ String pageName = "Foo";
+ String componentId = "bar.baz";
+ String fieldName = "woops";
+
+ replay();
+
+ ClientPersistentFieldStorage storage = new ClientPersistentFieldStorageImpl(request);
+
+ storage.postChange(pageName, componentId, fieldName, 99);
+ storage.postChange(pageName, componentId, fieldName, null);
+
+ storage.updateLink(link);
+
+ assertTrue(storage.gatherFieldChanges(pageName).isEmpty());
+
+ verify();
+ }
+
+ @Test
+ public void value_not_serializable()
+ {
+ Request request = mockRequest(null);
+
+ Object badBody = new Object()
+ {
+ @Override
+ public String toString()
+ {
+ return "<BadBoy>";
+ }
+ };
+
+ replay();
+
+ ClientPersistentFieldStorage storage = new ClientPersistentFieldStorageImpl(request);
+
+ try
+ {
+ storage.postChange("Foo", "bar.baz", "woops", badBody);
+ unreachable();
+ }
+ catch (IllegalArgumentException ex)
+ {
+ assertEquals(
+ ex.getMessage(),
+ "State persisted on the client must be serializable, but <BadBoy> does not implement the Serializable interface.");
+ }
+
+ verify();
+ }
+
+ @Test
+ public void corrupt_client_data()
+ {
+ // A cut-n-paste from some previous output, with the full value significantly truncated.
+ Request request = mockRequest("H4sIAAAAAAAAAEWQsUoDQRCGJxdDTEwRU2hlZ71pBQ");
+
+ replay();
+
+ ClientPersistentFieldStorage storage = new ClientPersistentFieldStorageImpl(request);
+
+ try
+ {
+ storage.postChange("Foo", "bar.baz", "woops", 99);
+ unreachable();
+ }
+ catch (RuntimeException ex)
+ {
+ assertEquals(ex.getMessage(), ServicesMessages.corruptClientState());
+ assertNotNull(ex.getCause());
+ }
+
+ verify();
+ }
+
+ protected final Request mockRequest(String clientData)
+ {
+ Request request = mockRequest();
+
+ train_getParameter(request, ClientPersistentFieldStorageImpl.PARAMETER_NAME, clientData);
+
+ return request;
+ }
+
+}