Author: hlship
Date: Tue Jan 16 18:50:12 2007
New Revision: 496918

URL: http://svn.apache.org/viewvc?view=rev&rev=496918
Log:
Refine the PrimaryKeyEncoder interface and DefaultPrimaryKeyEncoder 
implementation.
Refine and test the Loop component's serialization of state in to the enclosing 
Form, both in volatile and non-volatile mode.

Added:
    
tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/TapestryMessages.java
    
tapestry/tapestry5/tapestry-core/trunk/src/main/resources/org/apache/tapestry/TapestryStrings.properties
    
tapestry/tapestry5/tapestry-core/trunk/src/test/app1/WEB-INF/ToDoListVolatile.html
      - copied, changed from r496578, 
tapestry/tapestry5/tapestry-core/trunk/src/test/app1/WEB-INF/ToDoList.html
    
tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/integration/app1/pages/ToDoListVolatile.java
      - copied, changed from r496578, 
tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/integration/app1/pages/ToDoList.java
Modified:
    
tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/DefaultPrimaryKeyEncoder.java
    
tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/PrimaryKeyEncoder.java
    
tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/corelib/components/Form.java
    
tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/corelib/components/Loop.java
    
tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/internal/util/Holder.java
    tapestry/tapestry5/tapestry-core/trunk/src/test/app1/index.html
    
tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/DefaultPrimaryKeyEncoderTest.java
    
tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/integration/IntegrationTests.java
    
tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/integration/app1/services/ToDoDatabase.java
    
tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/integration/app1/services/ToDoDatabaseImpl.java

Modified: 
tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/DefaultPrimaryKeyEncoder.java
URL: 
http://svn.apache.org/viewvc/tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/DefaultPrimaryKeyEncoder.java?view=diff&rev=496918&r1=496917&r2=496918
==============================================================================
--- 
tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/DefaultPrimaryKeyEncoder.java
 (original)
+++ 
tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/DefaultPrimaryKeyEncoder.java
 Tue Jan 16 18:50:12 2007
@@ -16,24 +16,28 @@
 
 import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newList;
 import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newMap;
+import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newSet;
 
 import java.io.Serializable;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import org.apache.tapestry.ioc.internal.util.Defense;
 
 /**
  * A default, extensible version of [EMAIL PROTECTED] PrimaryKeyEncoder} that 
is based on loading known values
- * into an internal map.
+ * into an internal map. When there's a reasonable number (hundreds, perhaps 
thousands) of items to
+ * choose from, and those items are fast and cheap to read and instantiate, 
this implementation is a
+ * good bet. For very large result sets, you'll need to create your own 
implementation of
+ * [EMAIL PROTECTED] PrimaryKeyEncoder}.
  * 
  * @param <K>
- *            the key type
+ *            the key type (which must be serializable)
  * @param <V>
  *            the value type
  */
-public abstract class DefaultPrimaryKeyEncoder<K extends Serializable, V> 
implements
-        PrimaryKeyEncoder<K, V>
+public class DefaultPrimaryKeyEncoder<K extends Serializable, V> implements 
PrimaryKeyEncoder<K, V>
 {
     private final Map<K, V> _keyToValue = newMap();
 
@@ -41,30 +45,67 @@
 
     private final List<K> _orderedKeys = newList();
 
+    private Set<K> _deletedKeys;
+
+    private K _currentKey;
+
     /** Adds a new key/value pair to the encoder. */
     public final void add(K key, V value)
     {
         Defense.notNull(key, "key");
         Defense.notNull(value, "value");
 
-        // TODO: Ensure the the key is unique
+        V existing = _keyToValue.get(key);
+        if (existing != null)
+            throw new 
IllegalArgumentException(TapestryMessages.duplicateKey(key, value, existing));
 
         _keyToValue.put(key, value);
         _orderedKeys.add(key);
 
-        // TODO: Ensure that the value is unique
+        // TODO: Ensure that the value is unique?
 
         _valueToKey.put(value, key);
     }
 
     /**
      * Returns the values previously [EMAIL PROTECTED] #add(Serializable, 
Object) added to the encoder},
-     * <em>in the order in which they were added</em>.
+     * <em>in the order in which they were added</em>. Values that are deleted 
are not returned.
      * 
      * @return ordered list of values
      */
     public final List<V> getValues()
     {
+        return valuesNotInKeySet(_deletedKeys);
+    }
+
+    /**
+     * Returns a list of all the values <em>except</em> those values whose 
keys are in the
+     * provided set. The set may be null, in which case all values are 
returned.
+     * 
+     * @param keySet
+     *            set of keys identifying values to exclude, or null to 
exclude no values
+     * @return values (not in the set) in order origionally added
+     */
+    protected final List<V> valuesNotInKeySet(Set<K> keySet)
+    {
+        if (keySet == null || keySet.isEmpty())
+            return getAllValues();
+
+        List<V> result = newList();
+
+        for (K key : _orderedKeys)
+        {
+            if (keySet.contains(key))
+                continue;
+
+            result.add(_keyToValue.get(key));
+        }
+
+        return result;
+    }
+
+    public final List<V> getAllValues()
+    {
         List<V> result = newList();
 
         for (K key : _orderedKeys)
@@ -79,27 +120,43 @@
      * For a previously [EMAIL PROTECTED] #add(Serializable, Object) added 
key/value pair}, returns the key
      * corresponding to the given value.
      */
-    public final K extractKey(V value)
+    public final K toKey(V value)
     {
-        return _valueToKey.get(value);
+        Defense.notNull(value, "value");
+
+        _currentKey = _valueToKey.get(value);
+
+        if (_currentKey == null)
+            throw new 
IllegalArgumentException(TapestryMessages.missingValue(value, _valueToKey
+                    .keySet()));
+
+        return _currentKey;
     }
 
-    public final V recoverObject(K key)
+    public final V toValue(K key)
     {
         V result = _keyToValue.get(key);
 
         if (result == null)
+        {
             result = provideMissingObject(key);
 
+            _currentKey = key;
+        }
+        else
+        {
+            _currentKey = key;
+        }
+
         return result;
     }
 
     /**
-     * Invoked by [EMAIL PROTECTED] #recoverObject(Serializable)} whenever a 
key can not be converted to a
-     * value using the internal cache. This is an opportunity to record the 
fact that an error
-     * occured (they key was not valuable, possibly because it points to a 
deleted entity object)
-     * and provide a temporary object. This method may return null, but in a 
typical application,
-     * that will likely case NullPointerExceptions further down the processing 
chain.
+     * Invoked by [EMAIL PROTECTED] #toValue(Serializable)} whenever a key can 
not be converted to a value
+     * using the internal cache. This is an opportunity to record the fact 
that an error occured
+     * (they key was not valuable, possibly because it points to a deleted 
entity object) and
+     * provide a temporary object. This method may return null, but in a 
typical application, that
+     * will likely case NullPointerExceptions further down the processing 
chain.
      * <p>
      * This implementation returns null, and is intended to be overriden in 
subclasses.
      * 
@@ -112,4 +169,66 @@
         return null;
     }
 
+    public final boolean isDeleted()
+    {
+        return inKeySet(_deletedKeys);
+    }
+
+    public final void setDeleted(boolean value)
+    {
+        _deletedKeys = modifyKeySet(_deletedKeys, value);
+    }
+
+    /**
+     * Returns true if the current key is in the provided set.
+     * 
+     * @param keySet
+     *            the set of keys to check, or null
+     * @return true if the key is in the set, false if it is missing (or if 
keySet is null)
+     */
+    protected final boolean inKeySet(Set<K> keySet)
+    {
+        return keySet != null ? keySet.contains(_currentKey) : false;
+    }
+
+    /**
+     * Modifies a keySet to add or remove the current key. If necessary, a new 
Set is created.
+     * <p>
+     * Useage: <code>
+     * private Set<K> _myFlagKeys;
+     * 
+     * public boolean void setMyFlag(boolean value)
+     * {
+     *   _myFlagKeys = modifySet(_myFlagKeys, value);
+     * }
+     * </code>
+     * 
+     * @param keySet
+     *            the set of keys, or null
+     * @param value
+     *            true to add the current key, false to remove
+     * @return the provided key set, or a new one
+     */
+    protected final Set<K> modifyKeySet(Set<K> keySet, boolean value)
+    {
+        if (keySet == null)
+        {
+            if (!value)
+                return null;
+
+            keySet = newSet();
+        }
+
+        if (value)
+            keySet.add(_currentKey);
+        else
+            keySet.remove(_currentKey);
+
+        return keySet;
+    }
+
+    /** Does nothing. Subclasses may override as necessary. */
+    public void prepareForKeys(List<K> keys)
+    {
+    }
 }

Modified: 
tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/PrimaryKeyEncoder.java
URL: 
http://svn.apache.org/viewvc/tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/PrimaryKeyEncoder.java?view=diff&rev=496918&r1=496917&r2=496918
==============================================================================
--- 
tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/PrimaryKeyEncoder.java
 (original)
+++ 
tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/PrimaryKeyEncoder.java
 Tue Jan 16 18:50:12 2007
@@ -15,6 +15,7 @@
 package org.apache.tapestry;
 
 import java.io.Serializable;
+import java.util.List;
 
 import org.apache.tapestry.corelib.components.Loop;
 
@@ -24,7 +25,7 @@
  * side object.
  * 
  * @param <K>
- *            the type of the primary key
+ *            the type of the primary key, used to identify the value (which 
must be serializable)
  * @param <V>
  *            the type of value identified by the key
  */
@@ -33,21 +34,31 @@
     /**
      * Given a particular value, this method extracts and returns the primary 
key that identifies
      * the value. The key will later be converted back into a value using
-     * [EMAIL PROTECTED] #recoverObject(Serializable)}.
+     * [EMAIL PROTECTED] #toValue(Serializable)}.
      * 
      * @param value
      *            whose primary key is needed
      * @return the key for the value
      */
-    K extractKey(V value);
+    K toKey(V value);
 
     /**
-     * For a particular primary key, previously obtained via [EMAIL PROTECTED] 
#extractKey(Object)}, this
-     * method returns the same or equivalent object.
+     * Invoked as part of a form submission to alert the encoder that a series 
of keys may be
+     * converted back to values. This is advisory only, and the keys passed to
+     * [EMAIL PROTECTED] #toValue(Serializable)} may not include all keys in 
the list, or may include keys not
+     * in the list. In general, though, the keys passed in will match the 
actual keys to be
+     * converted, giving the encoder a chance to efficiently fetch the 
necessary value objects as a
+     * group.
+     */
+    void prepareForKeys(List<K> keys);
+
+    /**
+     * For a particular primary key, previously obtained via [EMAIL PROTECTED] 
#toKey(Object)}, this method
+     * returns the same or equivalent object.
      * 
      * @param key
      *            used to identify the object
-     * @return the object for the key
+     * @return the value object for the key
      */
-    V recoverObject(K key);
+    V toValue(K key);
 }

Added: 
tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/TapestryMessages.java
URL: 
http://svn.apache.org/viewvc/tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/TapestryMessages.java?view=auto&rev=496918
==============================================================================
--- 
tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/TapestryMessages.java
 (added)
+++ 
tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/TapestryMessages.java
 Tue Jan 16 18:50:12 2007
@@ -0,0 +1,22 @@
+package org.apache.tapestry;
+
+import java.util.Collection;
+
+import org.apache.tapestry.ioc.Messages;
+import org.apache.tapestry.ioc.internal.util.InternalUtils;
+import org.apache.tapestry.ioc.internal.util.MessagesImpl;
+
+final class TapestryMessages
+{
+    private static final Messages MESSAGES = 
MessagesImpl.forClass(TapestryMessages.class);
+
+    static String duplicateKey(Object key, Object newValue, Object 
existingValue)
+    {
+        return MESSAGES.format("duplicate-key", key, newValue, existingValue);
+    }
+
+    static <V> String missingValue(V value, Collection<V> values)
+    {
+        return MESSAGES.format("missing-value", value, 
InternalUtils.joinSorted(values));
+    }
+}

Modified: 
tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/corelib/components/Form.java
URL: 
http://svn.apache.org/viewvc/tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/corelib/components/Form.java?view=diff&rev=496918&r1=496917&r2=496918
==============================================================================
--- 
tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/corelib/components/Form.java
 (original)
+++ 
tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/corelib/components/Form.java
 Tue Jan 16 18:50:12 2007
@@ -300,7 +300,7 @@
 
             _resources.triggerEvent(PREPARE_EVENT, context, handler);
 
-            if (holder.get() != null)
+            if (holder.hasValue())
                 return holder.get();
 
             // TODO: Ajax stuff will eventually mean there are multiple values 
for this parameter
@@ -344,7 +344,7 @@
 
             _resources.triggerEvent(VALIDATE, context, handler);
 
-            if (holder.get() != null)
+            if (holder.hasValue())
                 return holder.get();
 
             _formSupport.executeDeferred();
@@ -352,11 +352,18 @@
             // Let the listeners know about overall success or failure. Most 
listeners fall into
             // one of those two camps.
 
+            // If the tracker has no errors, then clear it of any input values
+            // as well, so that the next page render will be "clean" and show
+            // true persistent data, not value from the previous form 
submission.
+            
+            if (!_tracker.getHasErrors())
+                _tracker.clear();
+
             _resources.triggerEvent(tracker.getHasErrors() ? FAILURE : 
SUCCESS, context, handler);
 
             // Lastly, tell anyone whose interested that the form is 
completely submitted.
 
-            if (holder.get() != null)
+            if (holder.hasValue())
                 return holder.get();
 
             _resources.triggerEvent(SUBMIT, context, handler);

Modified: 
tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/corelib/components/Loop.java
URL: 
http://svn.apache.org/viewvc/tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/corelib/components/Loop.java?view=diff&rev=496918&r1=496917&r2=496918
==============================================================================
--- 
tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/corelib/components/Loop.java
 (original)
+++ 
tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/corelib/components/Loop.java
 Tue Jan 16 18:50:12 2007
@@ -14,8 +14,11 @@
 
 package org.apache.tapestry.corelib.components;
 
+import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newList;
+
 import java.io.Serializable;
 import java.util.Iterator;
+import java.util.List;
 
 import org.apache.tapestry.ComponentAction;
 import org.apache.tapestry.ComponentResources;
@@ -33,48 +36,129 @@
 
 /**
  * Basic looping class; loops over a number of items (provided by its source 
parameter), rendering
- * its body for each one.
+ * its body for each one. It turns out that gettting the component to 
<em>not</em> store its state
+ * in the Form is very tricky and, in fact, a series of commands for starting 
and ending heartbeats,
+ * and advancing through the iterator, are still stored. For a non-volatile 
Loop inside the form,
+ * the Loop stores a series of commands that start and end heartbeats and 
store state (either as
+ * full objects when there is not encoder, or as client-side objects when 
there is an encoder).
  */
 @ComponentClass
 public class Loop
 {
+    /** Setup command for non-volatile rendering. */
+    private static final ComponentAction<Loop> RESET_INDEX = new 
ComponentAction<Loop>()
+    {
+        private static final long serialVersionUID = 6477493424977597345L;
+
+        public void execute(Loop component)
+        {
+            component.resetIndex();
+        }
+    };
+
+    /**
+     * Setup command for volatile rendering. Volatile rendering relies on 
re-acquiring the Iterator
+     * and working our way through it (and hoping for the best!).
+     */
+    private static final ComponentAction<Loop> SETUP_FOR_VOLATILE = new 
ComponentAction<Loop>()
+    {
+        private static final long serialVersionUID = -977168791667037377L;
+
+        public void execute(Loop component)
+        {
+            component.setupForVolatile();
+        };
+    };
+
+    /**
+     * Advances to next value in a volatile way. So, the <em>number</em> of 
steps is intrinsically
+     * stored in the Form (as the number of ADVANCE_VOLATILE commands), but 
the actual values are
+     * expressly stored only on the server.
+     */
+    private static final ComponentAction<Loop> ADVANCE_VOLATILE = new 
ComponentAction<Loop>()
+    {
+        private static final long serialVersionUID = -4600281573714776832L;
+
+        public void execute(Loop component)
+        {
+            component.advanceVolatile();
+        }
+    };
+
+    /**
+     * Used in both volatile and non-volatile mode to end the current 
heartbeat (started by either
+     * ADVANCE_VOLATILE or one of the RestoreState commands). Also increments 
the index.
+     */
+    private static final ComponentAction<Loop> END_HEARTBEAT = new 
ComponentAction<Loop>()
+    {
+        private static final long serialVersionUID = -977168791667037377L;
+
+        public void execute(Loop component)
+        {
+            component.endHeartbeat();
+        };
+    };
+
+    /**
+     * Restores a state value (this is the case when there is no encoder and 
the complete value is
+     * stored).
+     */
     static class RestoreState implements ComponentAction<Loop>
     {
         private static final long serialVersionUID = -3926831611368720764L;
 
-        private final int _index;
-
         private final Object _storedValue;
 
-        public RestoreState(final int index, final Object storedValue)
+        public RestoreState(final Object storedValue)
         {
-            _index = index;
             _storedValue = storedValue;
         }
 
         public void execute(Loop component)
         {
-            component.restoreState(_index, _storedValue);
+            component.restoreState(_storedValue);
         }
     };
 
-    static class RestoreConvertedState implements ComponentAction<Loop>
+    /**
+     * Restores the value using a stored primary key via
+     * [EMAIL PROTECTED] PrimaryKeyEncoder#toValue(Serializable)}.
+     */
+    static class RestoreStateViaEncodedPrimaryKey implements 
ComponentAction<Loop>
     {
         private static final long serialVersionUID = -2422790241589517336L;
 
-        private final int _index;
-
         private final Serializable _primaryKey;
 
-        public RestoreConvertedState(final int index, final Serializable 
primaryKey)
+        public RestoreStateViaEncodedPrimaryKey(final Serializable primaryKey)
         {
-            _index = index;
             _primaryKey = primaryKey;
         }
 
         public void execute(Loop component)
         {
-            component.restoreConvertedState(_index, _primaryKey);
+            component.restoreStateViaEncodedPrimaryKey(_primaryKey);
+        }
+    };
+
+    /**
+     * Stores a list of keys to be passed to [EMAIL PROTECTED] 
PrimaryKeyEncoder#prepareForKeys(List)}.
+     */
+    static class PrepareForKeys implements ComponentAction<Loop>
+    {
+        private static final long serialVersionUID = -6515255627142956828L;
+
+        /** The variable is final, the contents are mutable while the Loop 
renders. */
+        private final List<Serializable> _keys;
+
+        public PrepareForKeys(final List<Serializable> keys)
+        {
+            _keys = keys;
+        }
+
+        public void execute(Loop component)
+        {
+            component.prepareForKeys(_keys);
         }
     };
 
@@ -126,6 +210,8 @@
     @Inject
     private ComponentResources _resources;
 
+    private List<Serializable> _keyList;
+
     @SetupRender
     boolean setup()
     {
@@ -136,11 +222,50 @@
 
         _iterator = _source.iterator();
 
-        _storeRenderStateInForm = _formSupport != null & !_volatile;
+        _storeRenderStateInForm = _formSupport != null && !_volatile;
 
         // Only render the body if there is something to iterate over
 
-        return _iterator.hasNext();
+        boolean result = _iterator.hasNext();
+
+        if (_formSupport != null && result)
+        {
+
+            _formSupport.store(this, _volatile ? SETUP_FOR_VOLATILE : 
RESET_INDEX);
+
+            if (_encoder != null)
+            {
+                _keyList = newList();
+
+                // We'll keep updating the _keyList while the Loop renders, 
the values will "lock
+                // down" when the Form serializes all the data.
+
+                _formSupport.store(this, new PrepareForKeys(_keyList));
+            }
+        }
+
+        return result;
+    }
+
+    private void prepareForKeys(List<Serializable> keys)
+    {
+        // Again, the encoder existed when we rendered, we better have another 
available
+        // when the enclosing Form is submitted.
+
+        _encoder.prepareForKeys(keys);
+    }
+
+    private void setupForVolatile()
+    {
+        _index = 0;
+        _iterator = _source.iterator();
+    }
+
+    private void advanceVolatile()
+    {
+        _value = _iterator.next();
+
+        startHeartbeat();
     }
 
     /** Begins a new heartbeat. */
@@ -153,17 +278,24 @@
         {
             if (_encoder == null)
             {
-                _formSupport.store(this, new RestoreState(_index, _value));
+                _formSupport.store(this, new RestoreState(_value));
             }
             else
             {
-                Serializable primaryKey = _encoder.extractKey(_value);
-                _formSupport.store(this, new RestoreConvertedState(_index, 
primaryKey));
+                Serializable primaryKey = _encoder.toKey(_value);
+                _formSupport.store(this, new 
RestoreStateViaEncodedPrimaryKey(primaryKey));
             }
         }
 
-        _heartbeat.begin();
+        if (_formSupport != null && _volatile)
+            _formSupport.store(this, ADVANCE_VOLATILE);
+
+        startHeartbeat();
+    }
 
+    private void startHeartbeat()
+    {
+        _heartbeat.begin();
     }
 
     void beforeRenderBody(MarkupWriter writer)
@@ -185,29 +317,43 @@
     @AfterRender
     boolean after()
     {
+        endHeartbeat();
+
+        if (_formSupport != null)
+            _formSupport.store(this, END_HEARTBEAT);
+
+        return _iterator.hasNext();
+    }
+
+    private void endHeartbeat()
+    {
         _heartbeat.end();
 
         _index++;
+    }
 
-        return _iterator.hasNext();
+    private void resetIndex()
+    {
+        _index = 0;
     }
 
     /** Restores state previously stored by the Loop into a Form. */
-    private void restoreState(int index, Object storedValue)
+    private void restoreState(Object storedValue)
     {
-        _index = index;
         _value = storedValue;
+
+        startHeartbeat();
     }
 
-    /** Restores state previosly converted by the Loop and stored into the 
Form. */
-    private void restoreConvertedState(int index, Serializable primaryKey)
+    /** Restores state previously encoded by the Loop and stored into the 
Form. */
+    private void restoreStateViaEncodedPrimaryKey(Serializable primaryKey)
     {
-        // We assume that if a converter is available when we rendered, that 
one will be available
+        // We assume that if a encoder is available when we rendered, that one 
will be available
         // when the form is submitted. TODO: Check for this.
 
-        Object restoredValue = _encoder.recoverObject(primaryKey);
+        Object restoredValue = _encoder.toValue(primaryKey);
 
-        restoreState(index, restoredValue);
+        restoreState(restoredValue);
     }
 
     // For testing:

Modified: 
tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/internal/util/Holder.java
URL: 
http://svn.apache.org/viewvc/tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/internal/util/Holder.java?view=diff&rev=496918&r1=496917&r2=496918
==============================================================================
--- 
tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/internal/util/Holder.java
 (original)
+++ 
tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/internal/util/Holder.java
 Tue Jan 16 18:50:12 2007
@@ -34,6 +34,11 @@
         return _held;
     }
 
+    public boolean hasValue()
+    {
+        return _held != null;
+    }
+
     public static <T> Holder<T> create()
     {
         return new Holder<T>();

Added: 
tapestry/tapestry5/tapestry-core/trunk/src/main/resources/org/apache/tapestry/TapestryStrings.properties
URL: 
http://svn.apache.org/viewvc/tapestry/tapestry5/tapestry-core/trunk/src/main/resources/org/apache/tapestry/TapestryStrings.properties?view=auto&rev=496918
==============================================================================
--- 
tapestry/tapestry5/tapestry-core/trunk/src/main/resources/org/apache/tapestry/TapestryStrings.properties
 (added)
+++ 
tapestry/tapestry5/tapestry-core/trunk/src/main/resources/org/apache/tapestry/TapestryStrings.properties
 Tue Jan 16 18:50:12 2007
@@ -0,0 +1,2 @@
+duplicate-key=Key %s may not be added with value %s, as an existing value, %s, 
is already present.
+missing-value=Key for value %s not found. Available values: %s
\ No newline at end of file

Copied: 
tapestry/tapestry5/tapestry-core/trunk/src/test/app1/WEB-INF/ToDoListVolatile.html
 (from r496578, 
tapestry/tapestry5/tapestry-core/trunk/src/test/app1/WEB-INF/ToDoList.html)
URL: 
http://svn.apache.org/viewvc/tapestry/tapestry5/tapestry-core/trunk/src/test/app1/WEB-INF/ToDoListVolatile.html?view=diff&rev=496918&p1=tapestry/tapestry5/tapestry-core/trunk/src/test/app1/WEB-INF/ToDoList.html&r1=496578&p2=tapestry/tapestry5/tapestry-core/trunk/src/test/app1/WEB-INF/ToDoListVolatile.html&r2=496918
==============================================================================
--- tapestry/tapestry5/tapestry-core/trunk/src/test/app1/WEB-INF/ToDoList.html 
(original)
+++ 
tapestry/tapestry5/tapestry-core/trunk/src/test/app1/WEB-INF/ToDoListVolatile.html
 Tue Jan 16 18:50:12 2007
@@ -11,7 +11,7 @@
                 <th> Title </th>
                 <th> Reorder </th>
             </tr>
-            <tr t:type="Loop" source="database.findAll()" value="item">
+            <tr t:type="Loop" source="items" value="item" volatile="true">
                 <td>
                     <input t:type="TextField" t:id="title" value="item.title" 
size="30"
                         validate="validate:required"/>
@@ -21,10 +21,16 @@
             <tr>
                 <td colspan="2">
                    <input type="submit" value="Update ToDos"/>
-                   <input t:type="Submit" value="'Add new ToDo'"/>
+                   <input t:type="Submit" t:id="addNew" value="'Add new 
ToDo'"/>
                                    </td>
             </tr>
  
         </table>
     </form>
+    
+
+    <p>
+        <a t:type="ActionLink" t:id="reset">reset the database</a>
+    </p>
+
 </html>

Modified: tapestry/tapestry5/tapestry-core/trunk/src/test/app1/index.html
URL: 
http://svn.apache.org/viewvc/tapestry/tapestry5/tapestry-core/trunk/src/test/app1/index.html?view=diff&rev=496918&r1=496917&r2=496918
==============================================================================
--- tapestry/tapestry5/tapestry-core/trunk/src/test/app1/index.html (original)
+++ tapestry/tapestry5/tapestry-core/trunk/src/test/app1/index.html Tue Jan 16 
18:50:12 2007
@@ -48,12 +48,13 @@
                         <li>
                             <a 
href="RenderPhaseOrder.html">RenderPhaseOrder</a> -- Order of
                             operations when invoking render phase methods </li>
+                        <li><a href="SimpleForm.html">SimpleForm</a> -- first 
pass at writing Form
+                            and TextField components </li>
+
                     </ul>
                 </td>
                 <td>
                     <ul>
-                        <li><a href="SimpleForm.html">SimpleForm</a> -- first 
pass at writing Form
-                            and TextField components </li>
                         <li>
                             <a href="NumberSelect.html">NumberSelect</a> -- 
passivate/activate page
                             context demo </li>
@@ -80,14 +81,17 @@
                             <a 
href="PasswordFieldDemo.html">PasswordFieldDemo</a> -- test for the
                             PasswordField component </li>
                         <li>
-                            <a 
href="RenderComponentDemo.html">RenderComponentDemo</a> -- components that 
"nominate" other components to render
-                        </li>
+                            <a 
href="RenderComponentDemo.html">RenderComponentDemo</a> -- components
+                            that "nominate" other components to render </li>
+                        <li>
+                            <a href="BlockDemo.html">BlockDemo</a> -- use of 
blocks to control
+                            rendering </li>
                         <li>
-                            <a href="BlockDemo.html">BlockDemo</a> -- use of 
blocks to control rendering
-                        </li>
+                            <a href="ToDoListVolatile.html">ToDo List 
(Volatile)</a> -- Loops and
+                            Submit inside Form, volatile mode </li>
                         <li>
-                            <a href="ToDoList.html">ToDo list</a>  -- Loops 
and Submit inside Form
-                        </li>
+                            <a href="ToDoList.html">ToDo List</a> -- Loops and 
Submit inside Form
+                            using a primary key encoder </li>
                     </ul>
                 </td>
             </tr>

Modified: 
tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/DefaultPrimaryKeyEncoderTest.java
URL: 
http://svn.apache.org/viewvc/tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/DefaultPrimaryKeyEncoderTest.java?view=diff&rev=496918&r1=496917&r2=496918
==============================================================================
--- 
tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/DefaultPrimaryKeyEncoderTest.java
 (original)
+++ 
tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/DefaultPrimaryKeyEncoderTest.java
 Tue Jan 16 18:50:12 2007
@@ -47,6 +47,42 @@
     }
 
     @Test
+    public void keys_must_be_unique()
+    {
+        IntStringEncoder encoder = newEncoder();
+
+        try
+        {
+            encoder.add(FRED_ID, "NewFred");
+            unreachable();
+        }
+        catch (IllegalArgumentException ex)
+        {
+            assertEquals(
+                    ex.getMessage(),
+                    "Key 1 may not be added with value NewFred, as an existing 
value, FRED, is already present.");
+        }
+    }
+
+    @Test
+    public void extract_key_for_missing_value()
+    {
+        IntStringEncoder encoder = newEncoder();
+
+        try
+        {
+            encoder.toKey("BETTY");
+            unreachable();
+        }
+        catch (IllegalArgumentException ex)
+        {
+            assertEquals(
+                    ex.getMessage(),
+                    "Key for value BETTY not found. Available values: BARNEY, 
FRED, WILMA");
+        }
+    }
+
+    @Test
     public void value_ordered_maintained()
     {
         IntStringEncoder encoder = newEncoder();
@@ -59,8 +95,8 @@
     {
         IntStringEncoder encoder = newEncoder();
 
-        assertEquals(encoder.extractKey(FRED).intValue(), FRED_ID);
-        assertEquals(encoder.extractKey(BARNEY).intValue(), BARNEY_ID);
+        assertEquals(encoder.toKey(FRED).intValue(), FRED_ID);
+        assertEquals(encoder.toKey(BARNEY).intValue(), BARNEY_ID);
     }
 
     @Test
@@ -68,8 +104,8 @@
     {
         IntStringEncoder encoder = newEncoder();
 
-        assertEquals(encoder.recoverObject(FRED_ID), FRED);
-        assertEquals(encoder.recoverObject(BARNEY_ID), BARNEY);
+        assertEquals(encoder.toValue(FRED_ID), FRED);
+        assertEquals(encoder.toValue(BARNEY_ID), BARNEY);
     }
 
     @Test
@@ -77,7 +113,7 @@
     {
         IntStringEncoder encoder = newEncoder();
 
-        assertNull(encoder.recoverObject(99), null);
+        assertNull(encoder.toValue(99), null);
     }
 
     @Test
@@ -97,7 +133,50 @@
             }
         };
 
-        assertSame(encoder.recoverObject(bettyId), betty);
+        assertSame(encoder.toValue(bettyId), betty);
+    }
+
+    @Test
+    public void set_delete_false_when_nothing_yet_deleted()
+    {
+        IntStringEncoder encoder = newEncoder();
+
+        assertSame(FRED, encoder.toValue(FRED_ID));
+
+        encoder.setDeleted(false);
+
+        assertEquals(encoder.getValues(), encoder.getAllValues());
+    }
+
+    @Test
+    public void difference_between_get_values_and_get_all_values()
+    {
+        IntStringEncoder encoder = newEncoder();
+
+        assertSame(FRED, encoder.toValue(FRED_ID));
+
+        assertFalse(encoder.isDeleted());
+
+        encoder.setDeleted(true);
+
+        assertTrue(encoder.isDeleted());
+
+        assertEquals(encoder.getValues(), Arrays.asList(BARNEY, WILMA));
+
+        assertEquals(encoder.getAllValues(), Arrays.asList(BARNEY, FRED, 
WILMA));
+    }
+
+    @Test
+    public void undelete_a_value()
+    {
+        IntStringEncoder encoder = newEncoder();
+
+        assertSame(FRED, encoder.toValue(FRED_ID));
+
+        encoder.setDeleted(true);
+        encoder.setDeleted(false);
+
+        assertEquals(encoder.getValues(), encoder.getAllValues());
     }
 
     private IntStringEncoder newEncoder()

Modified: 
tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/integration/IntegrationTests.java
URL: 
http://svn.apache.org/viewvc/tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/integration/IntegrationTests.java?view=diff&rev=496918&r1=496917&r2=496918
==============================================================================
--- 
tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/integration/IntegrationTests.java
 (original)
+++ 
tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/integration/IntegrationTests.java
 Tue Jan 16 18:50:12 2007
@@ -506,6 +506,51 @@
         assertTextPresent("Should now show up:");
     }
 
+    @Test
+    public void volatile_loop_inside_a_form()
+    {
+        test_loop_inside_form("ToDo List (Volatile)");        
+    }
+
+    @Test
+    public void encoded_loop_inside_a_form()
+    {
+        test_loop_inside_form("ToDo List");
+    }
+
+    private void test_loop_inside_form(String linkLabel)
+    {
+        _selenium.open(BASE_URL);
+
+        clickAndWait("link=" + linkLabel);
+        clickAndWait("reset");
+
+        assertValue("title", "Ditch Struts");
+        assertValue("title_0", "Eliminate JSF");
+        assertValue("title_1", "Conquer Rife");
+
+        _selenium.type("title", "Ditch Struts - today");
+        _selenium.type("title_0", "Eliminate JSF - immediately");
+        _selenium.type("title_1", "Conquer Rife - post haste");
+
+        clickAndWait("//[EMAIL PROTECTED]'Update ToDos']");
+
+        assertValue("title", "Ditch Struts - today");
+        assertValue("title_0", "Eliminate JSF - immediately");
+        assertValue("title_1", "Conquer Rife - post haste");
+
+        clickAndWait("addNew");
+
+        _selenium.type("title_2", "Conquer World");
+
+        clickAndWait("//[EMAIL PROTECTED]'Update ToDos']");
+
+        assertValue("title", "Ditch Struts - today");
+        assertValue("title_0", "Eliminate JSF - immediately");
+        assertValue("title_1", "Conquer Rife - post haste");
+        assertValue("title_2", "Conquer World");
+    }
+
     /**
      * Tests the ability to inject a Block, and the ability to use the block 
to control rendering.
      */

Copied: 
tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/integration/app1/pages/ToDoListVolatile.java
 (from r496578, 
tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/integration/app1/pages/ToDoList.java)
URL: 
http://svn.apache.org/viewvc/tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/integration/app1/pages/ToDoListVolatile.java?view=diff&rev=496918&p1=tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/integration/app1/pages/ToDoList.java&r1=496578&p2=tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/integration/app1/pages/ToDoListVolatile.java&r2=496918
==============================================================================
--- 
tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/integration/app1/pages/ToDoList.java
 (original)
+++ 
tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/integration/app1/pages/ToDoListVolatile.java
 Tue Jan 16 18:50:12 2007
@@ -14,6 +14,8 @@
 
 package org.apache.tapestry.integration.app1.pages;
 
+import java.util.List;
+
 import org.apache.tapestry.annotations.Component;
 import org.apache.tapestry.annotations.ComponentClass;
 import org.apache.tapestry.annotations.Inject;
@@ -22,16 +24,23 @@
 import org.apache.tapestry.integration.app1.services.ToDoDatabase;
 
 @ComponentClass
-public class ToDoList
+public class ToDoListVolatile
 {
     @Inject
     private ToDoDatabase _database;
 
     private ToDoItem _item;
 
+    private List<ToDoItem> _items;
+
     @Component
     private Form _form;
 
+    public List<ToDoItem> getItems()
+    {
+        return _items;
+    }
+
     public ToDoItem getItem()
     {
         return _item;
@@ -45,5 +54,38 @@
     public ToDoDatabase getDatabase()
     {
         return _database;
+    }
+
+    void onPrepare()
+    {
+        _items = _database.findAll();
+    }
+
+    void onSuccess()
+    {
+        int order = 0;
+
+        for (ToDoItem item : _items)
+        {
+            item.setOrder(order++);
+            _database.update(item);
+        }
+    }
+
+    void onSelectedFromAddNew()
+    {
+        if (_form.isValid())
+        {
+            ToDoItem item = new ToDoItem();
+            item.setTitle("<New To Do>");
+            item.setOrder(_items.size());
+
+            _database.add(item);
+        }
+    }
+
+    void onActionFromReset()
+    {
+        _database.reset();
     }
 }

Modified: 
tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/integration/app1/services/ToDoDatabase.java
URL: 
http://svn.apache.org/viewvc/tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/integration/app1/services/ToDoDatabase.java?view=diff&rev=496918&r1=496917&r2=496918
==============================================================================
--- 
tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/integration/app1/services/ToDoDatabase.java
 (original)
+++ 
tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/integration/app1/services/ToDoDatabase.java
 Tue Jan 16 18:50:12 2007
@@ -34,4 +34,7 @@
      *             if the item does not exist
      */
     void update(ToDoItem item);
+    
+    /** Resets the database, clearing out all data, re-adding base data. */
+    void reset();
 }

Modified: 
tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/integration/app1/services/ToDoDatabaseImpl.java
URL: 
http://svn.apache.org/viewvc/tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/integration/app1/services/ToDoDatabaseImpl.java?view=diff&rev=496918&r1=496917&r2=496918
==============================================================================
--- 
tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/integration/app1/services/ToDoDatabaseImpl.java
 (original)
+++ 
tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/integration/app1/services/ToDoDatabaseImpl.java
 Tue Jan 16 18:50:12 2007
@@ -39,6 +39,13 @@
     {
         // A couple of items to get us started:
 
+        reset();
+    }
+
+    public void reset()
+    {
+        _items.clear();
+        
         add("Ditch Struts", 1);
         add("Eliminate JSF", 2);
         add("Conquer Rife", 3);


Reply via email to