You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@velocity.apache.org by cb...@apache.org on 2012/03/09 17:23:32 UTC

svn commit: r1298906 [5/14] - in /velocity/sandbox/velosurf: ./ docs/ examples/ examples/ant-vpp/ examples/ant-vpp/lib/ examples/auth-l10n/ examples/auth-l10n/WEB-INF/ examples/auth-l10n/WEB-INF/lib/ examples/auth-l10n/WEB-INF/src/ examples/auth-l10n/e...

Added: velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/AttributeReference.java
URL: http://svn.apache.org/viewvc/velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/AttributeReference.java?rev=1298906&view=auto
==============================================================================
--- velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/AttributeReference.java (added)
+++ velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/AttributeReference.java Fri Mar  9 16:23:25 2012
@@ -0,0 +1,249 @@
+/*
+ * Copyright 2003 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.velocity.velosurf.context;
+
+import java.sql.SQLException;
+import java.util.*;
+import org.apache.velocity.velosurf.model.Attribute;
+import org.apache.velocity.velosurf.util.Logger;
+import org.apache.velocity.velosurf.util.StringLists;
+
+/**
+ * Context wrapper for attributes.
+ *
+ *  @author <a href=mailto:claude.brisson@gmail.com>Claude Brisson</a>
+ */
+public class AttributeReference implements Iterable
+{
+    /**
+     * Constructor.
+     *
+     * @param params the parameters map this attribute reference applies to
+     * @param attribute the wrapped attribute
+     */
+    public AttributeReference(Map<String, Object> params, Attribute attribute)
+    {
+        this.params = params;
+        this.attribute = attribute;
+    }
+
+    /**
+     * <p>Refines this attribute's reference result. The provided criterium will be added to the 'where' clause (or a 'where' clause will be added).</p>
+     * <p>This method can be called several times, thus allowing a field-by-field handling of an html search form.</p>
+     * <p>All criteria will be merged with the sql 'and' operator (if there is an initial where clause, it is wrapped into parenthesis).</p>
+     * <p>Example : suppose we have defined the attribute 'person.children' as " *person(person_id):select * from person where parent_id=?". Then, if we issue the following calls from inside the template :</p>
+     * <blockquote>
+     * $bob.children.refine("age>18")
+     * <br>
+     * $bob.children.refine("gender='F'")
+     * </blockquote>
+     * <p>the resulting query that will be issed is :</p>
+     * <p><code>select * from person where (parent_id=?) and (age>18) and (gender='F')</code></p>
+     *
+     * @param criterium a valid sql condition
+     */
+    public void refine(String criterium)
+    {
+        if(refineCriteria == null)
+        {
+            refineCriteria = new ArrayList();
+        }
+        refineCriteria.add(criterium);
+    }
+
+    /**
+     * Clears any refinement made on this attribute.
+     * <p>
+     */
+    public void clearRefinement()
+    {
+        refineCriteria = null;
+    }
+
+    /**
+     * Called by the #foreach directive.
+     * <p>
+     * Returns a RowIterator on all possible instances of this entity, possibly previously refined and ordered.</p>
+     *
+     * @return a RowIterator on instances of this entity, or null if an error
+     *     occured.
+     */
+    public Iterator iterator()
+    {
+        try
+        {
+            RowIterator iterator = attribute.query(params, refineCriteria, order);
+
+            return iterator;
+        }
+        catch(SQLException sqle)
+        {
+            Logger.log(sqle);
+            attribute.getDB().setError(sqle.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * Gets all the rows in a list of maps.
+     *
+     * @return a list of all the rows
+     */
+    public List getRows()
+    {
+        try
+        {
+            RowIterator iterator = attribute.query(params, refineCriteria, order);
+
+            return iterator.getRows();
+        }
+        catch(SQLException sqle)
+        {
+            Logger.log(sqle);
+            attribute.getDB().setError(sqle.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * Gets a list of scalars
+     *  @return a list of scalars
+     */
+    public List getScalars()
+    {
+        try
+        {
+            RowIterator iterator = attribute.query(params, refineCriteria, order);
+
+            return iterator.getScalars();
+        }
+        catch(SQLException sqle)
+        {
+            Logger.log(sqle);
+            attribute.getDB().setError(sqle.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * Get all the rows in a map firstcol->secondcol.
+     * FIXME: better in Attribute than in AttributeReference
+     * @return a list of all the rows
+     */
+    public Map getMap()
+    {
+        Map result = null;
+
+        try
+        {
+            RowIterator iterator = attribute.query(params, refineCriteria, order);
+            List<String> keys = iterator.keyList();
+
+            if(keys.size() < 2)
+            {
+                Logger.error(".map needs at least two result columns");
+                return null;
+            }
+            if(keys.size() > 2)
+            {
+                Logger.warn(
+                    "attribute.map needs only two result columns, only the first two will be taken into account");
+            }
+            result = new TreeMap();
+            while(iterator.hasNext())
+            {
+                Instance i = iterator.next();
+
+                result.put(i.get(keys.get(0)), i.get(keys.get(1)));
+            }
+        }
+        catch(SQLException sqle)
+        {
+            Logger.log(sqle);
+            attribute.getDB().setError(sqle.getMessage());
+            return null;
+        }
+        return result;
+    }
+
+    /**
+     * Get all the rows in a map firstcol->instance
+     * FIXME: better in Attribute than in AttributeReference
+     * @return a list of all the rows
+     */
+    public Map getInstanceMap()
+    {
+        Map result = null;
+
+        try
+        {
+            RowIterator iterator = attribute.query(params, refineCriteria, order);
+            List<String> keys = iterator.keyList();
+
+            result = new TreeMap();
+            while(iterator.hasNext())
+            {
+                Instance i = iterator.next();
+
+                result.put(i.get(keys.get(0)), i);
+            }
+        }
+        catch(SQLException sqle)
+        {
+            Logger.log(sqle);
+            attribute.getDB().setError(sqle.getMessage());
+            return null;
+        }
+        return result;
+    }
+
+    /**
+     * <p>Specify an 'order by' clause for this attribute reference result.</p>
+     * <p>If an 'order by' clause is already present in the original query, the new one is appended (but successive calls to this method overwrite previous ones)</p>
+     * <p> postfix " DESC " to a column for descending order.</p>
+     * <p>Pass it null or an empty string to clear any ordering.</p>
+     *
+     * @param order valid sql column names (separated by commas) indicating the
+     *      desired order
+     */
+    public void setOrder(String order)
+    {
+        this.order = order;
+    }
+
+    /**
+     * Specified refining criteria defined on this attribute reference.
+     */
+    private List<String> refineCriteria = null;
+
+    /**
+     * Specified 'order by' clause specified for this attribute reference.
+     */
+    private String order = null;
+
+    /**
+     * The map this attribute reference applies to.
+     */
+    private Map<String, Object> params = null;
+
+    /**
+     * The wrapped attribute.
+     */
+    private Attribute attribute = null;
+}

Added: velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/DBReference.java
URL: http://svn.apache.org/viewvc/velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/DBReference.java?rev=1298906&view=auto
==============================================================================
--- velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/DBReference.java (added)
+++ velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/DBReference.java Fri Mar  9 16:23:25 2012
@@ -0,0 +1,354 @@
+/*
+ * Copyright 2003 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.velocity.velosurf.context;
+
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import org.apache.velocity.velosurf.model.Action;
+import org.apache.velocity.velosurf.model.Attribute;
+import org.apache.velocity.velosurf.model.Entity;
+import org.apache.velocity.velosurf.sql.Database;
+import org.apache.velocity.velosurf.util.Logger;
+import org.apache.velocity.velosurf.util.UserContext;
+
+/**
+ * <p>A context wrapper for the main database connection object.</p>
+ * <p>The "<code>$db</code>" context variable is assigned a new instance of this class at each velocity parsing.</p>
+ *
+ *  @author <a href=mailto:claude.brisson@gmail.com>Claude Brisson</a>
+ */
+
+// FIXME : right now, extends HashMap bkoz velocity wants a HashMap for setters
+public class DBReference extends HashMap<String, Object> implements HasParametrizedGetter
+{
+    /**
+     * Default constructor for use by derived classes.
+     */
+    protected DBReference(){}
+
+    /**
+     * Constructs a new database reference.
+     *
+     * @param db the wrapped database connection
+     */
+    public DBReference(Database db)
+    {
+        init(db);
+    }
+
+    /**
+     * Protected initialization method.
+     *
+     * @param db database connection
+     */
+    protected void init(Database db)
+    {
+        this.db = db;
+        cache = new HashMap<String, Object>();
+        externalParams = new HashMap<String, Object>();
+    }
+
+    /**
+     * <p>Generic getter, used to access entities or root attributes by their name.</p>
+     * <p>For attributes, the return value depends upon the type of the attribute :</p>
+     * <ul>
+     * <li>if the attribute is multivalued, returns an AttributeReference.
+     * <li>if the attribute is singlevalued, returns an Instance.
+     * <li>if the attribute is scalar, returns a String.
+     * </ul>
+     * <p>If no attribute is found, entities are searched, then external parameters.</p>
+     * @param key the name of the desired entity or root attribute.
+     * @return an entity, an attribute reference, an instance, a string or null
+     *     if not found. See  See above.
+     */
+    public Object get(Object key)
+    {
+        String property = db.adaptCase((String)key);
+
+        try
+        {
+            Object result = null;
+
+            /* 1) try the cache */
+            result = cache.get(property);
+            if(result != null)
+            {
+                return result;
+            }
+
+            /* 2) try to get a root attribute */
+            Attribute attribute = db.getAttribute(property);
+
+            if(attribute != null)
+            {
+                switch(attribute.getType())
+                {
+                    case Attribute.ROWSET :
+                        result = new AttributeReference(this, attribute);
+                        break;
+                    case Attribute.SCALAR :
+                        result = attribute.evaluate(this);
+                        break;
+                    case Attribute.ROW :
+                        result = attribute.fetch(this);
+                        break;
+                    default :
+                        Logger.error("Unknown attribute type encountered: db." + property);
+                        result = null;
+                }
+                cache.put(property, result);
+                return result;
+            }
+
+            /* 3) try to get a root action */
+            Action action = db.getAction(property);
+
+            if(action != null)
+            {
+                return Integer.valueOf(action.perform(this));
+            }
+
+            /* 4) try to get an entity */
+            Entity entity = db.getEntity(property);
+
+            if(entity != null)
+            {
+                result = new EntityReference(entity);
+                cache.put(property, result);
+                return result;
+            }
+
+            /* 5) try to get an external param */
+            result = externalParams.get(property);
+            if(result != null)
+            {
+                return result;
+            }
+
+            /* 6) try with the user context */
+            result = db.getUserContext().get(property);
+            if(result != null)
+            {
+                return result;
+            }
+
+            /* Sincerely, I don't see... */
+            return null;
+        }
+        catch(SQLException sqle)
+        {
+            db.setError(sqle.getMessage());
+            Logger.log(sqle);
+            return null;
+        }
+    }
+
+    /**
+     * <p>Specific entity getter. </p>
+     * @param key the name of the desired entity or root attribute.
+     * @return an entity, an attribute reference, an instance, a string or null
+     */
+    public EntityReference getEntity(String key)
+    {
+        key = db.adaptCase(key);
+
+        Entity entity = db.getEntity(key);
+
+        if(entity != null)
+        {
+            EntityReference result = new EntityReference(entity);
+
+            cache.put(key, result);
+            return result;
+        }
+        return null;
+    }
+
+    /**
+     * <p>Specific getter for last error message in user context </p>
+     * @return last threaded error message if any or null
+     */
+    public String getError()
+    {
+        return(String)db.getUserContext().get("error");
+    }
+
+    /**
+     * Default method handler, called by Velocity when it did not find the specified method.
+     *
+     * @param key asked key
+     * @param params passed parameters
+     * @see HasParametrizedGetter
+     */
+    public Object getWithParams(String key, Map params)
+    {
+        Object result = null;
+
+        try
+        {
+            String property = db.adaptCase((String)key);
+
+            for(Map.Entry entry : (Set<Map.Entry>)params.entrySet())
+            {
+                externalParams.put(db.adaptCase((String)entry.getKey()), entry.getValue());
+            }
+
+            /* 1) try to get a root attribute */
+            Attribute attribute = db.getAttribute(property);
+
+            if(attribute != null)
+            {
+                switch(attribute.getType())
+                {
+                    case Attribute.ROWSET :
+                        result = new AttributeReference(this, attribute);
+                        break;
+                    case Attribute.SCALAR :
+                        result = attribute.evaluate(this);
+                        break;
+                    case Attribute.ROW :
+                        result = attribute.fetch(this);
+                        break;
+                    default :
+                        Logger.error("Unknown attribute type encountered: db." + property);
+                }
+            }
+
+            /* 2) try to get a root action */
+            Action action = db.getAction(property);
+
+            if(action != null)
+            {
+                result = Integer.valueOf(action.perform(this));    // TODO actions should return a Long !!!!
+            }
+        }
+        catch(SQLException sqle)
+        {
+            db.setError(sqle.getMessage());
+            Logger.log(sqle);
+            result = null;
+        }
+        return result;
+    }
+
+    /**
+     * Generic setter used to set external params for children attributes.
+     *
+     * @param key name of the external parameter
+     * @param value value given to the external parameter
+     * @return the previous value, if any
+     */
+    public Object put(String key, Object value)
+    {
+        /*
+         * Clear actual values in the cache, because the value of attributes may change...
+         */
+        for(Iterator i = cache.keySet().iterator(); i.hasNext(); )
+        {
+            Object k = i.next();
+            Object v = cache.get(k);
+
+            if(!(v instanceof AttributeReference) &&!(v instanceof EntityReference))
+            {
+                i.remove();
+            }
+        }
+        return externalParams.put(db.adaptCase((String)key), value);
+    }
+
+    /**
+     * Get the schema name.
+     * @return the schema
+     */
+    public String getSchema()
+    {
+        return db.getSchema();
+    }
+
+    /**
+     * Obfuscate the given value.
+     * @param value value to obfuscate
+     *
+     * @return obfuscated value
+     */
+    public String obfuscate(Object value)
+    {
+        return db.obfuscate(value);
+    }
+
+    /**
+     * De-obfuscate the given value.
+     * @param value value to de-obfuscate
+     *
+     * @return obfuscated value
+     */
+    public String deobfuscate(Object value)
+    {
+        return db.deobfuscate(value);
+    }
+
+    /**
+     * User context getter
+     * @return current user context
+     */
+    public UserContext getUserContext()
+    {
+        return db.getUserContext();
+    }
+
+    /**
+     * User context setter
+     * @param userContext user context
+     */
+    public void setUserContext(UserContext userContext)
+    {
+        db.setUserContext(userContext);
+    }
+
+    /**
+     * String representation of this db reference.
+     * For now, returns a list of defined external parameters.
+     */
+    public String toString()
+    {
+        return "[DBRef with root attributes " + db.getRootEntity().getAttributes() + " and external params"
+               + externalParams + "]";
+    }
+
+    /**
+     * The wrapped database connection.
+     */
+    protected Database db = null;
+
+    /**
+     * A cache used by the generic getter. Its purpose is to avoid the creation of several
+     * attribute references for the same multivalued attribute.
+     */
+    private Map<String, Object> cache = null;
+
+    /**
+     * The map of external query parameters used by children attributes.
+     */
+    private Map<String, Object> externalParams = null;
+
+//  public void displayStats() { db.displayStats(); }
+}

Added: velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/EntityReference.java
URL: http://svn.apache.org/viewvc/velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/EntityReference.java?rev=1298906&view=auto
==============================================================================
--- velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/EntityReference.java (added)
+++ velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/EntityReference.java Fri Mar  9 16:23:25 2012
@@ -0,0 +1,482 @@
+/*
+ * Copyright 2003 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.velocity.velosurf.context;
+
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import org.apache.velocity.velosurf.model.Entity;
+import org.apache.velocity.velosurf.util.Logger;
+import org.apache.velocity.velosurf.util.UserContext;
+
+/**
+ * Context wrapper for an entity.
+ *
+ *  @author <a href=mailto:claude.brisson@gmail.com>Claude Brisson</a>
+ */
+public class EntityReference implements Iterable
+{
+    /**
+     * Builds a new EntityReference.
+     *
+     * @param entity the wrapped entity
+     */
+    public EntityReference(Entity entity)
+    {
+        this.entity = entity;
+    }
+
+    /**
+     * gets the name of the wrapped entity
+     */
+    public String getName()
+    {
+        return entity.getName();
+    }
+
+    /**
+     * Insert a new row in this entity's table.
+     *
+     * @param values col -> value map
+     * @return <code>true</code> if successfull, <code>false</code> if an error occurs (in which case $db.error can be checked).
+     */
+    public boolean insert(Map<String, Object> values)
+    {
+        try
+        {
+            return entity.insert(values);
+        }
+        catch(SQLException sqle)
+        {
+            Logger.log(sqle);
+            entity.getDB().setError(sqle.getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * Returns the ID of the last inserted row (obfuscated if needed).
+     *
+     * @return last insert ID
+     */
+    public Object getLastInsertID()
+    {
+        long id = entity.getDB().getUserContext().getLastInsertedID(entity);
+
+        return entity.filterID(id);
+    }
+
+    /**
+     * <p>Update a row in this entity's table.</p>
+     *
+     * <p>Velosurf will ensure all key columns are specified, to avoid an accidental massive update.</p>
+     *
+     * @param values col -> value map
+     * @return <code>true</code> if successfull, <code>false</code> if an error occurs (in which case $db.error can be checked).
+     */
+    public boolean update(Map<String, Object> values)
+    {
+        try
+        {
+            return entity.update(values);
+        }
+        catch(SQLException sqle)
+        {
+            Logger.log(sqle);
+            entity.getDB().setError(sqle.getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * <p>Upsert a row in this entity's table.</p>
+     *
+     * <p>Primary key must be a single column.</p>
+     *
+     * @param values col -> value map
+     * @return <code>true</code> if successfull, <code>false</code> if an error occurs (in which case $db.error can be checked).
+     */
+    public boolean upsert(Map<String, Object> values)
+    {
+        try
+        {
+            return entity.upsert(values);
+        }
+        catch(SQLException sqle)
+        {
+            Logger.log(sqle);
+            entity.getDB().setError(sqle.getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * <p>Detele a row from this entity's table.</p>
+     *
+     * <p>Velosurf will ensure all key columns are specified, to avoid an accidental massive update.</p>
+     *
+     * @param values col -> value map
+     * @return <code>true</code> if successfull, <code>false</code> if an error occurs (in which case $db.error can be checked).
+     */
+    public boolean delete(Map<String, Object> values)
+    {
+        try
+        {
+            return entity.delete(values);
+        }
+        catch(SQLException sqle)
+        {
+            Logger.log(sqle);
+            entity.getDB().setError(sqle.getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * <p>Detele a row from this entity's table, specifying the value of its unique key column.</p>
+     *
+     * @param keyValue key value
+     * @return <code>true</code> if successfull, <code>false</code> if an error occurs (in which case $db.error can be checked).
+     */
+    public boolean delete(String keyValue)
+    {
+        try
+        {
+            return entity.delete(keyValue);
+        }
+        catch(SQLException sqle)
+        {
+            Logger.log(sqle);
+            entity.getDB().setError(sqle.getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * <p>Detele a row from this entity's table, specifying the value of its unique key column.</p>
+     *
+     * <p>Velosurf will ensure all key columns are specified, to avoid an accidental massive update.</p>
+     *
+     * @param keyValue key value
+     * @return <code>true</code> if successfull, <code>false</code> if an error occurs (in which case $db.error can be checked).
+     */
+    public boolean delete(Number keyValue)
+    {
+        try
+        {
+            return entity.delete(keyValue);
+        }
+        catch(SQLException sqle)
+        {
+            Logger.log(sqle);
+            entity.getDB().setError(sqle.getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * Fetch an Instance of this entity, specifying the values of its key columns in their natural order.
+     *
+     * @param values values of the key columns
+     * @return an Instance, or null if an error occured (in which case
+     *     $db.error can be checked)
+     */
+    public Instance fetch(List<Object> values)
+    {
+        try
+        {
+            Instance instance = entity.fetch(values);
+
+            return instance;
+        }
+        catch(SQLException sqle)
+        {
+            Logger.log(sqle);
+            entity.getDB().setError(sqle.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * Fetch an Instance of this entity, specifying the values of its key columns in the map.
+     *
+     * @param values key=>value map
+     * @return an Instance, or null if an error occured (in which case
+     *     $db.error can be checked)
+     */
+    public Instance fetch(Map<String, Object> values)
+    {
+        try
+        {
+            Instance instance = entity.fetch(values);
+
+            return instance;
+        }
+        catch(SQLException sqle)
+        {
+            Logger.log(sqle);
+            entity.getDB().setError(sqle.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * Fetch an Instance of this entity, specifying the value of its unique key column as a string
+     *
+     * @param keyValue value of the key column
+     * @return an Instance, or null if an error occured (in which case
+     *     $db.error can be checked)
+     */
+    public Instance fetch(String keyValue)
+    {
+        try
+        {
+            Instance instance = entity.fetch(keyValue);
+
+            return instance;
+        }
+        catch(SQLException sqle)
+        {
+            Logger.log(sqle);
+            entity.getDB().setError(sqle.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * Fetch an Instance of this entity, specifying the value of its unique key column as an integer
+     *
+     * @param keyValue value of the key column
+     * @return an Instance, or null if an error occured (in which case
+     *     $db.error can be checked)
+     */
+    public Instance fetch(Number keyValue)
+    {
+        try
+        {
+            Instance instance = entity.fetch(keyValue);
+
+            return instance;
+        }
+        catch(SQLException sqle)
+        {
+            Logger.log(sqle);
+            entity.getDB().setError(sqle.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * Called by the #foreach directive.
+     *
+     * @return a RowIterator on all instances of this entity, possibly previously
+     *      refined or ordered.
+     */
+    public Iterator iterator()
+    {
+        try
+        {
+            RowIterator iterator = entity.query(refineCriteria, order);
+
+            return iterator;
+        }
+        catch(SQLException sqle)
+        {
+            Logger.log(sqle);
+            entity.getDB().setError(sqle.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * Get all the rows in a list of maps.
+     *
+     * @return a list of all the rows
+     */
+    public List getRows()
+    {
+        try
+        {
+            RowIterator iterator = entity.query(refineCriteria, order);
+
+            return iterator.getRows();
+        }
+        catch(SQLException sqle)
+        {
+            Logger.log(sqle);
+            entity.getDB().setError(sqle.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * <p>Refines this entity reference querying result. The provided criterium will be added to the 'where' clause (or a 'where' clause will be added).</p>
+     *
+     * <p>This method can be called several times, thus allowing a field-by-field handling of an html search form.</p>
+     *
+     * <p>All criteria will be merged with the sql 'and' operator (if there is an initial where clause, it is wrapped into parenthesis).</p>
+     *
+     * <p>Example: if we issue the following calls from inside the template:</p>
+     * <blockquote>
+     * $person.refine("age>30")
+     * <br>
+     * $person.refine("salary>3000")
+     * </blockquote>
+     * <p>the resulting query that will be issed is:</p>
+     *
+     * <p><code>select * from person where (age>30) and (salary>3000)</code></p>
+     *
+     * @param criterium a valid sql condition
+     */
+    public void refine(String criterium)
+    {
+        Logger.trace("refine: " + criterium);
+
+        /* protect from SQL query injection */
+
+        // FIXME: check that there is an even number of "'"
+        if( /* criterium.indexOf('\'') != -1 || */criterium.indexOf(';') != -1 || criterium.indexOf("--") != -1)
+        {
+            Logger.error("bad refine string: " + criterium);
+        }
+        else
+        {
+            if(refineCriteria == null)
+            {
+                refineCriteria = new ArrayList();
+            }
+            refineCriteria.add(criterium);
+        }
+    }
+
+    /**
+     * Clears any refinement made on this entity.
+     */
+    public void clearRefinement()
+    {
+        refineCriteria = null;
+    }
+
+    /**
+     * <p>Specify an 'order by' clause for this attribute reference result.</p>
+     * <p>If an 'order by' clause is already present in the original query, the ew one is appended (but successive calls to this method overwrite previous ones).</p>
+     * <p> postfix " DESC " to a column for descending order.</p>
+     * <p>Pass it null or an empty string to clear any ordering.</p>
+     *
+     * @param order valid sql column names (separated by commas) indicating the
+     *      desired order
+     */
+    public void setOrder(String order)
+    {
+        /* protect from SQL query injection */
+        if(order.indexOf('\'') != -1 || order.indexOf(';') != -1 || order.indexOf("--") != -1)
+        {
+            Logger.error("bad order string: " + order);
+        }
+        else
+        {
+            this.order = order;
+        }
+    }
+
+    /**
+     * Create a new instance for this entity.
+     *
+     * @return the newly created instance
+     */
+    public Instance newInstance()
+    {
+        Instance instance = entity.newInstance();
+
+        return instance;
+    }
+
+    /**
+     * Build a new instance from a Map object.
+     *
+     * @param values the Map object containing the values
+     * @return the newly created instance
+     */
+    public Instance newInstance(Map<String, Object> values)
+    {
+        Instance instance = entity.newInstance(values);
+
+        return instance;
+    }
+
+    /**
+     * Validate values of this instance.
+     * @param values
+     * @return true if values are valid with respect to defined column constraints
+     */
+    public boolean validate(Map<String, Object> values)
+    {
+        try
+        {
+            return entity.validate(values);
+        }
+        catch(SQLException sqle)
+        {
+            Logger.error("could not check data validity!");
+            entity.getDB().getUserContext().addValidationError("internal errror");
+            Logger.log(sqle);
+            return false;
+        }
+    }
+
+    /**
+     * Getter for the list of column names.
+     *
+     * @return the list of column names
+     */
+    public List getColumns()
+    {
+        return entity.getColumns();
+    }
+
+    public long getCount()
+    {
+        return entity.getCount(refineCriteria);
+    }
+
+    /**
+     * The wrapped entity.
+     */
+    private Entity entity = null;
+
+    /**
+     * Specified order.
+     */
+    private String order = null;
+
+    /**
+     * Specified refining criteria.
+     */
+    private List<String> refineCriteria = null;
+
+    /**
+     * toString, used for debugging
+     */
+    public String toString()
+    {
+        return "[" + getName() + " with attributes: " + entity.getAttributes().keySet() + "]";
+    }
+}

Added: velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/ExternalObjectWrapper.java
URL: http://svn.apache.org/viewvc/velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/ExternalObjectWrapper.java?rev=1298906&view=auto
==============================================================================
--- velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/ExternalObjectWrapper.java (added)
+++ velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/ExternalObjectWrapper.java Fri Mar  9 16:23:25 2012
@@ -0,0 +1,640 @@
+package org.apache.velocity.velosurf.context;
+
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.velocity.velosurf.model.Entity;
+import org.apache.velocity.velosurf.util.Logger;
+
+/**
+ * <p>This wrapper allows one to specify custom mapping objects that don't inherit from Instance.</p>
+ * <p>For now, the introspection is rather basic but may work for standard getters without ambiguity.</p>
+ *
+ *  @author <a href="mailto:claude.brisson@gmail.com">Claude Brisson</a>
+ */
+public class ExternalObjectWrapper extends Instance
+{
+    /**
+     * Builds a new PlaiObjectWrapper.
+     *
+     * @param entity the related entity
+     * @param object target object
+     */
+    public ExternalObjectWrapper(Entity entity, Object object)
+    {
+        super(entity);
+        wrapped = object;
+
+        Class clazz = wrapped.getClass();
+
+        classInfo = classInfoMap.get(clazz.getName());
+        if(classInfo == null)
+        {
+            classInfo = new ClassInfo(clazz);
+            classInfoMap.put(clazz.getName(), classInfo);
+        }
+    }
+
+    /**
+     * Wrapper generic getter. Tries first to get the property from the wrapped object, and falls back to the superclass
+     * if not found.
+     *
+     * @param key key of the property to be returned
+     * @return a String, an Instance, an AttributeReference or null if not found or if an error
+     *      occurs
+     */
+    public Object get(Object key)
+    {
+        Object ret = getExternal(key);
+
+        if(ret == null)
+        {
+            ret = super.get(key);
+        }
+        return ret;
+    }
+
+    /**
+     * External getter. Get a value on the external object
+     *
+     * @param key key of the property to be returned
+     * @return a String, an Instance, an AttributeReference or null if not found or if an error
+     *      occurs
+     */
+    public Object getExternal(Object key)
+    {
+        Method m = classInfo.getGetter((String)key);
+
+        if(m != null)
+        {
+            try
+            {
+                return m.invoke(wrapped, m.getParameterTypes().length > 0 ? new Object[] { key } : new Object[] {});    // return even if result is null
+            }
+            catch(Exception e)
+            {
+                Logger.warn("could not get value of field " + getEntity().getName() + "." + key
+                            + "... falling back to the Instance getter");
+                Logger.log(e);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Wrapper generic setter. Tries first to set the property into the wrapped object, and falls back to the superclass
+     * if not found.
+     * @param key key of the property to be set
+     * @param value corresponding value
+     * @return previous value, or null
+     */
+    public Object put(String key, Object value)
+    {
+        Method m = classInfo.getSetter((String)key);
+
+        if(m != null)
+        {
+            try
+            {
+                return m.invoke(wrapped,
+                                m.getParameterTypes().length == 2
+                                ? new Object[] { key, value } : new Object[] { value });
+            }
+            catch(Exception e)
+            {
+                Logger.warn("could not set value of field " + getEntity().getName() + "." + key + " to '" + value
+                            + "'... falling back to the Instance setter");
+                Logger.log(e);
+            }
+        }
+        return super.put(key, value);
+    }
+
+    /**
+     * <p>Try to update the row associated with this Instance using an update() method
+     * in the external object.</p>
+     *
+     * @return <code>true</code> if successfull, <code>false</code> if an error
+     *     occurs (in which case $db.error can be checked).
+     */
+    public boolean update()
+    {
+        Method m = classInfo.getUpdate1();
+
+        if(m != null)
+        {
+            Class cls = m.getReturnType();
+            Object args[] = {};
+
+            try
+            {
+                if(cls == boolean.class || cls == Boolean.class)
+                {
+                    return((Boolean)m.invoke(wrapped, args)).booleanValue();
+                }
+                else
+                {
+                    Logger.warn(
+                        "external object wrapper: update method should return boolean or Boolean. Actual result will be ignored.");
+                    m.invoke(wrapped, args);
+                    return true;
+                }
+            }
+            catch(Exception e)
+            {
+                Logger.warn("method" + cls.getName() + ".update() throw exception: " + e);
+                return false;
+            }
+        }
+        else
+        {
+            return super.update();
+        }
+    }
+
+    /**
+     * <p>Try to update the row associated with this Instance using an update(map) method
+     * in the external object.</p>
+     *
+     * @return <code>true</code> if successfull, <code>false</code> if an error
+     *     occurs (in which case $db.error can be checked).
+     */
+    public boolean update(Map values)
+    {
+        Method m = classInfo.getUpdate2();
+
+        if(m != null)
+        {
+            Class cls = m.getReturnType();
+            Object args[] = { values };
+
+            try
+            {
+                if(cls == boolean.class || cls == Boolean.class)
+                {
+                    return((Boolean)m.invoke(wrapped, args)).booleanValue();
+                }
+                else
+                {
+                    Logger.warn(
+                        "external object wrapper: update method should return boolean or Boolean. Actual result will be ignored.");
+                    m.invoke(wrapped, args);
+                    return true;
+                }
+            }
+            catch(Exception e)
+            {
+                Logger.warn("method" + cls.getName() + ".update() throw exception: " + e);
+                return false;
+            }
+        }
+        else
+        {
+            return super.update();
+        }
+    }
+
+    /**
+     * <p>Tries to delete the row associated with this Instance using a delete() method in the external object.
+     * Velosurf will ensure all key columns are specified, to avoid an accidental massive update.</p>
+     *
+     * @return <code>true</code> if successfull, <code>false</code> if an error
+     *     occurs (in which case $db.error can be checked).
+     */
+    public boolean delete()
+    {
+        Method m = classInfo.getDelete();
+
+        if(m != null)
+        {
+            Class cls = m.getReturnType();
+            Object args[] = {};
+
+            try
+            {
+                if(cls == boolean.class || cls == Boolean.class)
+                {
+                    return((Boolean)m.invoke(wrapped, args)).booleanValue();
+                }
+                else
+                {
+                    Logger.warn(
+                        "external object wrapper: delete method should return boolean or Boolean. Actual result will be ignored.");
+                    m.invoke(wrapped, args);
+                    return true;
+                }
+            }
+            catch(Exception e)
+            {
+                Logger.warn("method" + cls.getName() + ".delete() throw exception: " + e);
+                return false;
+            }
+        }
+        else
+        {
+            return super.delete();
+        }
+    }
+
+    /**
+     * Tries to insert a new row corresponding to this Instance using an insert() method in the external object.
+     *
+     * @return <code>true</code> if successfull, <code>false</code> if an error
+     *     occurs (in which case $db.error can be checked).
+     */
+    public boolean insert()
+    {
+        Method m = classInfo.getInsert();
+
+        if(m != null)
+        {
+            Class cls = m.getReturnType();
+            Object args[] = {};
+
+            try
+            {
+                if(cls == boolean.class || cls == Boolean.class)
+                {
+                    return((Boolean)m.invoke(wrapped, args)).booleanValue();
+                }
+                else
+                {
+                    Logger.warn(
+                        "external object wrapper: insert method should return boolean or Boolean. Actual result will be ignored.");
+                    m.invoke(wrapped, args);
+                    return true;
+                }
+            }
+            catch(Exception e)
+            {
+                Logger.warn("method" + cls.getName() + ".delete() throw exception: " + e);
+                return false;
+            }
+        }
+        else
+        {
+            return super.insert();
+        }
+    }
+
+    /**
+     * Returns the underlying external object.
+     *
+     * @return the external object
+     */
+    public Object unwrap()
+    {
+        return wrapped;
+    }
+
+    /** The wrapped object. */
+    Object wrapped = null;
+
+    /** Info on the wrapped object class. */
+    ClassInfo classInfo = null;
+
+    /** A map of class infos. */
+    static Map<String, ClassInfo> classInfoMap = new HashMap<String, ClassInfo>();
+
+    /** A cache for the wrapped object getter methods. */
+    Map getterCache = null;
+
+    /** A cache for the wrapped object setter methods. */
+    Map setterCache = null;
+
+    /** This private class gathers informations on the class of wrapped objects. */
+    static private class ClassInfo
+    {
+        ClassInfo(Class clazz)
+        {
+            this.clazz = clazz;
+        }
+
+        /**
+         * Getter getter :-) .
+         * @param key property name
+         * @return property getter, if found
+         */
+        Method getGetter(String key)
+        {
+            Method result = (Method)getterMap.get(key);
+
+            if(result == noSuchMethod)
+            {
+                return null;
+            }
+            if(result != null)
+            {
+                return result;
+            }
+
+            Class[] types = {};
+
+            // getFoo
+            StringBuffer sb = new StringBuffer("get");
+
+            sb.append(key);
+            try
+            {
+                result = clazz.getMethod(sb.toString(), types);
+                getterMap.put(key, result);
+                return result;
+            }
+            catch(NoSuchMethodException nsme) {}
+
+            // getfoo
+            char c = sb.charAt(3);
+
+            sb.setCharAt(3, Character.isLowerCase(c) ? Character.toUpperCase(c) : Character.toLowerCase(c));
+            try
+            {
+                result = clazz.getMethod(sb.toString(), types);
+                getterMap.put(key, result);
+                return result;
+            }
+            catch(NoSuchMethodException nsme) {}
+
+            // get(foo)
+            result = getGenericGetter();
+            if(result == null)
+            {
+                getterMap.put(key, noSuchMethod);
+            }
+            else
+            {
+                getterMap.put(key, result);
+            }
+            return result;
+        }
+
+        /**
+         * Setter getter
+         * @param key property name
+         * @return property setter, if found
+         */
+        Method getSetter(String key)
+        {
+            Method result = setterMap.get(key);
+
+            if(result == noSuchMethod)
+            {
+                return null;
+            }
+            if(result != null)
+            {
+                return result;
+            }
+
+            Class[] types = {};
+
+            /* setFoo */
+            StringBuffer sb = new StringBuffer("set");
+
+            sb.append(key);
+            try
+            {
+                result = clazz.getMethod(sb.toString(), types);
+                setterMap.put(key, result);
+                return result;
+            }
+            catch(NoSuchMethodException nsme) {}
+
+            /* setfoo */
+            char c = sb.charAt(3);
+
+            sb.setCharAt(3, Character.isLowerCase(c) ? Character.toUpperCase(c) : Character.toLowerCase(c));
+            try
+            {
+                result = clazz.getMethod(sb.toString(), types);
+                setterMap.put(key, result);
+                return result;
+            }
+            catch(NoSuchMethodException nsme) {}
+
+            /* put(foo,bar) */
+            result = getGenericSetter();
+            if(result == null)
+            {
+                setterMap.put(key, noSuchMethod);
+            }
+            else
+            {
+                setterMap.put(key, result);
+            }
+            return result;
+        }
+
+        /**
+         * Tries to get an update() method in the wrapped object.
+         * @return found update method, if any
+         */
+        Method getUpdate1()
+        {
+            if(update1 == noSuchMethod)
+            {
+                return null;
+            }
+            if(update1 != null)
+            {
+                return update1;
+            }
+            try
+            {
+                return update1 = clazz.getMethod("update", new Class[] {});
+            }
+            catch(NoSuchMethodException nsme)
+            {
+                update1 = noSuchMethod;
+                return null;
+            }
+        }
+
+        /**
+         * Tries to get an update(Map) method in the wrapped object.
+         * @return found update method, if any
+         */
+        Method getUpdate2()
+        {
+            if(update2 == noSuchMethod)
+            {
+                return null;
+            }
+            if(update2 != null)
+            {
+                return update2;
+            }
+            try
+            {
+                return update2 = clazz.getMethod("update", new Class[] { Map.class });
+            }
+            catch(NoSuchMethodException nsme)
+            {
+                update2 = noSuchMethod;
+                return null;
+            }
+        }
+
+        /**
+         * Tries to get an insert() method in the wrapped object.
+         * @return found method, if any
+         */
+        Method getInsert()
+        {
+            if(insert == noSuchMethod)
+            {
+                return null;
+            }
+            if(insert != null)
+            {
+                return insert;
+            }
+            try
+            {
+                return insert = clazz.getMethod("update", new Class[] { Map.class });
+            }
+            catch(NoSuchMethodException nsme)
+            {
+                insert = noSuchMethod;
+                return null;
+            }
+        }
+
+        /**
+         * Tries to get a delete() method in the wrapped object.
+         * @return found method, if any
+         */
+        Method getDelete()
+        {
+            if(delete == noSuchMethod)
+            {
+                return null;
+            }
+            if(delete != null)
+            {
+                return delete;
+            }
+            try
+            {
+                return delete = clazz.getMethod("update", new Class[] { Map.class });
+            }
+            catch(NoSuchMethodException nsme)
+            {
+                delete = noSuchMethod;
+                return null;
+            }
+        }
+
+        /**
+         * Tries to get a generic getter in the wrapped object.
+         * @return found method, if any
+         */
+        Method getGenericGetter()
+        {
+            if(genericGetter == noSuchMethod)
+            {
+                return null;
+            }
+            if(genericGetter != null)
+            {
+                return genericGetter;
+            }
+
+            Class[] types = new Class[] { Object.class };
+
+            try
+            {
+                return genericGetter = clazz.getMethod("get", types);
+            }
+            catch(NoSuchMethodException nsme)
+            {
+                genericGetter = noSuchMethod;
+                return null;
+            }
+        }
+
+        /**
+         * Tries to get a generic setter in the wrapped object.
+         * @return found method, if any
+         */
+        Method getGenericSetter()
+        {
+            if(genericSetter == noSuchMethod)
+            {
+                return null;
+            }
+            if(genericSetter != null)
+            {
+                return genericSetter;
+            }
+
+            Class[] types = new Class[] { Object.class, Object.class };
+
+            try
+            {
+                return genericSetter = clazz.getMethod("put", types);
+            }
+            catch(NoSuchMethodException nsme)
+            {
+                genericSetter = noSuchMethod;
+                return null;
+            }
+        }
+
+        /**
+         * Wrapped class.
+         */
+        Class clazz;
+
+        /**
+         * Getter map.
+         */
+        Map<String, Method> getterMap = new HashMap<String, Method>();
+
+        /**
+         * Setter map.
+         */
+        Map<String, Method> setterMap = new HashMap<String, Method>();
+
+        /**
+         * Generic getter.
+         */
+        Method genericGetter = null;
+
+        /**
+         * Generic setter.
+         */
+        Method genericSetter = null;
+
+        /**
+         * Update method, first form.
+         */
+        Method update1 = null;
+
+        /**
+         * Update method, second form.
+         */
+        Method update2 = null;
+
+        /**
+         * Insert method.
+         */
+        Method insert = null;
+
+        /**
+         * Delete method.
+         */
+        Method delete = null;
+
+        /* dummy method object used to remember we already tried to find an unexistant method. */
+        static Method noSuchMethod;
+
+        static
+        {
+            try
+            {
+                noSuchMethod = Object.class.getMethod("toString", new Class[] {});
+            }
+            catch(NoSuchMethodException nsme) {}
+        }
+    }
+}

Added: velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/HasParametrizedGetter.java
URL: http://svn.apache.org/viewvc/velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/HasParametrizedGetter.java?rev=1298906&view=auto
==============================================================================
--- velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/HasParametrizedGetter.java (added)
+++ velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/HasParametrizedGetter.java Fri Mar  9 16:23:25 2012
@@ -0,0 +1,20 @@
+package org.apache.velocity.velosurf.context;
+
+import java.util.Map;
+
+/**
+ * Implemented by context objects that do provide a default method handler. This is used
+ * to let template designers provide extra external parameters to an attribute or an action,
+ * like: <code>$myinstance.myattribute({'param1':'value1','param2':'value2'})</code>
+ *
+ */
+public interface HasParametrizedGetter
+{
+    /**
+     * Default method handler, called by Velocity when it did not find the specified method
+     *
+     * @param key asked key
+     * @param params passed parameters
+     */
+    public Object getWithParams(String key, Map params);
+}

Added: velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/Instance.java
URL: http://svn.apache.org/viewvc/velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/Instance.java?rev=1298906&view=auto
==============================================================================
--- velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/Instance.java (added)
+++ velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/Instance.java Fri Mar  9 16:23:25 2012
@@ -0,0 +1,682 @@
+/*
+ * Copyright 2003 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.velocity.velosurf.context;
+
+import java.sql.SQLException;
+import java.util.*;
+import org.apache.velocity.velosurf.model.Action;
+import org.apache.velocity.velosurf.model.Attribute;
+import org.apache.velocity.velosurf.model.Entity;
+import org.apache.velocity.velosurf.sql.Database;
+import org.apache.velocity.velosurf.sql.PooledPreparedStatement;
+import org.apache.velocity.velosurf.util.Logger;
+import org.apache.velocity.velosurf.util.StringLists;
+import org.apache.velocity.velosurf.util.UserContext;
+
+/**
+ * An Instance provides field values by their name.
+ *
+ *  @author <a href=mailto:claude.brisson@gmail.com>Claude Brisson</a>
+ */
+public class Instance extends TreeMap<String,Object> implements HasParametrizedGetter
+{
+
+    /**
+     * Build an empty instance for the given entity.
+     *  The method initialize(Entity) should be called afterwards.
+     */
+    public Instance()
+    {
+    }
+
+
+    /**
+     * Build an empty instance for the given entity.
+     * This is the only constructor that will keep columns in their natural order
+     * (others will sort columns alphabetically)
+     *
+     * @param entity Entity this instance is a realisation of
+     */
+    public Instance(Entity entity)
+    {
+        super(entity.getColumnOrderComparator());
+        initialize(entity);
+    }
+
+    /**
+     * Builds a generic instance using <code>values</code>.
+     * @param values
+     */
+    public Instance(Map<String,Object> values)
+    {
+        this(values,null);
+    }
+
+    /**
+     * Builds a generic instance using <code>values</code>.
+     * @param values
+     * @param db
+     */
+    public Instance(Map<String,Object> values, Database db)
+    {
+        this.db = db;
+        for(Object key:values.keySet())
+        {
+            put(Database.adaptContextCase((String)key),values.get(key));
+        }
+    }
+
+     /**
+      * Initialization. Meant to be overloaded if needed.
+      * @param entity
+      */
+     public void initialize(Entity entity)
+     {
+         this.entity = entity;
+         db = this.entity.getDB();
+         localized = this.entity.hasLocalizedColumns();
+         dirtyFlags = new ArrayList();
+         for(int i=0; i<entity.getUpdatableColumns().size();i++)
+         {
+             dirtyFlags.add(false);
+         }
+     }
+
+    /**
+     * Get this Instance's Entity.
+     *
+     * @return this Instance's Entity.
+     */
+    public EntityReference getEntity()
+    {
+        return new EntityReference(entity);
+    }
+
+    /**
+     * <p>Returns an ArrayList of two-entries maps ('name' & 'value'), meant to be use in a #foreach loop to build form fields.</p>
+     * <p>Example:</p>
+     * <code>
+     * #foreach ($field in $product.primaryKey)<br>
+     * &nbsp;&nbsp;&lt;input type=hidden name='$field.name' value='$field.value'&gt;<br>
+     * #end</code>
+     * <p>Please note that this method won't be of any help if you are using column aliases.</p>
+     *
+     * @return an ArrayList of two-entries maps ('name' & 'value')
+     */
+    public List getPrimaryKey()
+    {
+        List<Map<String,Object>> result = new ArrayList<Map<String,Object>>();
+        if (entity!=null)
+        {
+            for (Iterator i=entity.getPKCols().iterator();i.hasNext();)
+            {
+                String key = (String)i.next();
+                Map<String,Object> map = new HashMap<String,Object>();
+                map.put("name",key);
+                map.put("value",getInternal(key));
+                result.add(map);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * <p>Generic getter, used to access this instance properties by their name.</p>
+     * <p>Asked property is first searched in the Map, then among Attributes defined for the entity.</p>
+     *
+     * @param k key of the property to be returned
+     * @return a String, an Instance, an AttributeReference or null if an error
+     *      occurs
+     */
+    public Object get(Object k)
+    {
+        String key = resolveName((String)k);
+        Object result = null;
+        try
+        {
+            result = super.get(key);
+            if (result == null)
+            {
+                if (entity!=null)
+                {
+                    Attribute attribute = entity.getAttribute(key);
+                    if (attribute != null)
+                    {
+                        switch (attribute.getType())
+                        {
+                            case Attribute.ROWSET:
+                                result = new AttributeReference(this,attribute);
+                                // then cache it in the map, so that order and refinement will work later in the same context
+                                super.put(key,result);
+                                break;
+                            case Attribute.ROW:
+                                result = attribute.fetch(this);
+                                if(attribute.getCaching())
+                                {
+                                  super.put(key,result);
+                                }
+                                break;
+                            case Attribute.SCALAR:
+                                result = attribute.evaluate(this);
+                                if(attribute.getCaching())
+                                {
+                                  super.put(key,result);
+                                }
+                                break;
+                            default:
+                                Logger.error("Unknown attribute type for "+entity.getName()+"."+key+"!");
+                        }
+                    }
+                    else
+                    {
+                        Action action = entity.getAction(key);
+                        if (action != null)
+                        {
+                            result = action.perform(this);
+                        }
+                    }
+                }
+            }
+            else if (localized && entity.isLocalized(key))
+            {
+                result = db.getUserContext().localize(result.toString());
+            }
+        }
+        catch (SQLException sqle)
+        {
+            handleSQLException(sqle);
+        }
+        return result;
+    }
+
+    /**
+     * Default method handler, called by Velocity when it did not find the specified method.
+     *
+     * @param key asked key
+     * @param params passed parameters
+     * @see HasParametrizedGetter
+     */
+    public Object getWithParams(String key,Map params)
+    {
+        for(Map.Entry entry: (Set<Map.Entry>)params.entrySet())
+        {
+            put(db.adaptCase((String)entry.getKey()),entry.getValue());
+        }
+        return get(db.adaptCase(key));
+    }
+
+    /**
+     * Generic setter.
+     *
+     * @param key key of the property to be set
+     * @param value corresponding value
+     * @return previous value, or null
+     */
+    public synchronized Object put(String key, Object value)
+    {
+        key = resolveName(key);
+        int index;
+        if (entity != null)
+        {
+            value = entity.filterIncomingValue(key,value);
+            if ( (index = entity.getUpdatableColumnIndex(key) ) != -1)
+            {
+                dirtyFlags.set(index,true);
+            }
+        }
+        return super.put(key,value);
+    }
+
+    public synchronized void setClean()
+    {
+        if(dirtyFlags != null)
+        {
+            for(int i=0;i<dirtyFlags.size();i++)
+            {
+                dirtyFlags.set(i,false);
+            }
+        }
+    }
+
+    /**
+     * Global setter that will only set values the correspond to actual
+     * columns (otherwise, use putAll(Map values)).
+     *
+     * @param values corresponding values
+     */
+
+    public synchronized void setColumnValues(Map<String,Object> values)
+    {
+        if(entity == null)
+        {
+            Logger.warn("instance.setColumnValues(map) cannot be used when entity is null");
+            return;
+        }
+        int index;
+        for(Map.Entry<String,Object> entry:values.entrySet())
+        {
+            if( entity.isColumn(entity.resolveName(entry.getKey())))
+            {
+                put(entry.getKey(),entry.getValue());
+            }
+        }
+    }
+
+    /**
+     * Internal getter. First tries on the external object then on the Map interface.
+     *
+     * @param key key of the property to be returned
+     * @return a String, an Instance, an AttributeReference or null if not found or if an error
+     *      occurs
+     */
+    public Object getInternal(Object key)
+    {
+        Object ret = getExternal(key);
+        if (ret == null) ret = super.get(key);
+        return ret;
+    }
+
+    /**
+     * External getter. Meant to be overloaded in ExternalObjectWrapper.
+     *
+     * @param key key of the property to be returned
+     * @return a String, an Instance, an AttributeReference or null if not found or if an error
+     *      occurs
+     */
+    public Object getExternal(Object key)
+    {
+        return null;
+    }
+
+    /**
+     * Test equality of two instances.
+     * @param o other instance
+     * @return equality status
+     */
+    public boolean equals(Object o)
+    {
+        return super.equals(o);
+    }
+
+    /**
+     * <p>Update the row associated with this Instance from passed values.</p>
+     * <p>Velosurf will ensure all key columns are specified, to avoid an accidental massive update.</p>
+     *
+     * @return <code>true</code> if successfull, <code>false</code> if an error
+     *     occurs (in which case $db.error can be checked).
+     */
+    public synchronized boolean update()
+    {
+        try
+        {
+            if (entity == null)
+            {
+                throw new SQLException("Cannot update an instance whose Entity is null.");
+            }
+            if (entity.isReadOnly())
+            {
+                throw new SQLException("Entity "+entity.getName()+" is read-only.");
+            }
+            
+            List<String> updateClause = new ArrayList<String>();
+            List<String> whereClause = new ArrayList<String>();
+            List<Object> params = new ArrayList<Object>();
+            List<String> cols = entity.getUpdatableColumns();
+            for (int c = 0; c < cols.size(); c++)
+            {
+                String col = cols.get(c);
+                if(dirtyFlags.get(c))
+                {
+                    Object value = getInternal(col);
+                    if (value!=null)
+                    {
+                        updateClause.add(col+"=?");
+                        if (entity.isObfuscated(col))
+                        {
+                            value = entity.deobfuscate(value);
+                        }
+                        params.add(value);
+                    } // TODO else " = null " ? May be a configuration option.
+                }
+            }
+            if(updateClause.size() ==0)
+            {
+                Logger.warn("update of instance '"+entity.getName()+"' all non-key columns are null or non-dirty - no update will be performed");
+                // return true anyway ?
+                return true;
+            }
+            for (String col:entity.getPKCols())
+            {
+                Object value = getInternal(col);
+                if (value == null) throw new SQLException("field '"+col+"' belongs to primary key and cannot be null!");
+                if (entity.isObfuscated(col)) value = entity.deobfuscate(value);
+//                if (entity.isLocalized(col)) value = entity.unlocalize(value); ???
+                whereClause.add(col+"=?");
+                params.add(value);
+            }
+            String query = "update "+entity.getTableName()+" set "+StringLists.join(updateClause,",")+" where "+StringLists.join(whereClause," and ");
+            PooledPreparedStatement statement = db.prepare(query);
+            int nb = statement.update(params);
+            if (nb==0)
+            {
+                Logger.warn("query \""+query+"\" affected 0 row...");
+            }
+            else if (nb>1)
+            { // ?!?! Referential integrities on key columns should avoid this...
+                throw new SQLException("query \""+query+"\" affected more than 1 rows!");
+            }
+            else
+            {
+                /* invalidate cache */
+                if (entity != null)
+                {
+                    entity.invalidateInstance(this);
+                }
+            }
+            setClean();
+            return true;
+        }
+        catch (SQLException sqle)
+        {
+            handleSQLException(sqle);
+            return false;
+        }
+    }
+
+    /**
+     * <p>Update the row associated with this Instance from actual values.</p>
+     * <p>Velosurf will ensure all key columns are specified, to avoid an accidental massive update.</p>
+     *
+     * @param values values to be used for the update
+     * @return <code>true</code> if successfull, <code>false</code> if an error
+     *      occurs (in which case $db.error can be checked).
+     */
+    public synchronized boolean update(Map<String,Object> values)
+    {
+        if (values != null && values != this)
+        {
+            setColumnValues(values);
+        }
+        return update();
+    }
+
+    /**
+     * <p>Delete the row associated with this Instance.</p>
+     * <p>Velosurf will ensure all key columns are specified, to avoid an accidental massive update.</p>
+     *
+     * @return <code>true</code> if successfull, <code>false</code> if an error
+     *     occurs (in which case $db.error can be checked).
+     */
+    public synchronized boolean delete()
+    {
+        try
+        {
+            if (entity == null)
+            {
+              throw new SQLException("Instance.delete: Error: Entity is null!");
+            }
+            List<String> whereClause = new ArrayList<String>();
+            List<Object> params = new ArrayList<Object>();
+            for (String col:entity.getPKCols())
+            {
+                Object value = getInternal(col);
+                if (value == null) throw new SQLException("Instance.delete: Error: field '"+col+"' belongs to primary key and cannot be null!");
+                if (entity.isObfuscated(col)) value = entity.deobfuscate(value);
+                whereClause.add(col+"=?");
+                params.add(value);
+            }
+            String query = "delete from "+entity.getTableName()+" where "+StringLists.join(whereClause," and ");
+            PooledPreparedStatement statement = db.prepare(query);
+            int nb = statement.update(params);
+            if (nb==0)
+            {
+                Logger.warn("query \""+query+"\" affected 0 row...");
+            }
+            else if (nb>1) // ?!?! Referential integrities on key columns should avoid this...
+            {
+                throw new SQLException("query \""+query+"\" affected more than 1 rows!");
+            }
+            else
+            {
+                /* invalidate cache */
+                if (entity != null)
+                {
+                    entity.invalidateInstance(this);
+                }
+            }
+            return true;
+        }
+        catch (SQLException sqle)
+        {
+            handleSQLException(sqle);
+            return false;
+        }
+    }
+
+    /**
+     * Insert a new row corresponding to this Instance.
+     *
+     * @return <code>true</code> if successfull, <code>false</code> if an error
+     *     occurs (in which case $db.error can be checked).
+     */
+    public synchronized boolean insert()
+    {
+        try
+        {
+            if (entity == null)
+            {
+                throw new SQLException("Instance.insert: Error: Entity is null!");
+            }
+
+            if (!entity.validate(this))
+            {
+                return false;
+            }
+            List<String> colsClause = new ArrayList<String>();
+            List<String> valsClause = new ArrayList<String>();
+            List<Object> params = new ArrayList<Object>();
+            List<String> cols = entity.getColumns();
+            for (String col:cols)
+            {
+                Object value = getInternal(col);
+                if (value!=null)
+                {
+                    colsClause.add(col);
+                    valsClause.add("?");
+                    if (entity.isObfuscated(col))
+                    {
+                        value = entity.deobfuscate(value);
+                    }
+                    params.add(value);
+                }
+            }
+            String query = "insert into "+entity.getTableName()+" ("+StringLists.join(colsClause,",")+") values ("+StringLists.join(valsClause,",")+")";
+            PooledPreparedStatement statement = db.prepare(query);
+            statement.update(params);
+            List<String> keys = entity.getPKCols();
+            if (keys.size() == 1)
+            {
+                /* What if the ID is not autoincremented? TODO check it. => reverse engineering of autoincrement, and set the value in the instance itself */
+                String keycol = keys.get(0);
+                long newid = statement.getLastInsertID();
+                db.getUserContext().setLastInsertedID(entity,newid);
+                if(getInternal(keycol) == null)
+                {
+                    put(keycol,entity.isObfuscated(keycol)?entity.obfuscate(newid):newid);
+                }
+            }
+            setClean();
+            return true;
+        }
+        catch (SQLException sqle)
+        {
+            handleSQLException(sqle);
+            return false;
+        }
+    }
+
+    /**
+     * Validate this instance against declared contraints.
+     * @return a boolean stating whether this instance data are valid in regard to declared constraints
+     */
+    public boolean validate()
+    {
+        try
+        {
+            return entity.validate(this);
+        }
+        catch(SQLException sqle)
+        {
+            handleSQLException(sqle);
+            return false;
+        }
+    }
+
+    /**
+     * Handle an sql exception.
+     *
+     */
+    private void handleSQLException(SQLException sqle)
+    {
+        Logger.log(sqle);
+        db.setError(sqle.getMessage());
+    }
+    
+    protected String resolveName(String name)
+    {
+        if(entity != null)
+        {
+            return entity.resolveName(name);
+        }
+        else if (db != null)
+        {
+            return db.adaptCase(name);
+        }
+        else
+        {
+            return name;
+        }
+    }
+
+    /**
+     * Check for a key
+     *
+     * @return whether or not this key is present
+     */
+    public boolean containsKey(Object key)
+    {
+        return super.containsKey(resolveName((String)key));
+    }
+
+    /**
+     * Removes an association
+     *
+     * @return the removed object, or null
+     */
+    public Object remove(Object key)
+    {
+        return super.remove(resolveName((String)key));
+    }
+
+    /**
+     * This Instance's Entity.
+     */
+    protected Entity entity = null;
+
+    /**
+     * Is there a column to localize?
+     */
+    private boolean localized = false;
+
+    /**
+     * The main database connection.
+     */
+    protected Database db = null;
+
+    /**
+     * Keep a dirty flag per column
+     */
+    protected List<Boolean> dirtyFlags = null;
+
+    /**
+      Inherit toString to avoid listing cached AttributeReference
+     */
+    public String toString()
+    {
+	StringBuffer ret = new StringBuffer("{");
+	boolean comma = false;
+	for(Map.Entry<String,Object> entry:super.entrySet())
+        {
+	    if(comma)
+            {
+		ret.append(", ");
+	    }
+            else
+            {
+              comma = true;
+            }
+	    if(!(entry.getValue() instanceof AttributeReference))
+            {
+		ret.append(entry.getKey());
+		ret.append("=");
+		ret.append(entry.getValue());
+	    }
+	}
+	ret.append("}");
+	return ret.toString();
+    }
+
+    /**
+     * Insert or update, depending on whether or not a value for the id key is present and does exist
+     *
+     * @return success flag
+     */
+    public synchronized boolean upsert()
+    {
+	List<Map<String,Object>> primkey = getPrimaryKey();
+	if(primkey.size() != 1)
+        {
+            Logger.error("Instance.upsert: singleton primary key expected"); // TODO CB - should throw/catch for homogeneity
+	    return false;
+	}
+        Object keyVal = primkey.get(0).get("value");
+	if(keyVal == null)
+        {
+	    return insert();
+	}
+        else
+        {
+            Instance previous = getEntity().fetch(String.valueOf(keyVal)); // CB  -TODO: there should be an Entity.fetch(Object) method
+            return previous == null ? insert() : update();
+	}
+    }
+
+    /**
+     * Insert or update, depending on whether or not a value for the id key is present and does exist
+     */
+    public synchronized boolean upsert(Map<String,Object> values)
+    {
+	if (values != null && values != this)
+        {
+	    setColumnValues(values);
+	}
+	return upsert();
+    }
+
+}

Added: velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/RowIterator.java
URL: http://svn.apache.org/viewvc/velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/RowIterator.java?rev=1298906&view=auto
==============================================================================
--- velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/RowIterator.java (added)
+++ velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/RowIterator.java Fri Mar  9 16:23:25 2012
@@ -0,0 +1,383 @@
+/*
+ * Copyright 2003 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.velocity.velosurf.context;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.velocity.velosurf.model.Attribute;
+import org.apache.velocity.velosurf.model.Entity;
+import org.apache.velocity.velosurf.sql.PooledStatement;
+import org.apache.velocity.velosurf.sql.ReadOnlyMap;
+import org.apache.velocity.velosurf.sql.RowHandler;
+import org.apache.velocity.velosurf.sql.SqlUtil;
+import org.apache.velocity.velosurf.util.Logger;
+
+//import org.apache.velocity.velosurf.util.UserContext;
+
+/**
+ * This class is a context wrapper for ResultSets, and provides an iteration mecanism for #foreach loops, as long as getters for values of the current row.
+ *
+ *  @author <a href=mailto:claude.brisson@gmail.com>Claude Brisson</a>
+ */
+public class RowIterator implements Iterator<Instance>, RowHandler
+{
+    /**
+     * Build a new RowIterator.
+     *
+     * @param pooledStatement the sql statement
+     * @param resultSet the resultset
+     * @param resultEntity the resulting entity (may be null)
+     */
+    public RowIterator(PooledStatement pooledStatement, ResultSet resultSet, Entity resultEntity)
+    {
+        this.pooledStatement = pooledStatement;
+        this.resultSet = resultSet;
+        this.resultEntity = resultEntity;
+    }
+
+    /**
+     * Returns true if the iteration has more elements.
+     *
+     * @return <code>true</code> if the iterator has more elements.
+     */
+    public boolean hasNext()
+    {
+        boolean ret = false;
+
+        try
+        {
+            /* always need to prefetch, as some JDBC drivers (like HSQLDB driver) seem buggued to this regard */
+            if(isOver)
+            {
+                return false;
+            }
+            else if(prefetch)
+            {
+                return true;
+            }
+            else
+            {
+                try
+                {
+                    pooledStatement.getConnection().enterBusyState();
+                    ret = resultSet.next();
+                }
+                finally
+                {
+                    pooledStatement.getConnection().leaveBusyState();
+                }
+                if(ret)
+                {
+                    prefetch = true;
+                }
+                else
+                {
+                    isOver = true;
+                    pooledStatement.notifyOver();
+                }
+            }
+            return ret;
+        }
+        catch(SQLException e)
+        {
+            Logger.log(e);
+            isOver = true;
+            pooledStatement.notifyOver();
+            return false;
+        }
+    }
+
+    /**
+     * Returns the next element in the iteration.
+     *
+     * @return an Instance.
+     */
+    public Instance next()
+    {
+        try
+        {
+            if(isOver ||!prefetch &&!resultSet.next())
+            {
+                if(!isOver)
+                {
+                    isOver = true;
+                    pooledStatement.notifyOver();
+                }
+                return null;
+            }
+            prefetch = false;
+            if(resultEntity != null && !resultEntity.isRootEntity())
+            {
+                Instance row = null;
+
+                row = resultEntity.newInstance(new ReadOnlyMap(this), true);
+                row.setClean();
+                return row;
+            }
+            else
+            {
+                return new Instance(new ReadOnlyMap(this),resultEntity == null ? null : resultEntity.getDB());
+            }
+        }
+        catch(SQLException sqle)
+        {
+            Logger.log(sqle);
+            isOver = true;
+            pooledStatement.notifyOver();
+            return null;
+        }
+    }
+
+    // for Iterator interface, but RO (why? -> positionned updates and deletes => TODO)
+
+    /**
+     * not implemented.
+     */
+    public void remove()
+    {
+        Logger.warn("'remove' not implemented");
+    }
+
+    /**
+     * Generic getter for values of the current row. If no column corresponds to the specified name and a resulting entity has been specified, search among this entity's attributes.
+     * Note that this method is the only getter of RowIterator that cares about obfuscation - other specialized getters
+     * won't do any obfuscation.
+     *
+     * @param key the name of an existing column or attribute
+     * @return an entity, an attribute reference, an instance, a string or null
+     */
+    public Object get(Object key)
+    {
+        String property = (String)key;
+        Object result = null;
+
+        try
+        {
+            if(!dataAvailable())
+            {
+                return null;
+            }
+            if(resultEntity != null && !resultEntity.isRootEntity())
+            {
+                property = resultEntity.resolveName(property);
+
+                Attribute attribute = resultEntity.getAttribute(property);
+
+                if(attribute != null)
+                {
+                    switch(attribute.getType())
+                    {
+                        case Attribute.ROWSET :
+                            result = attribute.query(new ReadOnlyMap(this));
+                            break;
+                        case Attribute.ROW :
+                            result = attribute.fetch(new ReadOnlyMap(this));
+                            break;
+                        case Attribute.SCALAR :
+                            result = attribute.evaluate(new ReadOnlyMap(this));
+                            break;
+                        default :
+                            Logger.error("Unknown attribute type for " + resultEntity.getName() + "." + property + "!");
+                    }
+                }
+            }
+            if(result == null)
+            {
+                if(resultEntity != null && resultEntity.isObfuscated(property))
+                {
+                    result = resultEntity.obfuscate(resultSet.getObject(property));
+                }
+                else
+                {
+                    result = resultSet.getObject(property);
+                }
+            }
+        }
+        catch(SQLException e)
+        {
+            Logger.log(e);
+        }
+        return result;
+    }
+
+    /**
+     * Gets all the rows in a list of instances.
+     *
+     * @return a list of all the rows
+     */
+    public List<Instance> getRows()
+    {
+        try
+        {
+            List<Instance> ret = new ArrayList<Instance>();
+
+            pooledStatement.getConnection().enterBusyState();
+            if(resultEntity != null && !resultEntity.isRootEntity())
+            {
+                while(!resultSet.isAfterLast() && resultSet.next())
+                {
+                    Instance i = resultEntity.newInstance(new ReadOnlyMap(this), true);
+                    i.setClean();
+                    ret.add(i);
+                }
+            }
+            else
+            {
+                while(!resultSet.isAfterLast() && resultSet.next())
+                {
+                    Instance i = new Instance(new ReadOnlyMap(this), resultEntity == null ? null : resultEntity.getDB());
+                    ret.add(i);
+                }
+            }
+            return ret;
+        }
+        catch(SQLException sqle)
+        {
+            Logger.log(sqle);
+            return null;
+        }
+        finally
+        {
+            pooledStatement.getConnection().leaveBusyState();
+            pooledStatement.notifyOver();
+            isOver = true;
+        }
+    }
+
+    public List getScalars()
+    {
+        try
+        {
+            List ret = new ArrayList();
+
+            pooledStatement.getConnection().enterBusyState();
+            while(!resultSet.isAfterLast() && resultSet.next())
+            {
+                ret.add(resultSet.getObject(0));
+            }
+            return ret;
+        }
+        catch(SQLException sqle)
+        {
+            Logger.log(sqle);
+            return null;
+        }
+        finally
+        {
+            pooledStatement.getConnection().leaveBusyState();
+            pooledStatement.notifyOver();
+            isOver = true;
+        }
+    }
+
+    Set cachedSet = null;
+
+    /*  */
+    public Set<String> keySet()
+    {
+        try
+        {
+            if(cachedSet == null)
+            {
+                cachedSet = new HashSet<String>(SqlUtil.getColumnNames(resultSet));
+            }
+            return cachedSet;
+        }
+        catch(SQLException sqle)
+        {
+            Logger.log(sqle);
+            return null;
+        }
+    }
+
+    /*  */
+    public List<String> keyList()
+    {
+        try
+        {
+            return SqlUtil.getColumnNames(resultSet);
+        }
+        catch(SQLException sqle)
+        {
+            Logger.log(sqle);
+            return null;
+        }
+    }
+
+    /**
+     * Check if some data is available.
+     *
+     * @exception SQLException if the internal ResultSet is not happy
+     * @return <code>true</code> if some data is available (ie the internal
+     *     ResultSet is not empty, and not before first row neither after last
+     *     one)
+     */
+    private boolean dataAvailable() throws SQLException
+    {
+        boolean ret = false;
+
+        if(resultSet.isBeforeFirst())
+        {
+            try
+            {
+                pooledStatement.getConnection().enterBusyState();
+                ret = resultSet.next();
+                return ret;
+            }
+            finally
+            {
+                pooledStatement.getConnection().leaveBusyState();
+                if(!ret)
+                {
+                    pooledStatement.notifyOver();
+                    isOver = true;
+                }
+            }
+        }
+        ret = !resultSet.isAfterLast();
+        return ret;
+    }
+
+    /**
+     * Source statement.
+     */
+    private PooledStatement pooledStatement = null;
+
+    /**
+     * Wrapped result set.
+     */
+    private ResultSet resultSet = null;
+
+    /**
+     * Resulting entity.
+     */
+    private Entity resultEntity = null;
+
+    /** whether we did prefetch a row */
+    private boolean prefetch = false;
+
+    /** whether we reached the end */
+    private boolean isOver = false;
+}

Added: velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/package.html
URL: http://svn.apache.org/viewvc/velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/package.html?rev=1298906&view=auto
==============================================================================
--- velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/package.html (added)
+++ velocity/sandbox/velosurf/src/java/org/apache/velocity/velosurf/context/package.html Fri Mar  9 16:23:25 2012
@@ -0,0 +1,23 @@
+<html>
+<body bgcolor="white">
+
+Contains all classes that can be accessed from inside a Velocity template.
+
+<h2>Package Specification</h2>
+
+<!-- ANY SPECS NEEDED BY JAVA COMPATIBILITY KIT GO THERE 
+<ul>
+  <li><a href="">##### REFER TO ANY FRAMEMAKER SPECIFICATION HERE #####</a>
+</ul>
+
+<h2>Related Documentation</h2>
+
+For overviews, tutorials, examples, guides, and tool documentation, please see:
+<ul>
+  <li><a href="">##### REFER TO NON-SPEC DOCUMENTATION HERE #####</a>
+</ul>
+
+<!-- Put @see and @since tags down here. -->
+
+</body>
+</html>