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; +}
