Added: 
velocity/tools/branches/model/velocity-tools-model/src/main/java/org/apache/velocity/tools/model/impl/BaseModel.java
URL: 
http://svn.apache.org/viewvc/velocity/tools/branches/model/velocity-tools-model/src/main/java/org/apache/velocity/tools/model/impl/BaseModel.java?rev=1857755&view=auto
==============================================================================
--- 
velocity/tools/branches/model/velocity-tools-model/src/main/java/org/apache/velocity/tools/model/impl/BaseModel.java
 (added)
+++ 
velocity/tools/branches/model/velocity-tools-model/src/main/java/org/apache/velocity/tools/model/impl/BaseModel.java
 Thu Apr 18 15:04:54 2019
@@ -0,0 +1,1092 @@
+package org.apache.velocity.tools.model.impl;
+
+import org.apache.commons.beanutils.PropertyUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.velocity.app.VelocityEngine;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.tools.ClassUtils;
+import org.apache.velocity.tools.XmlUtils;
+import org.apache.velocity.tools.config.ConfigurationException;
+import org.apache.velocity.tools.model.Attribute;
+import org.apache.velocity.tools.model.Entity;
+import org.apache.velocity.tools.model.Instance;
+import org.apache.velocity.tools.model.Model;
+import org.apache.velocity.tools.model.WrappingInstance;
+import org.apache.velocity.tools.model.config.ConfigDigester;
+import org.apache.velocity.tools.model.config.ConfigHelper;
+import org.apache.velocity.tools.model.config.Constants;
+import org.apache.velocity.tools.model.filter.ValueFilterHandler;
+import org.apache.velocity.tools.model.filter.Identifiers;
+import org.apache.velocity.tools.model.sql.BasicDataSource;
+import org.apache.velocity.tools.model.sql.ConnectionPool;
+import org.apache.velocity.tools.model.sql.ConnectionWrapper;
+import org.apache.velocity.tools.model.sql.Credentials;
+import org.apache.velocity.tools.model.sql.DriverInfos;
+import org.apache.velocity.tools.model.sql.StatementPool;
+import org.apache.velocity.tools.model.util.Cryptograph;
+import org.w3c.dom.Element;
+import org.xml.sax.InputSource;
+
+import javax.naming.Context;
+import javax.naming.InitialContext;
+import javax.sql.DataSource;
+import javax.xml.parsers.DocumentBuilderFactory;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TreeMap;
+
+public abstract class BaseModel extends AttributeHolder implements Constants
+{
+    public BaseModel()
+    {
+    }
+
+    /*
+     * Configuration
+     */
+
+    public Model configure(Map params)
+    {
+        return configure(new ConfigHelper(params));
+    }
+
+    protected Model configure(ConfigHelper config)
+    {
+        try
+        {
+            setWriteAccess(config.getEnum(MODEL_WRITE_ACCESS, 
getWriteAccess()));
+            setReverseMode(config.getEnum(MODEL_REVERSE_MODE, 
getReverseMode()));
+            
Optional.ofNullable(config.getVelocityEngine()).ifPresent(this::setVelocityEngine);
+            
Optional.ofNullable(config.getString(MODEL_SCHEMA)).ifPresent(this::setSchema);
+            
Optional.ofNullable(config.getString(MODEL_IDENTIFIERS_INFLECTOR)).ifPresent(getIdentifiers()::setInflector);
+            Object flatMapping = config.get(MODEL_IDENTIFIERS_MAPPING);
+            if (flatMapping != null)
+            {
+                if (flatMapping instanceof String)
+                {
+                    getIdentifiers().setMapping((String)flatMapping);
+                }
+                else if(flatMapping instanceof Map)
+                {
+                    getIdentifiers().setMapping((Map)flatMapping);
+                }
+                else
+                {
+                    throw new ConfigurationException("expecting a string or a 
map for property " + MODEL_IDENTIFIERS_MAPPING);
+                }
+            }
+            
getIdentifiers().setMapping(config.getSubProperties(MODEL_IDENTIFIERS_MAPPING));
+            
getFilters().setReadMapping(config.getSubProperties(MODEL_FILTERS_READ));
+            
getFilters().setWriteMapping(config.getSubProperties(MODEL_FILTERS_WRITE));
+            Object dataSource = config.get(MODEL_DATASOURCE);
+            if (dataSource != null)
+            {
+                try
+                {
+                    if (dataSource instanceof String)
+                    {
+                        setDataSource((String)dataSource);
+                    }
+                    else if (dataSource instanceof DataSource)
+                    {
+                        setDataSource((DataSource)dataSource);
+                    }
+                }
+                catch (Exception e)
+                {
+                    throw new ConfigurationException("could not set model 
datasource", e);
+                }
+            }
+
+            
Optional.ofNullable(config.getString(MODEL_FILTERS_CRYPTOGRAPH)).ifPresent(getFilters()::setCryptographClass);
+
+            
Optional.ofNullable(config.get(MODEL_INSTANCES_FACTORY)).ifPresent(getInstances()::setFactory);
+            
Optional.ofNullable(config.getSubProperties(MODEL_INSTANCES_CLASSES)).ifPresent(getInstances()::setClasses);
+
+            
Optional.ofNullable(config.getString(MODEL_DATABASE)).ifPresent(this::setDatabaseURL);
+            
Optional.ofNullable(config.getString(MODEL_CREDENTIALS_USER)).ifPresent(getCredentials()::setUser);
+            
Optional.ofNullable(config.getString(MODEL_CREDENTIALS_PASSWORD)).ifPresent(getCredentials()::setPassword);
+
+            String path = config.getString(MODEL_DEFINITION);
+            boolean useDefault = false;
+            if (path == null)
+            {
+                URL definition = getDefinition();
+                if (definition == null)
+                {
+                    useDefault = true;
+                    path = MODEL_DEFAULT_PATH;
+                }
+            }
+            if (path != null)
+            {
+                try
+                {
+                    setDefinition(config.findURL(path));
+                }
+                catch (ConfigurationException ce)
+                {
+                    if (!useDefault)
+                    {
+                        throw ce;
+                    }
+                }
+            }
+            return getModel();
+        }
+        catch (RuntimeException re)
+        {
+            throw re;
+        }
+        catch (Exception e)
+        {
+            throw new ConfigurationException("configuration problem", e);
+        }
+    }
+
+    public NavigableMap<String, Attribute> getConfig()
+    {
+        return new TreeMap(); // TODO
+    }
+
+    /*
+     * Initialization
+     */
+
+    public Model initialize()
+    {
+        return initialize(getDefinition());
+    }
+
+    public Model initialize(URL url)
+    {
+        return initialize("default", url);
+    }
+
+    public Model initialize(String id, URL url)
+    {
+        try
+        {
+            if (url == null)
+            {
+                initialize(id, (Reader)null);
+            }
+            else
+            {
+                setDefinition(url);
+                Reader reader = new InputStreamReader(url.openStream());
+                InputSource source = new InputSource(reader);
+                source.setSystemId(url.toExternalForm());
+                initialize(id, source);
+            }
+        }
+        catch (IOException ioe)
+        {
+            throw new VelocityException("could not initialize model", ioe);
+        }
+        return getModel();
+    }
+
+    public Model initialize(String id, String path)
+    {
+        return initialize(id, new ConfigHelper().findURL(path));
+    }
+
+    public Model initialize(String id)
+    {
+        return initialize(id, getDefinition());
+    }
+
+    public Model initialize(Reader reader)
+    {
+        return initialize("default", new InputSource(reader));
+    }
+
+    public Model initialize(String id, Reader reader) throws 
ConfigurationException
+    {
+        return initialize(id, reader == null ? null : new InputSource(reader));
+
+    }
+
+    public Model initialize(String id, InputSource source) throws 
ConfigurationException
+    {
+        this.modelId = id;
+        try
+        {
+            readDefinition(source);
+            connect();
+            getIdentifiers().initialize();
+            getFilters().initialize();
+            reverseEngineer();
+            getInstances().initialize();
+            initializeAttributes();
+            modelRepository.put(id, getModel());
+        }
+        catch (ConfigurationException ce)
+        {
+            throw ce;
+        }
+        catch (Exception e)
+        {
+            throw new ConfigurationException("could not initialize model", e);
+        }
+        return getModel();
+    }
+
+    protected void readDefinition(InputSource source) throws Exception
+    {
+        if (source == null)
+        {
+            return;
+        }
+        DocumentBuilderFactory builderFactory = 
XmlUtils.createDocumentBuilderFactory();
+        builderFactory.setXIncludeAware(true);
+        Element doc = 
builderFactory.newDocumentBuilder().parse(source).getDocumentElement();
+        String rootTag = doc.getTagName();
+        // support the deprecated 'database' root tag
+        if ("database".equals(rootTag))
+        {
+            getLogger().warn("<database> root tag has been deprecated in favor 
of <model>");
+        }
+        else if (!"model".equals(rootTag))
+        {
+            throw new ConfigurationException("expecting a <model> root tag");
+        }
+        new ConfigDigester(doc, this).process();
+    }
+
+    protected void connect() throws Exception
+    {
+        if (dataSource == null)
+        {
+            if (databaseURL == null)
+            {
+                throw new ConfigurationException("cannot connect: no data 
source");
+            }
+            else
+            {
+                setDataSource(new BasicDataSource(databaseURL));
+            }
+        }
+        // override driver properties deduced from database metadata
+        // with properties provided by the user
+        Connection connection = dataSource.getConnection();
+        Properties props = 
ReverseEngineer.getStockDriverProperties(connection.getMetaData().getURL());
+        DriverInfos stockInfos = new DriverInfos();
+        ConfigDigester.setProperties(this, props);
+        getDriverInfos().setDefaults(stockInfos);
+        connectionPool = new ConnectionPool(dataSource, credentials, 
driverInfos, schema, true, maxConnections);
+        transactionConnectionPool = new ConnectionPool(dataSource, 
credentials, driverInfos, schema, false, maxConnections);
+        statementPool = new StatementPool(connectionPool);
+    }
+
+    /*
+     * Getters and setters
+     */
+
+    public String getModelId()
+    {
+        return modelId;
+    }
+
+    public WriteAccess getWriteAccess()
+    {
+        return writeAccess;
+    }
+
+    public Model setWriteAccess(WriteAccess writeAccess)
+    {
+        this.writeAccess = writeAccess;
+        return getModel();
+    }
+
+    public ReverseMode getReverseMode()
+    {
+        return reverseMode;
+    }
+
+    public Model setReverseMode(ReverseMode reverseMode)
+    {
+        this.reverseMode = reverseMode;
+        return getModel();
+    }
+
+    public VelocityEngine getVelocityEngine()
+    {
+        return velocityEngine;
+    }
+
+    public Model setVelocityEngine(VelocityEngine velocityEngine)
+    {
+        this.velocityEngine = velocityEngine;
+        return getModel();
+    }
+
+    public Model setDataSource(String dataSourceName) throws Exception
+    {
+        Context ctx = InitialContext.doLookup("java:comp/env");
+        DataSource dataSource = (DataSource)ctx.lookup(dataSourceName);
+        return setDataSource(dataSource);
+    }
+
+    public Model setDataSource(DataSource dataSource) throws Exception
+    {
+        if (this.dataSource != null)
+        {
+            throw new ConfigurationException("data source cannot be changed 
(no dynamic reloading)");
+        }
+        this.dataSource = dataSource;
+        return getModel();
+    }
+
+    public String getDatabaseURL()
+    {
+        return databaseURL;
+    }
+
+    public Model setDatabaseURL(String databaseURL)
+    {
+        this.databaseURL = databaseURL;
+        return getModel();
+    }
+
+    public String getSchema()
+    {
+        return schema;
+    }
+
+    public Model setSchema(String schema)
+    {
+        this.schema = schema;
+        if (connectionPool != null)
+        {
+            connectionPool.setSchema(schema);
+        }
+        return getModel();
+    }
+
+    public URL getDefinition()
+    {
+        return definition;
+    }
+
+    public Model setDefinition(String path) throws MalformedURLException
+    {
+        if (path != null && path.contains("://"))
+        {
+            setDefinition(new URL(path));
+        }
+        return getModel();
+    }
+
+    public Model setDefinition(URL definition)
+    {
+        this.definition = definition;
+        return getModel();
+    }
+
+    public Credentials getCredentials()
+    {
+        return credentials;
+    }
+
+    public Identifiers getIdentifiers()
+    {
+        return identifiers;
+    }
+
+    public FiltersSet getFilters()
+    {
+        return filters;
+    }
+
+    public Entity getEntity(String name)
+    {
+        return entitiesMap.get(name);
+    }
+
+    public DriverInfos getDriverInfos()
+    {
+        return driverInfos;
+    }
+
+    protected ConnectionPool getConnectionPool()
+    {
+        return connectionPool;
+    }
+
+    protected StatementPool getStatementPool()
+    {
+        return statementPool;
+    }
+
+    protected ConnectionWrapper getTransactionConnection() throws SQLException
+    {
+        return transactionConnectionPool.getConnection();
+    }
+
+    public NavigableMap<String, Entity> getEntities()
+    {
+        return Collections.unmodifiableNavigableMap(entitiesMap);
+    }
+
+    protected UserInstancesConfig getInstances()
+    {
+        return userInstancesConfig;
+    }
+
+    /*
+     * Definition
+     */
+
+    public void addEntity(Entity entity)
+    {
+        entitiesMap.put(entity.getName(), entity);
+    }
+
+    protected void reverseEngineer() throws SQLException
+    {
+        if (connectionPool == null)
+        {
+            getLogger().warn("connection pool not available: not performing 
reverse enginering");
+            return;
+        }
+        ConnectionWrapper connection = null;
+        try
+        {
+            connection = connectionPool.getConnection();
+            connection.enterBusyState();
+            ReverseEngineer reverseEngineer = new 
ReverseEngineer(connection.getMetaData(), driverInfos);
+
+            // adapt known entities table case if necessary
+            for (Entity entity : entitiesMap.values())
+            {
+                String table = entity.getTable();
+                if (table == null) table = entity.getName();
+                table = driverInfos.getTableName(table);
+                entity.setTable(table);
+            }
+
+            if (getReverseMode().reverseColumns())
+            {
+                // build a temporary map of declared entities per tables
+                Map<String, Entity> knownEntitiesByTable = new TreeMap<>();
+                for (Entity entity : entitiesMap.values())
+                {
+                    Entity prev = knownEntitiesByTable.put(entity.getTable(), 
entity);
+                    if (prev != null)
+                    {
+                        throw new ConfigurationException("entity table name 
collision: entities " + entity.getName() + " and " + prev.getName() + " both 
reference table " + entity.getTable());
+                    }
+                }
+
+                // reverse enginering of tables if asked so
+                if (getReverseMode().reverseTables())
+                {
+                    for (String table : reverseEngineer.getTables())
+                    {
+                        Entity entity = knownEntitiesByTable.get(table);
+                        if (entity == null)
+                        {
+                            String entityName = 
getIdentifiers().transformTableName(table);
+                            entity = getEntity(entityName);
+                            if (entity != null)
+                            {
+                                if (entity.getTable() != null)
+                                {
+                                    throw new ConfigurationException("entity 
table name collision: entity " + entity.getName() + " maps both tables " + 
entity.getTable() + " and " + table);
+                                }
+                                getLogger().warn("binding entity {} to table 
{}", entity.getName(), table);
+                                entity.setTable(table);
+                                knownEntitiesByTable.put(table, entity);
+                            }
+                            else
+                            {
+                                entity = new Entity(entityName, getModel());
+                                entity.setTable(table);
+                                addEntity(entity);
+                                knownEntitiesByTable.put(table, entity);
+                            }
+                        }
+                    }
+                }
+
+                // reverse enginering of columns and primary key
+                for (Entity entity : knownEntitiesByTable.values())
+                {
+                    List<Entity.Column> columns = 
reverseEngineer.getColumns(entity);
+                    for (Entity.Column column : columns)
+                    {
+                        entity.addColumn(column);
+                    }
+                    
entity.setSqlPrimaryKey(reverseEngineer.getPrimaryKey(entity));
+                }
+
+                // reverse enginering of joins, if asked so
+                Map<Entity, List<Pair<Entity, List<String>>>> 
potentialJoinTables = new HashMap<>();
+                if (getReverseMode().reverseJoins())
+                {
+                    for (Entity pkEntity : knownEntitiesByTable.values())
+                    {
+                        List<Pair<String, List<String>>> joins = 
reverseEngineer.getJoins(pkEntity);
+                        for (Pair<String, List<String>> join : joins)
+                        {
+                            String fkTable = join.getLeft();
+                            List<String> fkColumns = join.getRight();
+                            Entity fkEntity = 
knownEntitiesByTable.get(fkTable);
+                            if (fkEntity != null)
+                            {
+                                // define upstream attribute from fk to pk
+                                declareUpstreamJoin(pkEntity, fkEntity, 
fkColumns);
+
+                                // define downstream attribute from pk to fk
+                                declareJoinTowardsForeignKey(pkEntity, 
fkEntity, fkColumns);
+
+                                List<Pair<Entity, List<String>>> fks = 
potentialJoinTables.get(fkEntity);
+                                if (fks == null)
+                                {
+                                    fks = new ArrayList<Pair<Entity, 
List<String>>>();
+                                    potentialJoinTables.put(fkEntity, fks);
+                                }
+                                fks.add(Pair.of(pkEntity, fkColumns));
+                            }
+                        }
+                    }
+                    // reverse enginering of join tables
+                    if (getReverseMode().reverseExtended())
+                    {
+                        for (Map.Entry<Entity, List<Pair<Entity, 
List<String>>>> entry : potentialJoinTables.entrySet())
+                        {
+                            Entity fkEntity = entry.getKey();
+                            List<Pair<Entity, List<String>>> pks = 
entry.getValue();
+                            // TODO - joins detection should be configurable
+                            // for now:
+                            // - join table must reference two different 
tables with distinct columns
+                            // - join table name must be a snake case 
concatenation of both pk tables
+                            if (pks.size() == 2)
+                            {
+                                List<String> leftFkColumns = 
pks.get(0).getRight();
+                                List<String> rightFkColumns = 
pks.get(1).getRight();
+                                if (Collections.disjoint(leftFkColumns, 
rightFkColumns))
+                                {
+                                    Entity leftPK = pks.get(0).getLeft();
+                                    Entity rightPK = pks.get(1).getLeft();
+                                    String name1 = leftPK.getName() + "_" + 
rightPK.getName();
+                                    String name2 = rightPK.getName() + "_" + 
leftPK.getName();
+                                    if (fkEntity.getName().equals(name1) || 
fkEntity.getName().equals(name2))
+                                    {
+                                        declareExtendedJoin(leftPK, 
leftFkColumns, fkEntity, rightFkColumns, rightPK);
+                                        declareExtendedJoin(rightPK, 
rightFkColumns, fkEntity, leftFkColumns, leftPK);
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        finally
+        {
+            if (connection != null)
+            {
+                connection.leaveBusyState();
+            }
+        }
+    }
+
+    private void declareUpstreamJoin(Entity pkEntity, Entity fkEntity, 
List<String> fkColumns) throws SQLException
+    {
+        String upstreamAttributeName;
+        if (fkColumns.size() == 1)
+        {
+            // take fk column name with _id suffix stripped out
+            upstreamAttributeName = fkColumns.get(0).toLowerCase(Locale.ROOT);
+            if (upstreamAttributeName.length() > 3 && 
upstreamAttributeName.endsWith("_id"))
+            {
+                upstreamAttributeName = upstreamAttributeName.substring(0, 
upstreamAttributeName.length() - 3);
+            }
+            // except if the resulting is an abbreviation of the target pk 
entity name
+            if (pkEntity.getName().startsWith(upstreamAttributeName))
+            {
+                upstreamAttributeName = pkEntity.getName();
+            }
+        }
+        else
+        {
+            upstreamAttributeName = pkEntity.getName(); // hope it's a singular
+        }
+        Attribute previous = fkEntity.getAttribute(upstreamAttributeName);
+        if (previous != null)
+        {
+            getLogger().warn("explicit declaration of attribute {}.{} 
supersedes implicit imported key from {}", fkEntity.getName(), 
upstreamAttributeName, pkEntity.getName());
+        }
+        else
+        {
+            fkEntity.declareUpstreamJoin(upstreamAttributeName, pkEntity, 
fkColumns);
+        }
+    }
+
+    private void declareJoinTowardsForeignKey(Entity pkEntity, Entity 
fkEntity, List<String> fkColumns) throws SQLException
+    {
+        String downstreamAttributeName = 
getIdentifiers().pluralize(fkEntity.getName());
+        Attribute previous = pkEntity.getAttribute(downstreamAttributeName);
+        if (previous != null)
+        {
+            getLogger().warn("explicit declaration of attribute {}.{} 
supersedes implicit exported key towards {}", pkEntity.getName(), 
downstreamAttributeName, fkEntity.getName());
+        }
+        else
+        {
+            pkEntity.declareDownstreamJoin(downstreamAttributeName, fkEntity, 
fkColumns);
+        }
+    }
+
+    private void declareExtendedJoin(Entity leftEntity, List<String> 
leftFKCols, Entity joinEntity, List<String> rightFKCols, Entity rightEntity) 
throws SQLException
+    {
+        String joinAttributeName = 
getIdentifiers().pluralize(rightEntity.getName());
+        Attribute previous = leftEntity.getAttribute(joinAttributeName);
+        if (previous != null)
+        {
+            getLogger().warn("explicit declaration of attribute {}.{} 
supersedes implicit extended join {}", leftEntity.getName(), joinAttributeName, 
rightEntity.getName());
+        }
+        else
+        {
+            leftEntity.declareExtendedJoin(joinAttributeName, leftFKCols, 
joinEntity, rightFKCols, rightEntity);
+        }
+    }
+
+    /*
+     * Operations
+     */
+
+    public static Model getModel(String id)
+    {
+        Model ret = modelRepository.get(id);
+        if (ret == null)
+        {
+            throw new ConfigurationException("model id not found: " + id);
+        }
+        return ret;
+    }
+
+    protected final String quoteIdentifier(String identifier)
+    {
+        return driverInfos.quoteIdentifier(identifier);
+    }
+
+    /*
+     * Helper Classes
+     */
+
+    /**
+     * <p>gather filters getters and setters in a subclass to ease 
configuration</p>
+     * <p>Can be configured after initialization.</p>
+     */
+    public class FiltersSet
+    {
+        public FiltersSet()
+        {
+            readFilters = new ValueFilterHandler("filters.read");
+            writeFilters = new ValueFilterHandler("filters.write");
+        }
+
+        public ValueFilterHandler getReadFilters()
+        {
+            return readFilters;
+        }
+
+        public Model setReadMapping(Map filters) throws Exception
+        {
+            readFilters.setMapping(filters);
+            return getModel();
+        }
+
+        public ValueFilterHandler getWriteFilters()
+        {
+            return writeFilters;
+        }
+
+        public Model setWriteMapping(Map filters) throws Exception
+        {
+            writeFilters.setMapping(filters);
+            return getModel();
+        }
+
+        public final Model setCryptographClass(String cryptographClass)
+        {
+            this.cryptographClass = cryptographClass;
+            return getModel();
+        }
+
+        protected final void initialize()
+        {
+            if (readFilters.needsCryptograph() || 
writeFilters.needsCryptograph())
+                try
+                {
+                    Cryptograph cryptograph = initCryptograph();
+                    readFilters.setCryptograph(cryptograph);
+                    writeFilters.setCryptograph(cryptograph);
+                }
+                catch (RuntimeException re)
+                {
+                    throw re;
+                }
+                catch (Exception e)
+                {
+                    throw new ConfigurationException("could not initialize 
cryptograph", e);
+                }
+        }
+
+        private final Cryptograph initCryptograph() throws Exception
+        {
+            if (cryptographClass == null)
+            {
+                throw new ConfigurationException("no cryptograph classname 
found in filters.cryptograph");
+            }
+            Class clazz = ClassUtils.getClass(cryptographClass);
+            Cryptograph cryptograph = (Cryptograph)clazz.newInstance();
+            String secret = getSecret();
+            if (secret == null)
+            {
+                throw new ConfigurationException("no cryptograph secret: 
either definition file or database url must be provided");
+            }
+            cryptograph.init(getSecret());
+            return cryptograph;
+        }
+
+        private final String getSecret()
+        {
+            return Optional.ofNullable(
+                getDefinition()).map(x -> String.valueOf(x)).
+                orElse(Optional.ofNullable(getDatabaseURL()).filter(x -> 
x.length() >= 16).
+                    orElse("sixteen chars..."));
+        }
+
+        private String cryptographClass = null;
+    }
+
+    protected class UserInstancesConfig
+    {
+
+        protected Class getFactory()
+        {
+            return factory;
+        }
+
+        public Model setFactory(Object factory)
+        {
+            if (factory == null || factory instanceof Class)
+            {
+                this.factory = (Class)factory;
+            }
+            else if (factory instanceof String)
+            {
+                try
+                {
+                    this.factory = ClassUtils.getClass((String)factory);
+                }
+                catch (ClassNotFoundException cnfe)
+                {
+                    throw new ConfigurationException("cannot get instances 
factory", cnfe);
+                }
+            }
+            else
+            {
+                throw new ConfigurationException("expecting factory class or 
classname");
+            }
+            return getModel();
+        }
+
+        protected Map<String, Class> getClasses()
+        {
+            return classes;
+        }
+
+        public Model setClasses(Map<String, ?> classes)
+        {
+            this.classes = new TreeMap<String, Class>();
+            try
+            {
+                for (Map.Entry<String, ?> entry : classes.entrySet())
+                {
+                    String key = entry.getKey();
+                    Object value = entry.getValue();
+                    Class clazz = null;
+                    if (value instanceof String)
+                    {
+                        clazz = ClassUtils.getClass((String)value);
+                    }
+                    else if (value instanceof Class)
+                    {
+                        clazz = (Class)value;
+                    }
+                    this.classes.put(key, clazz);
+                }
+            }
+            catch (ClassNotFoundException cnfe)
+            {
+                throw new ConfigurationException("could not build instances 
classes map", cnfe);
+            }
+            return getModel();
+        }
+
+        protected void initialize()
+        {
+            Set<String> classProvided = new HashSet<String>();
+            if (classes != null)
+            {
+                for (Map.Entry<String, Class> entry : classes.entrySet())
+                {
+                    String key = entry.getKey();
+                    final Entity entity = getEntity(key);
+                    final Class clazz = entry.getValue();
+                    if (entity == null)
+                    {
+                        throw new ConfigurationException("instance.classes." + 
key + ": no entity named " + key);
+                    }
+                    if (Instance.class.isAssignableFrom(clazz))
+                    {
+                        try
+                        {
+                            final Constructor ctor = 
clazz.getDeclaredConstructor(Entity.class);
+                            // ctor.setAccessible(true);
+                            entity.setInstanceBuilder(() ->
+                            {
+                                try
+                                {
+                                    return (Instance)ctor.newInstance(entity);
+                                }
+                                catch (IllegalAccessException | 
InstantiationException | InvocationTargetException e)
+                                {
+                                    throw new RuntimeException("could not 
create instance of class " + clazz.getName());
+                                }
+                            });
+
+                        }
+                        catch (NoSuchMethodException nsme)
+                        {
+                            throw new ConfigurationException("Class " + 
clazz.getName() + " must declare a public ctor taking an Entity as argument");
+                        }
+                    }
+                    else
+                    {
+                        entity.setInstanceBuilder(() ->
+                        {
+                            try
+                            {
+                                Object obj = clazz.newInstance();
+                                return new WrappingInstance(entity, obj);
+                            }
+                            catch (InstantiationException | 
IllegalAccessException e)
+                            {
+                                throw new RuntimeException("could not create 
instance of class " + clazz.getName());
+                            }
+                        }, PropertyUtils.getPropertyDescriptors(clazz));
+
+                    }
+                    classProvided.add(key);
+                }
+            }
+            if (factory != null)
+            {
+                for (final Entity entity : entitiesMap.values())
+                {
+                    if (!classProvided.contains(entity.getName()))
+                    {
+                        Method method = null;
+                        String capitalized = 
StringUtils.capitalize(entity.getName());
+                        for (String prefix : factoryMethodPrefixes)
+                        {
+                            try
+                            {
+                                method = factory.getMethod(prefix + 
capitalized);
+                            }
+                            catch (NoSuchMethodException e)
+                            {
+                            }
+                        }
+                        if (method != null)
+                        {
+                            // let's try it
+                            Object obj;
+                            try
+                            {
+                                obj = method.invoke(null);
+                            }
+                            catch (IllegalAccessException | 
InvocationTargetException e)
+                            {
+                                throw new ConfigurationException("factory 
instance creation failed for entity " + entity.getName(), e);
+                            }
+                            if (obj == null)
+                            {
+                                throw new ConfigurationException("factory 
instance creation returned null for entity " + entity.getName());
+                            }
+                            Class clazz = obj.getClass();
+                            final Method creationMethod = method;
+                            if (Instance.class.isAssignableFrom(clazz))
+                            {
+                                entity.setInstanceBuilder(() ->
+                                {
+                                    try
+                                    {
+                                        return 
(Instance)creationMethod.invoke(null);
+                                    }
+                                    catch (IllegalAccessException | 
InvocationTargetException e)
+                                    {
+                                        throw new RuntimeException("could not 
create instance of class " + clazz.getName());
+                                    }
+                                });
+                            }
+                            else
+                            {
+                                entity.setInstanceBuilder(() ->
+                                {
+                                    try
+                                    {
+                                        return new WrappingInstance(entity, 
creationMethod.invoke(null));
+                                    }
+                                    catch (IllegalAccessException | 
InvocationTargetException e)
+                                    {
+                                        throw new RuntimeException("could not 
create instance of class " + clazz.getName());
+                                    }
+                                });
+
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        private Class factory = null;
+
+        private Map<String, Class> classes = null;
+    }
+
+    private String[] factoryMethodPrefixes = { "create", "new", "get" };
+
+    /*
+     * Members
+     */
+
+    private String modelId = null;
+
+    public enum WriteAccess { NONE, JAVA, VTL }
+
+    private WriteAccess writeAccess = WriteAccess.JAVA;
+
+    public enum ReverseMode
+    {
+        NONE, COLUMNS, TABLES, JOINS, FULL, EXTENDED;
+
+        public boolean reverseColumns()
+        {
+            return ordinal() > 0;
+        }
+
+        public boolean reverseTables()
+        {
+            return ordinal() == 2 || ordinal() > 3;
+        }
+
+        public boolean reverseJoins()
+        {
+            return ordinal() > 2;
+        }
+        public boolean reverseExtended()
+        {
+            return ordinal() == 5;
+        }
+    }
+
+    private ReverseMode reverseMode = ReverseMode.NONE;
+
+    private VelocityEngine velocityEngine = null;
+
+    private String schema = null;
+
+    /**
+     * driver properties
+     */
+    private DriverInfos driverInfos = new DriverInfos();
+
+    /**
+     * Entities map
+     */
+    private NavigableMap<String, Entity> entitiesMap = new TreeMap<>();
+
+    /**
+     * Definition file URL
+     */
+    private URL definition = null;
+
+    /**
+     * Data source
+     */
+    private transient DataSource dataSource = null;
+
+    private String databaseURL = null;
+
+    /**
+     * Pool of connections.
+     */
+    private transient ConnectionPool connectionPool = null;
+
+    /**
+     * Max connections.
+     */
+    private int maxConnections = 50; // applies to connectionPool and 
transactionConnectionPool
+
+    /**
+     * Pool of connections for transactions.
+     */
+    private transient ConnectionPool transactionConnectionPool = null;
+
+    /**
+     * Pool of prepared statements.
+     */
+    private transient StatementPool statementPool = null;
+
+    private transient Credentials credentials = new Credentials();
+
+    /**
+     * Identifiers mapper
+     */
+    private Identifiers identifiers = new Identifiers();
+
+    /**
+     * Value filters
+     */
+
+    protected ValueFilterHandler readFilters = null;
+
+    protected ValueFilterHandler writeFilters = null;
+
+    private FiltersSet filters = new FiltersSet();
+
+    private UserInstancesConfig userInstancesConfig = new 
UserInstancesConfig();
+
+    /**
+     * Model repository
+     */
+    private static Map<String, Model> modelRepository = new HashMap<>();
+}

Added: 
velocity/tools/branches/model/velocity-tools-model/src/main/java/org/apache/velocity/tools/model/impl/InstanceProducer.java
URL: 
http://svn.apache.org/viewvc/velocity/tools/branches/model/velocity-tools-model/src/main/java/org/apache/velocity/tools/model/impl/InstanceProducer.java?rev=1857755&view=auto
==============================================================================
--- 
velocity/tools/branches/model/velocity-tools-model/src/main/java/org/apache/velocity/tools/model/impl/InstanceProducer.java
 (added)
+++ 
velocity/tools/branches/model/velocity-tools-model/src/main/java/org/apache/velocity/tools/model/impl/InstanceProducer.java
 Thu Apr 18 15:04:54 2019
@@ -0,0 +1,49 @@
+package org.apache.velocity.tools.model.impl;
+
+import org.apache.velocity.tools.model.Entity;
+import org.apache.velocity.tools.model.Instance;
+import org.apache.velocity.tools.model.Model;
+
+public class InstanceProducer
+{
+    protected InstanceProducer(Model model, Entity resultEntity)
+    {
+        this.model = model;
+        this.resultEntity = resultEntity;
+    }
+
+    protected InstanceProducer(Model model)
+    {
+        this(model, null);
+    }
+
+    protected InstanceProducer(Entity resultEntity)
+    {
+        this(resultEntity.getModel(), resultEntity);
+    }
+
+    protected Model getModel()
+    {
+        return model;
+    }
+
+    protected Entity getResultEntity()
+    {
+        return resultEntity;
+    }
+
+    protected void setResultEntity(Entity resultEntity)
+    {
+        this.resultEntity = resultEntity;
+    }
+
+    protected Instance newResultInstance()
+    {
+        return resultEntity == null ?
+            new Instance(getModel()) :
+            resultEntity.newInstance();
+    }
+
+    private Model model = null;
+    private Entity resultEntity = null;
+}

Added: 
velocity/tools/branches/model/velocity-tools-model/src/main/java/org/apache/velocity/tools/model/impl/ReverseEngineer.java
URL: 
http://svn.apache.org/viewvc/velocity/tools/branches/model/velocity-tools-model/src/main/java/org/apache/velocity/tools/model/impl/ReverseEngineer.java?rev=1857755&view=auto
==============================================================================
--- 
velocity/tools/branches/model/velocity-tools-model/src/main/java/org/apache/velocity/tools/model/impl/ReverseEngineer.java
 (added)
+++ 
velocity/tools/branches/model/velocity-tools-model/src/main/java/org/apache/velocity/tools/model/impl/ReverseEngineer.java
 Thu Apr 18 15:04:54 2019
@@ -0,0 +1,270 @@
+package org.apache.velocity.tools.model.impl;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.velocity.tools.config.ConfigurationException;
+import org.apache.velocity.tools.model.Entity;
+import org.apache.velocity.tools.model.filter.Identifiers;
+import org.apache.velocity.tools.model.sql.DriverInfos;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.sql.DatabaseMetaData;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ReverseEngineer
+{
+    protected static Logger logger = 
LoggerFactory.getLogger(ReverseEngineer.class);
+
+    private static final String STOCK_DRIVERS_PATH = 
"org/apache/velocity/tools/model/drivers/";
+
+    public ReverseEngineer(DatabaseMetaData databaseMetaData, DriverInfos 
driverInfos)
+    {
+        this.databaseMetaData = databaseMetaData;
+        this.driverInfos = driverInfos;
+    }
+
+    public String getCatalog() throws SQLException
+    {
+        return databaseMetaData.getConnection().getCatalog();
+    }
+
+    public String getSchema() throws SQLException
+    {
+        return databaseMetaData.getConnection().getSchema();
+    }
+
+    public static Properties getStockDriverProperties(String url) throws 
IOException, SQLException
+    {
+        // TODO - instead of relying on generic.properties, try to deduce the 
maximum from the metadata when vendor is unknown
+        Properties stockDriverProps = null;
+        Matcher matcher = Pattern.compile("^jdbc:([^:]+):").matcher(url);
+        if (matcher.find())
+        {
+            String vendor = matcher.group(1);
+            // search in stock driver properties
+            InputStream is = 
ReverseEngineer.class.getClassLoader().getResourceAsStream(STOCK_DRIVERS_PATH + 
vendor + ".properties");
+            if (is == null)
+            {
+                logger.error("no stock driver properties for vendor tag {}", 
vendor);
+            }
+            else
+            {
+                stockDriverProps = new Properties();
+                stockDriverProps.load(is);
+            }
+        }
+        else
+        {
+            logger.error("could not determine JDBC database vendor tag");
+        }
+        if (stockDriverProps == null)
+        {
+            logger.info("using generic driver properties");
+            InputStream is = 
ReverseEngineer.class.getClassLoader().getResourceAsStream(STOCK_DRIVERS_PATH + 
"generic.properties");
+            if (is == null)
+            {
+                throw new ConfigurationException("drivers/generic.properties 
not found in classpath");
+            }
+            stockDriverProps = new Properties();
+            stockDriverProps.load(is);
+        }
+        return stockDriverProps;
+    }
+
+    public List<String> getTables() throws SQLException
+    {
+        List<String> ret = new ArrayList<String>();
+        ResultSet tables = null;
+        try
+        {
+            //tables = databaseMetaData.getTables(getCatalog(), getSchema(), 
null, new String[] { "TABLE", "VIEW" });
+            tables = databaseMetaData.getTables(null, null, null, new String[] 
{ "TABLE", "VIEW" });
+            while (tables.next())
+            {
+                String tableName = tables.getString("TABLE_NAME");
+                String tableType = tables.getString("TABLE_TYPE");
+                if (!"SYSTEM TABLE".equals(tableType) && !"SYSTEM 
VIEW".equals(tableType) && !driverInfos.ignoreTable(tableName))
+                {
+                    ret.add(tableName);
+                }
+            }
+        }
+        finally
+        {
+            if (tables != null)
+            {
+                tables.close();
+            }
+        }
+        return ret;
+    }
+
+    public List<Entity.Column> getColumns(Entity entity) throws SQLException
+    {
+        Identifiers identifiers = entity.getModel().getIdentifiers();
+        List<Entity.Column> ret = new ArrayList<>();
+        ResultSet columns = null;
+        try
+        {
+            // get columns
+            String table = entity.getTable();
+            columns = databaseMetaData.getColumns(getCatalog(), getSchema(), 
table, null);
+            while (columns.next())
+            {
+                Integer size = columns.getInt("COLUMN_SIZE");
+                if (columns.wasNull()) size = null;
+                String colSqlName = columns.getString("COLUMN_NAME");
+                String colName = identifiers.transformColumnName(table, 
colSqlName);
+                int dataType = columns.getInt("DATA_TYPE");
+                String gen1 = columns.getString("IS_AUTOINCREMENT");
+                String gen2 = columns.getString("IS_GENERATEDCOLUMN");
+                boolean generated = "YES".equals(gen1) || "YES".equals(gen2);
+                ret.add(new Entity.Column(colName, colSqlName, dataType, size, 
generated));
+            }
+            return ret;
+        }
+        finally
+        {
+            if (columns != null)
+            {
+                columns.close();
+            }
+        }
+    }
+
+    public String[] getPrimaryKey(Entity entity) throws SQLException
+    {
+        ArrayList<String> keyColumns = new ArrayList<String>();
+        ResultSet columns = null;
+        try
+        {
+            // get primary key
+            String table = entity.getTable();
+            columns = databaseMetaData.getPrimaryKeys(getCatalog(), 
getSchema(), table);
+            while (columns.next())
+            {
+                short ord = columns.getShort("KEY_SEQ");
+                String columnName = columns.getString("COLUMN_NAME");
+                while (keyColumns.size() < ord)
+                {
+                    keyColumns.add(null);
+                }
+                keyColumns.set(ord - 1, columnName);
+            }
+            return keyColumns.toArray(new String[keyColumns.size()]);
+        }
+        finally
+        {
+            if (columns != null)
+            {
+                columns.close();
+            }
+        }
+    }
+
+    public List<Pair<String, List<String>>> getJoins(Entity pkEntity) throws 
SQLException
+    {
+        List<Pair<String, List<String>>> joins = new ArrayList<>();
+        List<String> knownPK = pkEntity.getSqlPrimaryKey();
+        if (knownPK == null || knownPK.size() == 0)
+        {
+            return joins;
+        }
+        ResultSet exportedKeys = null;
+        try
+        {
+            String fkTable = null;
+            List<String> pkColumns = new ArrayList<String>();
+            List<String> fkColumns = new ArrayList<String>();
+            exportedKeys = databaseMetaData.getExportedKeys(getCatalog(), 
getSchema(), pkEntity.getTable());
+            while (exportedKeys.next())
+            {
+                short ord = exportedKeys.getShort("KEY_SEQ");
+                if (ord == 1 && pkColumns.size() > 0)
+                {
+                    // save previous key
+                    fkColumns = sortColumns(pkEntity.getSqlPrimaryKey(), 
pkColumns, fkColumns);
+                    joins.add(Pair.of(fkTable, fkColumns));
+                    pkColumns.clear();
+                    fkColumns.clear();
+                }
+                fkTable = exportedKeys.getString("FKTABLE_NAME");
+                pkColumns.add(exportedKeys.getString("PKCOLUMN_NAME"));
+                fkColumns.add(exportedKeys.getString("FKCOLUMN_NAME"));
+            }
+            // save last key
+            if (fkTable != null)
+            {
+                fkColumns = sortColumns(pkEntity.getSqlPrimaryKey(), 
pkColumns, fkColumns);
+                joins.add(Pair.of(fkTable, fkColumns));
+            }
+        }
+        finally
+        {
+            if (exportedKeys != null)
+            {
+                exportedKeys.close();
+            }
+        }
+        return joins;
+    }
+
+    /**
+     * Sort columns in <code>target</code> the same way <code>unordered</code> 
would have to
+     * be sorted to be like <code>ordered</code>.
+     * @param ordered ordered list reference
+     * @param unordered unordered list reference
+     * @param target target list
+     * @return sorted target list
+     */
+    private List<String> sortColumns(List<String> ordered, List<String> 
unordered, List<String> target)
+    {
+        if(ordered.size() == 1)
+        {
+            return target;
+        }
+
+        List<String> sorted = new ArrayList<String>();
+
+        for(String col : ordered)
+        {
+            int i = unordered.indexOf(col);
+            if (i == -1)
+            {
+                throw new ConfigurationException("foreign key inconsistency: 
pk column '" + col + "' not found in imported key columns");
+            }
+            sorted.add(target.get(i));
+        }
+        return sorted;
+    }
+
+    private DatabaseMetaData databaseMetaData = null;
+    private DriverInfos driverInfos = null;
+}

Added: 
velocity/tools/branches/model/velocity-tools-model/src/main/java/org/apache/velocity/tools/model/impl/RowIterator.java
URL: 
http://svn.apache.org/viewvc/velocity/tools/branches/model/velocity-tools-model/src/main/java/org/apache/velocity/tools/model/impl/RowIterator.java?rev=1857755&view=auto
==============================================================================
--- 
velocity/tools/branches/model/velocity-tools-model/src/main/java/org/apache/velocity/tools/model/impl/RowIterator.java
 (added)
+++ 
velocity/tools/branches/model/velocity-tools-model/src/main/java/org/apache/velocity/tools/model/impl/RowIterator.java
 Thu Apr 18 15:04:54 2019
@@ -0,0 +1,343 @@
+package org.apache.velocity.tools.model.impl;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import org.apache.velocity.tools.model.Entity;
+import org.apache.velocity.tools.model.Instance;
+import org.apache.velocity.tools.model.sql.PooledStatement;
+import org.apache.velocity.tools.model.sql.SqlUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Serializable;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+//import org.apache.velocity.tools.model.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:[email protected]>Claude Brisson</a>
+ */
+public class RowIterator extends InstanceProducer implements 
Iterator<Instance>, Serializable
+{
+    Logger logger = LoggerFactory.getLogger(RowIterator.class);
+
+    /**
+     * Build a new RowIterator.
+     *
+     * @param pooledStatement the sql statement
+     * @param resultSet the resultset
+     * @param resultEntity the resulting entity (may be null)
+     */
+    public RowIterator(AttributeHolder parent, PooledStatement 
pooledStatement, ResultSet resultSet, Entity resultEntity)
+    {
+        super(parent.getModel(), resultEntity);
+        this.pooledStatement = pooledStatement;
+        this.resultSet = resultSet;
+    }
+
+    /**
+     * 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.error(e.getMessage());
+            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;
+
+            Instance row =  newResultInstance();
+            row.setInitialValues(pooledStatement);
+            return row;
+        }
+        catch(SQLException sqle)
+        {
+            logger.error("could not get next row", 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.
+     *
+     * @param key the name of an existing column or attribute
+     * @return an entity, an attribute reference, an instance, a string or null
+     */
+    public Serializable get(Object key) throws SQLException // TODO object ?!
+    {
+        String property = (String)key;
+        Serializable result = null;
+
+        if(!dataAvailable())
+        {
+            return null;
+        }
+        result = (Serializable)resultSet.getObject(property);
+        /*
+        if (resultEntity != null)
+        {
+            if (result == null)
+            {
+                // TODO - resolveCase? property = 
resultEntity.resolveName(property);
+                Attribute attribute = resultEntity.getAttribute(property);
+                if (attribute != null && attribute instanceof ScalarAttribute)
+                {
+                    result = 
((ScalarAttribute)attribute).evaluate(pooledStatement);
+                }
+            }
+        }
+        */
+        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(1));
+            }
+            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() throws SQLException
+    {
+        if(cachedSet == null)
+        {
+            cachedSet = new 
HashSet<String>(SqlUtils.getColumnNames(resultSet));
+        }
+        return cachedSet;
+    }
+
+    /*  * /
+    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/tools/branches/model/velocity-tools-model/src/main/java/org/apache/velocity/tools/model/impl/UpdateAction.java
URL: 
http://svn.apache.org/viewvc/velocity/tools/branches/model/velocity-tools-model/src/main/java/org/apache/velocity/tools/model/impl/UpdateAction.java?rev=1857755&view=auto
==============================================================================
--- 
velocity/tools/branches/model/velocity-tools-model/src/main/java/org/apache/velocity/tools/model/impl/UpdateAction.java
 (added)
+++ 
velocity/tools/branches/model/velocity-tools-model/src/main/java/org/apache/velocity/tools/model/impl/UpdateAction.java
 Thu Apr 18 15:04:54 2019
@@ -0,0 +1,129 @@
+package org.apache.velocity.tools.model.impl;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.velocity.tools.model.Action;
+import org.apache.velocity.tools.model.Entity;
+import org.apache.velocity.tools.model.Instance;
+
+import java.io.Serializable;
+import java.sql.SQLException;
+import java.util.BitSet;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class UpdateAction extends Action
+{
+    protected static String DYNAMIC_PART = "_DYNAMIC_PART_";
+
+    public UpdateAction(AttributeHolder parent)
+    {
+        super("update", parent);
+    }
+
+    @Override
+    protected void addParameter(String paramName)
+    {
+        parameterNames.add(paramName);
+        if (DYNAMIC_PART.equals(paramName))
+        {
+            addQueryPart(DYNAMIC_PART);
+        }
+        else
+        {
+            addQueryPart("?");
+        }
+    }
+
+
+    @Override
+    public int perform(Map source) throws SQLException
+    {
+        if (!(source instanceof Instance))
+        {
+            throw new SQLException("unexpected condition");
+        }
+        Instance instance = (Instance)source;
+        setState(instance.getDirtyFlags());
+        return perform(getParamValues(source));
+    }
+
+    @Override
+    public String getQuery() throws SQLException
+    {
+        if (state == null)
+        {
+            throw new SQLException("update action called without state");
+        }
+        Entity entity = (Entity)getParent();
+        List<String> dirtyColumns = state.stream().mapToObj(col -> 
entity.quoteIdentifier(entity.getColumn(col).sqlName)).collect(Collectors.toList());
+        String dirtyPart = StringUtils.join(dirtyColumns, " = ?, ") + " = ?";
+        return super.getQuery().replace(DYNAMIC_PART, dirtyPart);
+    }
+
+    public void setState(BitSet state)
+    {
+        this.state = state;
+    }
+
+    @Override
+    protected Serializable[] getParamValues(Serializable[] params) throws 
SQLException
+    {
+        // already filtered
+        return params;
+    }
+
+    @Override
+    protected Serializable[] getParamValues(Map source) throws SQLException
+    {
+        Instance instance = (Instance)source;
+        Entity entity = instance.getEntity();
+        if (entity != getParent())
+        {
+            throw new SQLException("inconsistency");
+        }
+        List<String> columnNames = entity.getColumnNames();
+        Serializable[] paramValues = new Serializable[state.cardinality() + 
parameterNames.size() - 1];
+        int paramIndex = 0;
+        for (int i = 0; i < paramValues.length;)
+        {
+            String paramName = parameterNames.get(paramIndex++);
+            if (DYNAMIC_PART.equals(paramName))
+            {
+                int col = -1;
+                while ((col = state.nextSetBit(col + 1)) != -1)
+                {
+                    String columnName = entity.getColumnName(col);
+                    paramValues[i++] = entity.filterValue(columnName, 
instance.get(columnName));
+                }
+            }
+            else
+            {
+                paramValues[i] = entity.filterValue(paramName, 
instance.get(paramName));
+                ++i;
+            }
+        }
+        return paramValues;
+    }
+
+    private BitSet state = null;
+}


Reply via email to