diff options
author | Artur Signell <artur@vaadin.com> | 2012-08-13 18:34:33 +0300 |
---|---|---|
committer | Artur Signell <artur@vaadin.com> | 2012-08-13 19:18:33 +0300 |
commit | e85d933b25cc3c5cc85eb7eb4b13b950fd8e1569 (patch) | |
tree | 9ab6f13f7188cab44bbd979b1cf620f15328a03f /server/src/com/vaadin/data/util/sqlcontainer/query | |
parent | 14dd4d0b28c76eb994b181a4570f3adec53342e6 (diff) | |
download | vaadin-framework-e85d933b25cc3c5cc85eb7eb4b13b950fd8e1569.tar.gz vaadin-framework-e85d933b25cc3c5cc85eb7eb4b13b950fd8e1569.zip |
Moved server files to a server src folder (#9299)
Diffstat (limited to 'server/src/com/vaadin/data/util/sqlcontainer/query')
22 files changed, 2877 insertions, 0 deletions
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQuery.java b/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQuery.java new file mode 100644 index 0000000000..ec986fab95 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQuery.java @@ -0,0 +1,507 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query; + +import java.io.IOException; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.sqlcontainer.RowItem; +import com.vaadin.data.util.sqlcontainer.SQLContainer; +import com.vaadin.data.util.sqlcontainer.connection.JDBCConnectionPool; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; +import com.vaadin.data.util.sqlcontainer.query.generator.filter.QueryBuilder; + +@SuppressWarnings("serial") +public class FreeformQuery implements QueryDelegate { + + FreeformQueryDelegate delegate = null; + private String queryString; + private List<String> primaryKeyColumns; + private JDBCConnectionPool connectionPool; + private transient Connection activeConnection = null; + + /** + * Prevent no-parameters instantiation of FreeformQuery + */ + @SuppressWarnings("unused") + private FreeformQuery() { + } + + /** + * Creates a new freeform query delegate to be used with the + * {@link SQLContainer}. + * + * @param queryString + * The actual query to perform. + * @param primaryKeyColumns + * The primary key columns. Read-only mode is forced if this + * parameter is null or empty. + * @param connectionPool + * the JDBCConnectionPool to use to open connections to the SQL + * database. + * @deprecated @see + * {@link FreeformQuery#FreeformQuery(String, JDBCConnectionPool, String...)} + */ + @Deprecated + public FreeformQuery(String queryString, List<String> primaryKeyColumns, + JDBCConnectionPool connectionPool) { + if (primaryKeyColumns == null) { + primaryKeyColumns = new ArrayList<String>(); + } + if (primaryKeyColumns.contains("")) { + throw new IllegalArgumentException( + "The primary key columns contain an empty string!"); + } else if (queryString == null || "".equals(queryString)) { + throw new IllegalArgumentException( + "The query string may not be empty or null!"); + } else if (connectionPool == null) { + throw new IllegalArgumentException( + "The connectionPool may not be null!"); + } + this.queryString = queryString; + this.primaryKeyColumns = Collections + .unmodifiableList(primaryKeyColumns); + this.connectionPool = connectionPool; + } + + /** + * Creates a new freeform query delegate to be used with the + * {@link SQLContainer}. + * + * @param queryString + * The actual query to perform. + * @param connectionPool + * the JDBCConnectionPool to use to open connections to the SQL + * database. + * @param primaryKeyColumns + * The primary key columns. Read-only mode is forced if none are + * provided. (optional) + */ + public FreeformQuery(String queryString, JDBCConnectionPool connectionPool, + String... primaryKeyColumns) { + this(queryString, Arrays.asList(primaryKeyColumns), connectionPool); + } + + /** + * This implementation of getCount() actually fetches all records from the + * database, which might be a performance issue. Override this method with a + * SELECT COUNT(*) ... query if this is too slow for your needs. + * + * {@inheritDoc} + */ + @Override + public int getCount() throws SQLException { + // First try the delegate + int count = countByDelegate(); + if (count < 0) { + // Couldn't use the delegate, use the bad way. + Connection conn = getConnection(); + Statement statement = conn.createStatement( + ResultSet.TYPE_SCROLL_INSENSITIVE, + ResultSet.CONCUR_READ_ONLY); + + ResultSet rs = statement.executeQuery(queryString); + if (rs.last()) { + count = rs.getRow(); + } else { + count = 0; + } + rs.close(); + statement.close(); + releaseConnection(conn); + } + return count; + } + + @SuppressWarnings("deprecation") + private int countByDelegate() throws SQLException { + int count = -1; + if (delegate == null) { + return count; + } + /* First try using prepared statement */ + if (delegate instanceof FreeformStatementDelegate) { + try { + StatementHelper sh = ((FreeformStatementDelegate) delegate) + .getCountStatement(); + Connection c = getConnection(); + PreparedStatement pstmt = c.prepareStatement(sh + .getQueryString()); + sh.setParameterValuesToStatement(pstmt); + ResultSet rs = pstmt.executeQuery(); + rs.next(); + count = rs.getInt(1); + rs.close(); + pstmt.clearParameters(); + pstmt.close(); + releaseConnection(c); + return count; + } catch (UnsupportedOperationException e) { + // Count statement generation not supported + } + } + /* Try using regular statement */ + try { + String countQuery = delegate.getCountQuery(); + if (countQuery != null) { + Connection conn = getConnection(); + Statement statement = conn.createStatement(); + ResultSet rs = statement.executeQuery(countQuery); + rs.next(); + count = rs.getInt(1); + rs.close(); + statement.close(); + releaseConnection(conn); + return count; + } + } catch (UnsupportedOperationException e) { + // Count query generation not supported + } + return count; + } + + private Connection getConnection() throws SQLException { + if (activeConnection != null) { + return activeConnection; + } + return connectionPool.reserveConnection(); + } + + /** + * Fetches the results for the query. This implementation always fetches the + * entire record set, ignoring the offset and page length parameters. In + * order to support lazy loading of records, you must supply a + * FreeformQueryDelegate that implements the + * FreeformQueryDelegate.getQueryString(int,int) method. + * + * @throws SQLException + * + * @see FreeformQueryDelegate#getQueryString(int, int) + */ + @Override + @SuppressWarnings("deprecation") + public ResultSet getResults(int offset, int pagelength) throws SQLException { + if (activeConnection == null) { + throw new SQLException("No active transaction!"); + } + String query = queryString; + if (delegate != null) { + /* First try using prepared statement */ + if (delegate instanceof FreeformStatementDelegate) { + try { + StatementHelper sh = ((FreeformStatementDelegate) delegate) + .getQueryStatement(offset, pagelength); + PreparedStatement pstmt = activeConnection + .prepareStatement(sh.getQueryString()); + sh.setParameterValuesToStatement(pstmt); + return pstmt.executeQuery(); + } catch (UnsupportedOperationException e) { + // Statement generation not supported, continue... + } + } + try { + query = delegate.getQueryString(offset, pagelength); + } catch (UnsupportedOperationException e) { + // This is fine, we'll just use the default queryString. + } + } + Statement statement = activeConnection.createStatement(); + ResultSet rs = statement.executeQuery(query); + return rs; + } + + @Override + @SuppressWarnings("deprecation") + public boolean implementationRespectsPagingLimits() { + if (delegate == null) { + return false; + } + /* First try using prepared statement */ + if (delegate instanceof FreeformStatementDelegate) { + try { + StatementHelper sh = ((FreeformStatementDelegate) delegate) + .getCountStatement(); + if (sh != null && sh.getQueryString() != null + && sh.getQueryString().length() > 0) { + return true; + } + } catch (UnsupportedOperationException e) { + // Statement generation not supported, continue... + } + } + try { + String queryString = delegate.getQueryString(0, 50); + return queryString != null && queryString.length() > 0; + } catch (UnsupportedOperationException e) { + return false; + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.sqlcontainer.query.QueryDelegate#setFilters(java + * .util.List) + */ + @Override + public void setFilters(List<Filter> filters) + throws UnsupportedOperationException { + if (delegate != null) { + delegate.setFilters(filters); + } else if (filters != null) { + throw new UnsupportedOperationException( + "FreeFormQueryDelegate not set!"); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.sqlcontainer.query.QueryDelegate#setOrderBy(java + * .util.List) + */ + @Override + public void setOrderBy(List<OrderBy> orderBys) + throws UnsupportedOperationException { + if (delegate != null) { + delegate.setOrderBy(orderBys); + } else if (orderBys != null) { + throw new UnsupportedOperationException( + "FreeFormQueryDelegate not set!"); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.sqlcontainer.query.QueryDelegate#storeRow(com.vaadin + * .data.util.sqlcontainer.RowItem) + */ + @Override + public int storeRow(RowItem row) throws SQLException { + if (activeConnection == null) { + throw new IllegalStateException("No transaction is active!"); + } else if (primaryKeyColumns.isEmpty()) { + throw new UnsupportedOperationException( + "Cannot store items fetched with a read-only freeform query!"); + } + if (delegate != null) { + return delegate.storeRow(activeConnection, row); + } else { + throw new UnsupportedOperationException( + "FreeFormQueryDelegate not set!"); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.sqlcontainer.query.QueryDelegate#removeRow(com.vaadin + * .data.util.sqlcontainer.RowItem) + */ + @Override + public boolean removeRow(RowItem row) throws SQLException { + if (activeConnection == null) { + throw new IllegalStateException("No transaction is active!"); + } else if (primaryKeyColumns.isEmpty()) { + throw new UnsupportedOperationException( + "Cannot remove items fetched with a read-only freeform query!"); + } + if (delegate != null) { + return delegate.removeRow(activeConnection, row); + } else { + throw new UnsupportedOperationException( + "FreeFormQueryDelegate not set!"); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.sqlcontainer.query.QueryDelegate#beginTransaction() + */ + @Override + public synchronized void beginTransaction() + throws UnsupportedOperationException, SQLException { + if (activeConnection != null) { + throw new IllegalStateException("A transaction is already active!"); + } + activeConnection = connectionPool.reserveConnection(); + activeConnection.setAutoCommit(false); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.sqlcontainer.query.QueryDelegate#commit() + */ + @Override + public synchronized void commit() throws UnsupportedOperationException, + SQLException { + if (activeConnection == null) { + throw new SQLException("No active transaction"); + } + if (!activeConnection.getAutoCommit()) { + activeConnection.commit(); + } + connectionPool.releaseConnection(activeConnection); + activeConnection = null; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.sqlcontainer.query.QueryDelegate#rollback() + */ + @Override + public synchronized void rollback() throws UnsupportedOperationException, + SQLException { + if (activeConnection == null) { + throw new SQLException("No active transaction"); + } + activeConnection.rollback(); + connectionPool.releaseConnection(activeConnection); + activeConnection = null; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.sqlcontainer.query.QueryDelegate#getPrimaryKeyColumns + * () + */ + @Override + public List<String> getPrimaryKeyColumns() { + return primaryKeyColumns; + } + + public String getQueryString() { + return queryString; + } + + public FreeformQueryDelegate getDelegate() { + return delegate; + } + + public void setDelegate(FreeformQueryDelegate delegate) { + this.delegate = delegate; + } + + /** + * This implementation of the containsRowWithKey method rewrites existing + * WHERE clauses in the query string. The logic is, however, not very + * complex and some times can do the Wrong Thing<sup>TM</sup>. For the + * situations where this logic is not enough, you can implement the + * getContainsRowQueryString method in FreeformQueryDelegate and this will + * be used instead of the logic. + * + * @see FreeformQueryDelegate#getContainsRowQueryString(Object...) + * + */ + @Override + @SuppressWarnings("deprecation") + public boolean containsRowWithKey(Object... keys) throws SQLException { + String query = null; + boolean contains = false; + if (delegate != null) { + if (delegate instanceof FreeformStatementDelegate) { + try { + StatementHelper sh = ((FreeformStatementDelegate) delegate) + .getContainsRowQueryStatement(keys); + Connection c = getConnection(); + PreparedStatement pstmt = c.prepareStatement(sh + .getQueryString()); + sh.setParameterValuesToStatement(pstmt); + ResultSet rs = pstmt.executeQuery(); + contains = rs.next(); + rs.close(); + pstmt.clearParameters(); + pstmt.close(); + releaseConnection(c); + return contains; + } catch (UnsupportedOperationException e) { + // Statement generation not supported, continue... + } + } + try { + query = delegate.getContainsRowQueryString(keys); + } catch (UnsupportedOperationException e) { + query = modifyWhereClause(keys); + } + } else { + query = modifyWhereClause(keys); + } + Connection conn = getConnection(); + try { + Statement statement = conn.createStatement(); + ResultSet rs = statement.executeQuery(query); + contains = rs.next(); + rs.close(); + statement.close(); + } finally { + releaseConnection(conn); + } + return contains; + } + + /** + * Releases the connection if it is not part of an active transaction. + * + * @param conn + * the connection to release + */ + private void releaseConnection(Connection conn) { + if (conn != activeConnection) { + connectionPool.releaseConnection(conn); + } + } + + private String modifyWhereClause(Object... keys) { + // Build the where rules for the provided keys + StringBuffer where = new StringBuffer(); + for (int ix = 0; ix < primaryKeyColumns.size(); ix++) { + where.append(QueryBuilder.quote(primaryKeyColumns.get(ix))); + if (keys[ix] == null) { + where.append(" IS NULL"); + } else { + where.append(" = '").append(keys[ix]).append("'"); + } + if (ix < primaryKeyColumns.size() - 1) { + where.append(" AND "); + } + } + // Is there already a WHERE clause in the query string? + int index = queryString.toLowerCase().indexOf("where "); + if (index > -1) { + // Rewrite the where clause + return queryString.substring(0, index) + "WHERE " + where + " AND " + + queryString.substring(index + 6); + } + // Append a where clause + return queryString + " WHERE " + where; + } + + private void writeObject(java.io.ObjectOutputStream out) throws IOException { + try { + rollback(); + } catch (SQLException ignored) { + } + out.defaultWriteObject(); + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQueryDelegate.java b/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQueryDelegate.java new file mode 100644 index 0000000000..433d742be8 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQueryDelegate.java @@ -0,0 +1,118 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query; + +import java.io.Serializable; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.sqlcontainer.RowItem; + +public interface FreeformQueryDelegate extends Serializable { + /** + * Should return the SQL query string to be performed. This method is + * responsible for gluing together the select query from the filters and the + * order by conditions if these are supported. + * + * @param offset + * the first record (row) to fetch. + * @param pagelength + * the number of records (rows) to fetch. 0 means all records + * starting from offset. + * @deprecated Implement {@link FreeformStatementDelegate} instead of + * {@link FreeformQueryDelegate} + */ + @Deprecated + public String getQueryString(int offset, int limit) + throws UnsupportedOperationException; + + /** + * Generates and executes a query to determine the current row count from + * the DB. Row count will be fetched using filters that are currently set to + * the QueryDelegate. + * + * @return row count + * @throws SQLException + * @deprecated Implement {@link FreeformStatementDelegate} instead of + * {@link FreeformQueryDelegate} + */ + @Deprecated + public String getCountQuery() throws UnsupportedOperationException; + + /** + * Sets the filters to apply when performing the SQL query. These are + * translated into a WHERE clause. Default filtering mode will be used. + * + * @param filters + * The filters to apply. + * @throws UnsupportedOperationException + * if the implementation doesn't support filtering. + */ + public void setFilters(List<Filter> filters) + throws UnsupportedOperationException; + + /** + * Sets the order in which to retrieve rows from the database. The result + * can be ordered by zero or more columns and each column can be in + * ascending or descending order. These are translated into an ORDER BY + * clause in the SQL query. + * + * @param orderBys + * A list of the OrderBy conditions. + * @throws UnsupportedOperationException + * if the implementation doesn't support ordering. + */ + public void setOrderBy(List<OrderBy> orderBys) + throws UnsupportedOperationException; + + /** + * Stores a row in the database. The implementation of this interface + * decides how to identify whether to store a new row or update an existing + * one. + * + * @param conn + * the JDBC connection to use + * @param row + * RowItem to be stored or updated. + * @throws UnsupportedOperationException + * if the implementation is read only. + * @throws SQLException + */ + public int storeRow(Connection conn, RowItem row) + throws UnsupportedOperationException, SQLException; + + /** + * Removes the given RowItem from the database. + * + * @param conn + * the JDBC connection to use + * @param row + * RowItem to be removed + * @return true on success + * @throws UnsupportedOperationException + * @throws SQLException + */ + public boolean removeRow(Connection conn, RowItem row) + throws UnsupportedOperationException, SQLException; + + /** + * Generates an SQL Query string that allows the user of the FreeformQuery + * class to customize the query string used by the + * FreeformQuery.containsRowWithKeys() method. This is useful for cases when + * the logic in the containsRowWithKeys method is not enough to support more + * complex free form queries. + * + * @param keys + * the values of the primary keys + * @throws UnsupportedOperationException + * to use the default logic in FreeformQuery + * @deprecated Implement {@link FreeformStatementDelegate} instead of + * {@link FreeformQueryDelegate} + */ + @Deprecated + public String getContainsRowQueryString(Object... keys) + throws UnsupportedOperationException; +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformStatementDelegate.java b/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformStatementDelegate.java new file mode 100644 index 0000000000..95521c5019 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformStatementDelegate.java @@ -0,0 +1,57 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query; + +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +/** + * FreeformStatementDelegate is an extension to FreeformQueryDelegate that + * provides definitions for methods that produce StatementHelper objects instead + * of basic query strings. This allows the FreeformQuery query delegate to use + * PreparedStatements instead of regular Statement when accessing the database. + * + * Due to the injection protection and other benefits of prepared statements, it + * is advisable to implement this interface instead of the FreeformQueryDelegate + * whenever possible. + */ +public interface FreeformStatementDelegate extends FreeformQueryDelegate { + /** + * Should return a new instance of StatementHelper that contains the query + * string and parameter values required to create a PreparedStatement. This + * method is responsible for gluing together the select query from the + * filters and the order by conditions if these are supported. + * + * @param offset + * the first record (row) to fetch. + * @param pagelength + * the number of records (rows) to fetch. 0 means all records + * starting from offset. + */ + public StatementHelper getQueryStatement(int offset, int limit) + throws UnsupportedOperationException; + + /** + * Should return a new instance of StatementHelper that contains the query + * string and parameter values required to create a PreparedStatement that + * will fetch the row count from the DB. Row count should be fetched using + * filters that are currently set to the QueryDelegate. + */ + public StatementHelper getCountStatement() + throws UnsupportedOperationException; + + /** + * Should return a new instance of StatementHelper that contains the query + * string and parameter values required to create a PreparedStatement used + * by the FreeformQuery.containsRowWithKeys() method. This is useful for + * cases when the default logic in said method is not enough to support more + * complex free form queries. + * + * @param keys + * the values of the primary keys + * @throws UnsupportedOperationException + * to use the default logic in FreeformQuery + */ + public StatementHelper getContainsRowQueryStatement(Object... keys) + throws UnsupportedOperationException; +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/OrderBy.java b/server/src/com/vaadin/data/util/sqlcontainer/query/OrderBy.java new file mode 100644 index 0000000000..8ebe10067e --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/OrderBy.java @@ -0,0 +1,46 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query; + +import java.io.Serializable; + +/** + * OrderBy represents a sorting rule to be applied to a query made by the + * SQLContainer's QueryDelegate. + * + * The sorting rule is simple and contains only the affected column's name and + * the direction of the sort. + */ +public class OrderBy implements Serializable { + private String column; + private boolean isAscending; + + /** + * Prevent instantiation without required parameters. + */ + @SuppressWarnings("unused") + private OrderBy() { + } + + public OrderBy(String column, boolean isAscending) { + setColumn(column); + setAscending(isAscending); + } + + public void setColumn(String column) { + this.column = column; + } + + public String getColumn() { + return column; + } + + public void setAscending(boolean isAscending) { + this.isAscending = isAscending; + } + + public boolean isAscending() { + return isAscending; + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/QueryDelegate.java b/server/src/com/vaadin/data/util/sqlcontainer/query/QueryDelegate.java new file mode 100644 index 0000000000..6e4396fad1 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/QueryDelegate.java @@ -0,0 +1,211 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query; + +import java.io.Serializable; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.sqlcontainer.RowId; +import com.vaadin.data.util.sqlcontainer.RowItem; + +public interface QueryDelegate extends Serializable { + /** + * Generates and executes a query to determine the current row count from + * the DB. Row count will be fetched using filters that are currently set to + * the QueryDelegate. + * + * @return row count + * @throws SQLException + */ + public int getCount() throws SQLException; + + /** + * Executes a paged SQL query and returns the ResultSet. The query is + * defined through implementations of this QueryDelegate interface. + * + * @param offset + * the first item of the page to load + * @param pagelength + * the length of the page to load + * @return a ResultSet containing the rows of the page + * @throws SQLException + * if the database access fails. + */ + public ResultSet getResults(int offset, int pagelength) throws SQLException; + + /** + * Allows the SQLContainer implementation to check whether the QueryDelegate + * implementation implements paging in the getResults method. + * + * @see QueryDelegate#getResults(int, int) + * + * @return true if the delegate implements paging + */ + public boolean implementationRespectsPagingLimits(); + + /** + * Sets the filters to apply when performing the SQL query. These are + * translated into a WHERE clause. Default filtering mode will be used. + * + * @param filters + * The filters to apply. + * @throws UnsupportedOperationException + * if the implementation doesn't support filtering. + */ + public void setFilters(List<Filter> filters) + throws UnsupportedOperationException; + + /** + * Sets the order in which to retrieve rows from the database. The result + * can be ordered by zero or more columns and each column can be in + * ascending or descending order. These are translated into an ORDER BY + * clause in the SQL query. + * + * @param orderBys + * A list of the OrderBy conditions. + * @throws UnsupportedOperationException + * if the implementation doesn't support ordering. + */ + public void setOrderBy(List<OrderBy> orderBys) + throws UnsupportedOperationException; + + /** + * Stores a row in the database. The implementation of this interface + * decides how to identify whether to store a new row or update an existing + * one. + * + * @param columnToValueMap + * A map containing the values for all columns to be stored or + * updated. + * @return the number of affected rows in the database table + * @throws UnsupportedOperationException + * if the implementation is read only. + */ + public int storeRow(RowItem row) throws UnsupportedOperationException, + SQLException; + + /** + * Removes the given RowItem from the database. + * + * @param row + * RowItem to be removed + * @return true on success + * @throws UnsupportedOperationException + * @throws SQLException + */ + public boolean removeRow(RowItem row) throws UnsupportedOperationException, + SQLException; + + /** + * Starts a new database transaction. Used when storing multiple changes. + * + * Note that if a transaction is already open, it will be rolled back when a + * new transaction is started. + * + * @throws SQLException + * if the database access fails. + */ + public void beginTransaction() throws SQLException; + + /** + * Commits a transaction. If a transaction is not open nothing should + * happen. + * + * @throws SQLException + * if the database access fails. + */ + public void commit() throws SQLException; + + /** + * Rolls a transaction back. If a transaction is not open nothing should + * happen. + * + * @throws SQLException + * if the database access fails. + */ + public void rollback() throws SQLException; + + /** + * Returns a list of primary key column names. The list is either fetched + * from the database (TableQuery) or given as an argument depending on + * implementation. + * + * @return + */ + public List<String> getPrimaryKeyColumns(); + + /** + * Performs a query to find out whether the SQL table contains a row with + * the given set of primary keys. + * + * @param keys + * the primary keys + * @return true if the SQL table contains a row with the provided keys + * @throws SQLException + */ + public boolean containsRowWithKey(Object... keys) throws SQLException; + + /************************/ + /** ROWID CHANGE EVENT **/ + /************************/ + + /** + * An <code>Event</code> object specifying the old and new RowId of an added + * item after the addition has been successfully committed. + */ + public interface RowIdChangeEvent extends Serializable { + /** + * Gets the old (temporary) RowId of the added row that raised this + * event. + * + * @return old RowId + */ + public RowId getOldRowId(); + + /** + * Gets the new, possibly database assigned RowId of the added row that + * raised this event. + * + * @return new RowId + */ + public RowId getNewRowId(); + } + + /** RowId change listener interface. */ + public interface RowIdChangeListener extends Serializable { + /** + * Lets the listener know that a RowId has been changed. + * + * @param event + */ + public void rowIdChange(QueryDelegate.RowIdChangeEvent event); + } + + /** + * The interface for adding and removing <code>RowIdChangeEvent</code> + * listeners. By implementing this interface a class explicitly announces + * that it will generate a <code>RowIdChangeEvent</code> when it performs a + * database commit that may change the RowId. + */ + public interface RowIdChangeNotifier extends Serializable { + /** + * Adds a RowIdChangeListener for the object. + * + * @param listener + * listener to be added + */ + public void addListener(QueryDelegate.RowIdChangeListener listener); + + /** + * Removes the specified RowIdChangeListener from the object. + * + * @param listener + * listener to be removed + */ + public void removeListener(QueryDelegate.RowIdChangeListener listener); + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/TableQuery.java b/server/src/com/vaadin/data/util/sqlcontainer/query/TableQuery.java new file mode 100644 index 0000000000..d0606704f7 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/TableQuery.java @@ -0,0 +1,715 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query; + +import java.io.IOException; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EventObject; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.Compare.Equal; +import com.vaadin.data.util.sqlcontainer.ColumnProperty; +import com.vaadin.data.util.sqlcontainer.OptimisticLockException; +import com.vaadin.data.util.sqlcontainer.RowId; +import com.vaadin.data.util.sqlcontainer.RowItem; +import com.vaadin.data.util.sqlcontainer.SQLUtil; +import com.vaadin.data.util.sqlcontainer.TemporaryRowId; +import com.vaadin.data.util.sqlcontainer.connection.JDBCConnectionPool; +import com.vaadin.data.util.sqlcontainer.query.generator.DefaultSQLGenerator; +import com.vaadin.data.util.sqlcontainer.query.generator.MSSQLGenerator; +import com.vaadin.data.util.sqlcontainer.query.generator.SQLGenerator; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +@SuppressWarnings("serial") +public class TableQuery implements QueryDelegate, + QueryDelegate.RowIdChangeNotifier { + + /** Table name, primary key column name(s) and version column name */ + private String tableName; + private List<String> primaryKeyColumns; + private String versionColumn; + + /** Currently set Filters and OrderBys */ + private List<Filter> filters; + private List<OrderBy> orderBys; + + /** SQLGenerator instance to use for generating queries */ + private SQLGenerator sqlGenerator; + + /** Fields related to Connection and Transaction handling */ + private JDBCConnectionPool connectionPool; + private transient Connection activeConnection; + private boolean transactionOpen; + + /** Row ID change listeners */ + private LinkedList<RowIdChangeListener> rowIdChangeListeners; + /** Row ID change events, stored until commit() is called */ + private final List<RowIdChangeEvent> bufferedEvents = new ArrayList<RowIdChangeEvent>(); + + /** Set to true to output generated SQL Queries to System.out */ + private boolean debug = false; + + /** Prevent no-parameters instantiation of TableQuery */ + @SuppressWarnings("unused") + private TableQuery() { + } + + /** + * Creates a new TableQuery using the given connection pool, SQL generator + * and table name to fetch the data from. All parameters must be non-null. + * + * @param tableName + * Name of the database table to connect to + * @param connectionPool + * Connection pool for accessing the database + * @param sqlGenerator + * SQL query generator implementation + */ + public TableQuery(String tableName, JDBCConnectionPool connectionPool, + SQLGenerator sqlGenerator) { + if (tableName == null || tableName.trim().length() < 1 + || connectionPool == null || sqlGenerator == null) { + throw new IllegalArgumentException( + "All parameters must be non-null and a table name must be given."); + } + this.tableName = tableName; + this.sqlGenerator = sqlGenerator; + this.connectionPool = connectionPool; + fetchMetaData(); + } + + /** + * Creates a new TableQuery using the given connection pool and table name + * to fetch the data from. All parameters must be non-null. The default SQL + * generator will be used for queries. + * + * @param tableName + * Name of the database table to connect to + * @param connectionPool + * Connection pool for accessing the database + */ + public TableQuery(String tableName, JDBCConnectionPool connectionPool) { + this(tableName, connectionPool, new DefaultSQLGenerator()); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#getCount() + */ + @Override + public int getCount() throws SQLException { + getLogger().log(Level.FINE, "Fetching count..."); + StatementHelper sh = sqlGenerator.generateSelectQuery(tableName, + filters, null, 0, 0, "COUNT(*)"); + boolean shouldCloseTransaction = false; + if (!transactionOpen) { + shouldCloseTransaction = true; + beginTransaction(); + } + ResultSet r = executeQuery(sh); + r.next(); + int count = r.getInt(1); + r.getStatement().close(); + r.close(); + if (shouldCloseTransaction) { + commit(); + } + return count; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#getResults(int, + * int) + */ + @Override + public ResultSet getResults(int offset, int pagelength) throws SQLException { + StatementHelper sh; + /* + * If no ordering is explicitly set, results will be ordered by the + * first primary key column. + */ + if (orderBys == null || orderBys.isEmpty()) { + List<OrderBy> ob = new ArrayList<OrderBy>(); + ob.add(new OrderBy(primaryKeyColumns.get(0), true)); + sh = sqlGenerator.generateSelectQuery(tableName, filters, ob, + offset, pagelength, null); + } else { + sh = sqlGenerator.generateSelectQuery(tableName, filters, orderBys, + offset, pagelength, null); + } + return executeQuery(sh); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate# + * implementationRespectsPagingLimits() + */ + @Override + public boolean implementationRespectsPagingLimits() { + return true; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.sqlcontainer.query.QueryDelegate#storeRow(com.vaadin + * .addon.sqlcontainer.RowItem) + */ + @Override + public int storeRow(RowItem row) throws UnsupportedOperationException, + SQLException { + if (row == null) { + throw new IllegalArgumentException("Row argument must be non-null."); + } + StatementHelper sh; + int result = 0; + if (row.getId() instanceof TemporaryRowId) { + setVersionColumnFlagInProperty(row); + sh = sqlGenerator.generateInsertQuery(tableName, row); + result = executeUpdateReturnKeys(sh, row); + } else { + setVersionColumnFlagInProperty(row); + sh = sqlGenerator.generateUpdateQuery(tableName, row); + result = executeUpdate(sh); + } + if (versionColumn != null && result == 0) { + throw new OptimisticLockException( + "Someone else changed the row that was being updated.", + row.getId()); + } + return result; + } + + private void setVersionColumnFlagInProperty(RowItem row) { + ColumnProperty versionProperty = (ColumnProperty) row + .getItemProperty(versionColumn); + if (versionProperty != null) { + versionProperty.setVersionColumn(true); + } + } + + /** + * Inserts the given row in the database table immediately. Begins and + * commits the transaction needed. This method was added specifically to + * solve the problem of returning the final RowId immediately on the + * SQLContainer.addItem() call when auto commit mode is enabled in the + * SQLContainer. + * + * @param row + * RowItem to add to the database + * @return Final RowId of the added row + * @throws SQLException + */ + public RowId storeRowImmediately(RowItem row) throws SQLException { + beginTransaction(); + /* Set version column, if one is provided */ + setVersionColumnFlagInProperty(row); + /* Generate query */ + StatementHelper sh = sqlGenerator.generateInsertQuery(tableName, row); + PreparedStatement pstmt = activeConnection.prepareStatement( + sh.getQueryString(), primaryKeyColumns.toArray(new String[0])); + sh.setParameterValuesToStatement(pstmt); + getLogger().log(Level.FINE, "DB -> " + sh.getQueryString()); + int result = pstmt.executeUpdate(); + if (result > 0) { + /* + * If affected rows exist, we'll get the new RowId, commit the + * transaction and return the new RowId. + */ + ResultSet generatedKeys = pstmt.getGeneratedKeys(); + RowId newId = getNewRowId(row, generatedKeys); + generatedKeys.close(); + pstmt.clearParameters(); + pstmt.close(); + commit(); + return newId; + } else { + pstmt.clearParameters(); + pstmt.close(); + /* On failure return null */ + return null; + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.sqlcontainer.query.QueryDelegate#setFilters(java.util + * .List) + */ + @Override + public void setFilters(List<Filter> filters) + throws UnsupportedOperationException { + if (filters == null) { + this.filters = null; + return; + } + this.filters = Collections.unmodifiableList(filters); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.sqlcontainer.query.QueryDelegate#setOrderBy(java.util + * .List) + */ + @Override + public void setOrderBy(List<OrderBy> orderBys) + throws UnsupportedOperationException { + if (orderBys == null) { + this.orderBys = null; + return; + } + this.orderBys = Collections.unmodifiableList(orderBys); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#beginTransaction() + */ + @Override + public void beginTransaction() throws UnsupportedOperationException, + SQLException { + if (transactionOpen && activeConnection != null) { + throw new IllegalStateException(); + } + + getLogger().log(Level.FINE, "DB -> begin transaction"); + activeConnection = connectionPool.reserveConnection(); + activeConnection.setAutoCommit(false); + transactionOpen = true; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#commit() + */ + @Override + public void commit() throws UnsupportedOperationException, SQLException { + if (transactionOpen && activeConnection != null) { + getLogger().log(Level.FINE, "DB -> commit"); + activeConnection.commit(); + connectionPool.releaseConnection(activeConnection); + } else { + throw new SQLException("No active transaction"); + } + transactionOpen = false; + + /* Handle firing row ID change events */ + RowIdChangeEvent[] unFiredEvents = bufferedEvents + .toArray(new RowIdChangeEvent[] {}); + bufferedEvents.clear(); + if (rowIdChangeListeners != null && !rowIdChangeListeners.isEmpty()) { + for (RowIdChangeListener r : rowIdChangeListeners) { + for (RowIdChangeEvent e : unFiredEvents) { + r.rowIdChange(e); + } + } + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#rollback() + */ + @Override + public void rollback() throws UnsupportedOperationException, SQLException { + if (transactionOpen && activeConnection != null) { + getLogger().log(Level.FINE, "DB -> rollback"); + activeConnection.rollback(); + connectionPool.releaseConnection(activeConnection); + } else { + throw new SQLException("No active transaction"); + } + transactionOpen = false; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.sqlcontainer.query.QueryDelegate#getPrimaryKeyColumns() + */ + @Override + public List<String> getPrimaryKeyColumns() { + return Collections.unmodifiableList(primaryKeyColumns); + } + + public String getVersionColumn() { + return versionColumn; + } + + public void setVersionColumn(String column) { + versionColumn = column; + } + + public String getTableName() { + return tableName; + } + + public SQLGenerator getSqlGenerator() { + return sqlGenerator; + } + + /** + * Executes the given query string using either the active connection if a + * transaction is already open, or a new connection from this query's + * connection pool. + * + * @param sh + * an instance of StatementHelper, containing the query string + * and parameter values. + * @return ResultSet of the query + * @throws SQLException + */ + private ResultSet executeQuery(StatementHelper sh) throws SQLException { + Connection c = null; + if (transactionOpen && activeConnection != null) { + c = activeConnection; + } else { + throw new SQLException("No active transaction!"); + } + PreparedStatement pstmt = c.prepareStatement(sh.getQueryString()); + sh.setParameterValuesToStatement(pstmt); + getLogger().log(Level.FINE, "DB -> " + sh.getQueryString()); + return pstmt.executeQuery(); + } + + /** + * Executes the given update query string using either the active connection + * if a transaction is already open, or a new connection from this query's + * connection pool. + * + * @param sh + * an instance of StatementHelper, containing the query string + * and parameter values. + * @return Number of affected rows + * @throws SQLException + */ + private int executeUpdate(StatementHelper sh) throws SQLException { + Connection c = null; + PreparedStatement pstmt = null; + try { + if (transactionOpen && activeConnection != null) { + c = activeConnection; + } else { + c = connectionPool.reserveConnection(); + } + pstmt = c.prepareStatement(sh.getQueryString()); + sh.setParameterValuesToStatement(pstmt); + getLogger().log(Level.FINE, "DB -> " + sh.getQueryString()); + int retval = pstmt.executeUpdate(); + return retval; + } finally { + if (pstmt != null) { + pstmt.clearParameters(); + pstmt.close(); + } + if (!transactionOpen) { + connectionPool.releaseConnection(c); + } + } + } + + /** + * Executes the given update query string using either the active connection + * if a transaction is already open, or a new connection from this query's + * connection pool. + * + * Additionally adds a new RowIdChangeEvent to the event buffer. + * + * @param sh + * an instance of StatementHelper, containing the query string + * and parameter values. + * @param row + * the row item to update + * @return Number of affected rows + * @throws SQLException + */ + private int executeUpdateReturnKeys(StatementHelper sh, RowItem row) + throws SQLException { + Connection c = null; + PreparedStatement pstmt = null; + ResultSet genKeys = null; + try { + if (transactionOpen && activeConnection != null) { + c = activeConnection; + } else { + c = connectionPool.reserveConnection(); + } + pstmt = c.prepareStatement(sh.getQueryString(), + primaryKeyColumns.toArray(new String[0])); + sh.setParameterValuesToStatement(pstmt); + getLogger().log(Level.FINE, "DB -> " + sh.getQueryString()); + int result = pstmt.executeUpdate(); + genKeys = pstmt.getGeneratedKeys(); + RowId newId = getNewRowId(row, genKeys); + bufferedEvents.add(new RowIdChangeEvent(row.getId(), newId)); + return result; + } finally { + if (genKeys != null) { + genKeys.close(); + } + if (pstmt != null) { + pstmt.clearParameters(); + pstmt.close(); + } + if (!transactionOpen) { + connectionPool.releaseConnection(c); + } + } + } + + /** + * Fetches name(s) of primary key column(s) from DB metadata. + * + * Also tries to get the escape string to be used in search strings. + */ + private void fetchMetaData() { + Connection c = null; + try { + c = connectionPool.reserveConnection(); + DatabaseMetaData dbmd = c.getMetaData(); + if (dbmd != null) { + tableName = SQLUtil.escapeSQL(tableName); + ResultSet tables = dbmd.getTables(null, null, tableName, null); + if (!tables.next()) { + tables = dbmd.getTables(null, null, + tableName.toUpperCase(), null); + if (!tables.next()) { + throw new IllegalArgumentException( + "Table with the name \"" + + tableName + + "\" was not found. Check your database contents."); + } else { + tableName = tableName.toUpperCase(); + } + } + tables.close(); + ResultSet rs = dbmd.getPrimaryKeys(null, null, tableName); + List<String> names = new ArrayList<String>(); + while (rs.next()) { + names.add(rs.getString("COLUMN_NAME")); + } + rs.close(); + if (!names.isEmpty()) { + primaryKeyColumns = names; + } + if (primaryKeyColumns == null || primaryKeyColumns.isEmpty()) { + throw new IllegalArgumentException( + "Primary key constraints have not been defined for the table \"" + + tableName + + "\". Use FreeFormQuery to access this table."); + } + for (String colName : primaryKeyColumns) { + if (colName.equalsIgnoreCase("rownum")) { + if (getSqlGenerator() instanceof MSSQLGenerator + || getSqlGenerator() instanceof MSSQLGenerator) { + throw new IllegalArgumentException( + "When using Oracle or MSSQL, a primary key column" + + " named \'rownum\' is not allowed!"); + } + } + } + } + } catch (SQLException e) { + throw new RuntimeException(e); + } finally { + connectionPool.releaseConnection(c); + } + } + + private RowId getNewRowId(RowItem row, ResultSet genKeys) { + try { + /* Fetch primary key values and generate a map out of them. */ + Map<String, Object> values = new HashMap<String, Object>(); + ResultSetMetaData rsmd = genKeys.getMetaData(); + int colCount = rsmd.getColumnCount(); + if (genKeys.next()) { + for (int i = 1; i <= colCount; i++) { + values.put(rsmd.getColumnName(i), genKeys.getObject(i)); + } + } + /* Generate new RowId */ + List<Object> newRowId = new ArrayList<Object>(); + if (values.size() == 1) { + if (primaryKeyColumns.size() == 1) { + newRowId.add(values.get(values.keySet().iterator().next())); + } else { + for (String s : primaryKeyColumns) { + if (!((ColumnProperty) row.getItemProperty(s)) + .isReadOnlyChangeAllowed()) { + newRowId.add(values.get(values.keySet().iterator() + .next())); + } else { + newRowId.add(values.get(s)); + } + } + } + } else { + for (String s : primaryKeyColumns) { + newRowId.add(values.get(s)); + } + } + return new RowId(newRowId.toArray()); + } catch (Exception e) { + getLogger().log(Level.FINE, + "Failed to fetch key values on insert: " + e.getMessage()); + return null; + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.sqlcontainer.query.QueryDelegate#removeRow(com.vaadin + * .addon.sqlcontainer.RowItem) + */ + @Override + public boolean removeRow(RowItem row) throws UnsupportedOperationException, + SQLException { + getLogger().log(Level.FINE, + "Removing row with id: " + row.getId().getId()[0].toString()); + if (executeUpdate(sqlGenerator.generateDeleteQuery(getTableName(), + primaryKeyColumns, versionColumn, row)) == 1) { + return true; + } + if (versionColumn != null) { + throw new OptimisticLockException( + "Someone else changed the row that was being deleted.", + row.getId()); + } + return false; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.sqlcontainer.query.QueryDelegate#containsRowWithKey( + * java.lang.Object[]) + */ + @Override + public boolean containsRowWithKey(Object... keys) throws SQLException { + ArrayList<Filter> filtersAndKeys = new ArrayList<Filter>(); + if (filters != null) { + filtersAndKeys.addAll(filters); + } + int ix = 0; + for (String colName : primaryKeyColumns) { + filtersAndKeys.add(new Equal(colName, keys[ix])); + ix++; + } + StatementHelper sh = sqlGenerator.generateSelectQuery(tableName, + filtersAndKeys, orderBys, 0, 0, "*"); + + boolean shouldCloseTransaction = false; + if (!transactionOpen) { + shouldCloseTransaction = true; + beginTransaction(); + } + ResultSet rs = null; + try { + rs = executeQuery(sh); + boolean contains = rs.next(); + return contains; + } finally { + if (rs != null) { + if (rs.getStatement() != null) { + rs.getStatement().close(); + } + rs.close(); + } + if (shouldCloseTransaction) { + commit(); + } + } + } + + /** + * Custom writeObject to call rollback() if object is serialized. + */ + private void writeObject(java.io.ObjectOutputStream out) throws IOException { + try { + rollback(); + } catch (SQLException ignored) { + } + out.defaultWriteObject(); + } + + /** + * Simple RowIdChangeEvent implementation. + */ + public class RowIdChangeEvent extends EventObject implements + QueryDelegate.RowIdChangeEvent { + private final RowId oldId; + private final RowId newId; + + private RowIdChangeEvent(RowId oldId, RowId newId) { + super(oldId); + this.oldId = oldId; + this.newId = newId; + } + + @Override + public RowId getNewRowId() { + return newId; + } + + @Override + public RowId getOldRowId() { + return oldId; + } + } + + /** + * Adds RowIdChangeListener to this query + */ + @Override + public void addListener(RowIdChangeListener listener) { + if (rowIdChangeListeners == null) { + rowIdChangeListeners = new LinkedList<QueryDelegate.RowIdChangeListener>(); + } + rowIdChangeListeners.add(listener); + } + + /** + * Removes the given RowIdChangeListener from this query + */ + @Override + public void removeListener(RowIdChangeListener listener) { + if (rowIdChangeListeners != null) { + rowIdChangeListeners.remove(listener); + } + } + + private static final Logger getLogger() { + return Logger.getLogger(TableQuery.class.getName()); + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/DefaultSQLGenerator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/DefaultSQLGenerator.java new file mode 100644 index 0000000000..6485330541 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/DefaultSQLGenerator.java @@ -0,0 +1,367 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.sqlcontainer.ColumnProperty; +import com.vaadin.data.util.sqlcontainer.RowItem; +import com.vaadin.data.util.sqlcontainer.SQLUtil; +import com.vaadin.data.util.sqlcontainer.TemporaryRowId; +import com.vaadin.data.util.sqlcontainer.query.OrderBy; +import com.vaadin.data.util.sqlcontainer.query.generator.filter.QueryBuilder; +import com.vaadin.data.util.sqlcontainer.query.generator.filter.StringDecorator; + +/** + * Generates generic SQL that is supported by HSQLDB, MySQL and PostgreSQL. + * + * @author Jonatan Kronqvist / Vaadin Ltd + */ +@SuppressWarnings("serial") +public class DefaultSQLGenerator implements SQLGenerator { + + private Class<? extends StatementHelper> statementHelperClass = null; + + public DefaultSQLGenerator() { + + } + + /** + * Create a new DefaultSqlGenerator instance that uses the given + * implementation of {@link StatementHelper} + * + * @param statementHelper + */ + public DefaultSQLGenerator( + Class<? extends StatementHelper> statementHelperClazz) { + this(); + statementHelperClass = statementHelperClazz; + } + + /** + * Construct a DefaultSQLGenerator with the specified identifiers for start + * and end of quoted strings. The identifiers may be different depending on + * the database engine and it's settings. + * + * @param quoteStart + * the identifier (character) denoting the start of a quoted + * string + * @param quoteEnd + * the identifier (character) denoting the end of a quoted string + */ + public DefaultSQLGenerator(String quoteStart, String quoteEnd) { + QueryBuilder.setStringDecorator(new StringDecorator(quoteStart, + quoteEnd)); + } + + /** + * Same as {@link #DefaultSQLGenerator(String, String)} but with support for + * custom {@link StatementHelper} implementation. + * + * @param quoteStart + * @param quoteEnd + * @param statementHelperClazz + */ + public DefaultSQLGenerator(String quoteStart, String quoteEnd, + Class<? extends StatementHelper> statementHelperClazz) { + this(quoteStart, quoteEnd); + statementHelperClass = statementHelperClazz; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.generator.SQLGenerator# + * generateSelectQuery(java.lang.String, java.util.List, java.util.List, + * int, int, java.lang.String) + */ + @Override + public StatementHelper generateSelectQuery(String tableName, + List<Filter> filters, List<OrderBy> orderBys, int offset, + int pagelength, String toSelect) { + if (tableName == null || tableName.trim().equals("")) { + throw new IllegalArgumentException("Table name must be given."); + } + toSelect = toSelect == null ? "*" : toSelect; + StatementHelper sh = getStatementHelper(); + StringBuffer query = new StringBuffer(); + query.append("SELECT " + toSelect + " FROM ").append( + SQLUtil.escapeSQL(tableName)); + if (filters != null) { + query.append(QueryBuilder.getWhereStringForFilters(filters, sh)); + } + if (orderBys != null) { + for (OrderBy o : orderBys) { + generateOrderBy(query, o, orderBys.indexOf(o) == 0); + } + } + if (pagelength != 0) { + generateLimits(query, offset, pagelength); + } + sh.setQueryString(query.toString()); + return sh; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.generator.SQLGenerator# + * generateUpdateQuery(java.lang.String, + * com.vaadin.addon.sqlcontainer.RowItem) + */ + @Override + public StatementHelper generateUpdateQuery(String tableName, RowItem item) { + if (tableName == null || tableName.trim().equals("")) { + throw new IllegalArgumentException("Table name must be given."); + } + if (item == null) { + throw new IllegalArgumentException("Updated item must be given."); + } + StatementHelper sh = getStatementHelper(); + StringBuffer query = new StringBuffer(); + query.append("UPDATE ").append(tableName).append(" SET"); + + /* Generate column<->value and rowidentifiers map */ + Map<String, Object> columnToValueMap = generateColumnToValueMap(item); + Map<String, Object> rowIdentifiers = generateRowIdentifiers(item); + /* Generate columns and values to update */ + boolean first = true; + for (String column : columnToValueMap.keySet()) { + if (first) { + query.append(" " + QueryBuilder.quote(column) + " = ?"); + } else { + query.append(", " + QueryBuilder.quote(column) + " = ?"); + } + sh.addParameterValue(columnToValueMap.get(column), item + .getItemProperty(column).getType()); + first = false; + } + /* Generate identifiers for the row to be updated */ + first = true; + for (String column : rowIdentifiers.keySet()) { + if (first) { + query.append(" WHERE " + QueryBuilder.quote(column) + " = ?"); + } else { + query.append(" AND " + QueryBuilder.quote(column) + " = ?"); + } + sh.addParameterValue(rowIdentifiers.get(column), item + .getItemProperty(column).getType()); + first = false; + } + sh.setQueryString(query.toString()); + return sh; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.generator.SQLGenerator# + * generateInsertQuery(java.lang.String, + * com.vaadin.addon.sqlcontainer.RowItem) + */ + @Override + public StatementHelper generateInsertQuery(String tableName, RowItem item) { + if (tableName == null || tableName.trim().equals("")) { + throw new IllegalArgumentException("Table name must be given."); + } + if (item == null) { + throw new IllegalArgumentException("New item must be given."); + } + if (!(item.getId() instanceof TemporaryRowId)) { + throw new IllegalArgumentException( + "Cannot generate an insert query for item already in database."); + } + StatementHelper sh = getStatementHelper(); + StringBuffer query = new StringBuffer(); + query.append("INSERT INTO ").append(tableName).append(" ("); + + /* Generate column<->value map */ + Map<String, Object> columnToValueMap = generateColumnToValueMap(item); + /* Generate column names for insert query */ + boolean first = true; + for (String column : columnToValueMap.keySet()) { + if (!first) { + query.append(", "); + } + query.append(QueryBuilder.quote(column)); + first = false; + } + + /* Generate values for insert query */ + query.append(") VALUES ("); + first = true; + for (String column : columnToValueMap.keySet()) { + if (!first) { + query.append(", "); + } + query.append("?"); + sh.addParameterValue(columnToValueMap.get(column), item + .getItemProperty(column).getType()); + first = false; + } + query.append(")"); + sh.setQueryString(query.toString()); + return sh; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.generator.SQLGenerator# + * generateDeleteQuery(java.lang.String, + * com.vaadin.addon.sqlcontainer.RowItem) + */ + @Override + public StatementHelper generateDeleteQuery(String tableName, + List<String> primaryKeyColumns, String versionColumn, RowItem item) { + if (tableName == null || tableName.trim().equals("")) { + throw new IllegalArgumentException("Table name must be given."); + } + if (item == null) { + throw new IllegalArgumentException( + "Item to be deleted must be given."); + } + if (primaryKeyColumns == null || primaryKeyColumns.isEmpty()) { + throw new IllegalArgumentException( + "Valid keyColumnNames must be provided."); + } + StatementHelper sh = getStatementHelper(); + StringBuffer query = new StringBuffer(); + query.append("DELETE FROM ").append(tableName).append(" WHERE "); + int count = 1; + for (String keyColName : primaryKeyColumns) { + if ((this instanceof MSSQLGenerator || this instanceof OracleGenerator) + && keyColName.equalsIgnoreCase("rownum")) { + count++; + continue; + } + if (count > 1) { + query.append(" AND "); + } + if (item.getItemProperty(keyColName).getValue() != null) { + query.append(QueryBuilder.quote(keyColName) + " = ?"); + sh.addParameterValue(item.getItemProperty(keyColName) + .getValue(), item.getItemProperty(keyColName).getType()); + } + count++; + } + if (versionColumn != null) { + query.append(String.format(" AND %s = ?", + QueryBuilder.quote(versionColumn))); + sh.addParameterValue( + item.getItemProperty(versionColumn).getValue(), item + .getItemProperty(versionColumn).getType()); + } + + sh.setQueryString(query.toString()); + return sh; + } + + /** + * Generates sorting rules as an ORDER BY -clause + * + * @param sb + * StringBuffer to which the clause is appended. + * @param o + * OrderBy object to be added into the sb. + * @param firstOrderBy + * If true, this is the first OrderBy. + * @return + */ + protected StringBuffer generateOrderBy(StringBuffer sb, OrderBy o, + boolean firstOrderBy) { + if (firstOrderBy) { + sb.append(" ORDER BY "); + } else { + sb.append(", "); + } + sb.append(QueryBuilder.quote(o.getColumn())); + if (o.isAscending()) { + sb.append(" ASC"); + } else { + sb.append(" DESC"); + } + return sb; + } + + /** + * Generates the LIMIT and OFFSET clause. + * + * @param sb + * StringBuffer to which the clause is appended. + * @param offset + * Value for offset. + * @param pagelength + * Value for pagelength. + * @return StringBuffer with LIMIT and OFFSET clause added. + */ + protected StringBuffer generateLimits(StringBuffer sb, int offset, + int pagelength) { + sb.append(" LIMIT ").append(pagelength).append(" OFFSET ") + .append(offset); + return sb; + } + + protected Map<String, Object> generateColumnToValueMap(RowItem item) { + Map<String, Object> columnToValueMap = new HashMap<String, Object>(); + for (Object id : item.getItemPropertyIds()) { + ColumnProperty cp = (ColumnProperty) item.getItemProperty(id); + /* Prevent "rownum" usage as a column name if MSSQL or ORACLE */ + if ((this instanceof MSSQLGenerator || this instanceof OracleGenerator) + && cp.getPropertyId().equalsIgnoreCase("rownum")) { + continue; + } + Object value = cp.getValue() == null ? null : cp.getValue(); + /* Only include properties whose read-only status can be altered */ + if (cp.isReadOnlyChangeAllowed() && !cp.isVersionColumn()) { + columnToValueMap.put(cp.getPropertyId(), value); + } + } + return columnToValueMap; + } + + protected Map<String, Object> generateRowIdentifiers(RowItem item) { + Map<String, Object> rowIdentifiers = new HashMap<String, Object>(); + for (Object id : item.getItemPropertyIds()) { + ColumnProperty cp = (ColumnProperty) item.getItemProperty(id); + /* Prevent "rownum" usage as a column name if MSSQL or ORACLE */ + if ((this instanceof MSSQLGenerator || this instanceof OracleGenerator) + && cp.getPropertyId().equalsIgnoreCase("rownum")) { + continue; + } + Object value = cp.getValue() == null ? null : cp.getValue(); + if (!cp.isReadOnlyChangeAllowed() || cp.isVersionColumn()) { + rowIdentifiers.put(cp.getPropertyId(), value); + } + } + return rowIdentifiers; + } + + /** + * Returns the statement helper for the generator. Override this to handle + * platform specific data types. + * + * @see http://dev.vaadin.com/ticket/9148 + * @return a new instance of the statement helper + */ + protected StatementHelper getStatementHelper() { + if (statementHelperClass == null) { + return new StatementHelper(); + } + + try { + return statementHelperClass.newInstance(); + } catch (InstantiationException e) { + throw new RuntimeException( + "Unable to instantiate custom StatementHelper", e); + } catch (IllegalAccessException e) { + throw new RuntimeException( + "Unable to instantiate custom StatementHelper", e); + } + } + +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/MSSQLGenerator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/MSSQLGenerator.java new file mode 100644 index 0000000000..13ef1d0090 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/MSSQLGenerator.java @@ -0,0 +1,101 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator; + +import java.util.List; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.sqlcontainer.query.OrderBy; +import com.vaadin.data.util.sqlcontainer.query.generator.filter.QueryBuilder; + +@SuppressWarnings("serial") +public class MSSQLGenerator extends DefaultSQLGenerator { + + public MSSQLGenerator() { + + } + + /** + * Construct a MSSQLGenerator with the specified identifiers for start and + * end of quoted strings. The identifiers may be different depending on the + * database engine and it's settings. + * + * @param quoteStart + * the identifier (character) denoting the start of a quoted + * string + * @param quoteEnd + * the identifier (character) denoting the end of a quoted string + */ + public MSSQLGenerator(String quoteStart, String quoteEnd) { + super(quoteStart, quoteEnd); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.generator.DefaultSQLGenerator# + * generateSelectQuery(java.lang.String, java.util.List, + * com.vaadin.addon.sqlcontainer.query.FilteringMode, java.util.List, int, + * int, java.lang.String) + */ + @Override + public StatementHelper generateSelectQuery(String tableName, + List<Filter> filters, List<OrderBy> orderBys, int offset, + int pagelength, String toSelect) { + if (tableName == null || tableName.trim().equals("")) { + throw new IllegalArgumentException("Table name must be given."); + } + /* Adjust offset and page length parameters to match "row numbers" */ + offset = pagelength > 1 ? ++offset : offset; + pagelength = pagelength > 1 ? --pagelength : pagelength; + toSelect = toSelect == null ? "*" : toSelect; + StatementHelper sh = getStatementHelper(); + StringBuffer query = new StringBuffer(); + + /* Row count request is handled here */ + if ("COUNT(*)".equalsIgnoreCase(toSelect)) { + query.append(String.format( + "SELECT COUNT(*) AS %s FROM (SELECT * FROM %s", + QueryBuilder.quote("rowcount"), tableName)); + if (filters != null && !filters.isEmpty()) { + query.append(QueryBuilder.getWhereStringForFilters(filters, sh)); + } + query.append(") AS t"); + sh.setQueryString(query.toString()); + return sh; + } + + /* SELECT without row number constraints */ + if (offset == 0 && pagelength == 0) { + query.append("SELECT ").append(toSelect).append(" FROM ") + .append(tableName); + if (filters != null) { + query.append(QueryBuilder.getWhereStringForFilters(filters, sh)); + } + if (orderBys != null) { + for (OrderBy o : orderBys) { + generateOrderBy(query, o, orderBys.indexOf(o) == 0); + } + } + sh.setQueryString(query.toString()); + return sh; + } + + /* Remaining SELECT cases are handled here */ + query.append("SELECT * FROM (SELECT row_number() OVER ("); + if (orderBys != null) { + for (OrderBy o : orderBys) { + generateOrderBy(query, o, orderBys.indexOf(o) == 0); + } + } + query.append(") AS rownum, " + toSelect + " FROM ").append(tableName); + if (filters != null) { + query.append(QueryBuilder.getWhereStringForFilters(filters, sh)); + } + query.append(") AS a WHERE a.rownum BETWEEN ").append(offset) + .append(" AND ").append(Integer.toString(offset + pagelength)); + sh.setQueryString(query.toString()); + return sh; + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/OracleGenerator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/OracleGenerator.java new file mode 100644 index 0000000000..43a562d3a8 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/OracleGenerator.java @@ -0,0 +1,112 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator; + +import java.util.List; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.sqlcontainer.query.OrderBy; +import com.vaadin.data.util.sqlcontainer.query.generator.filter.QueryBuilder; + +@SuppressWarnings("serial") +public class OracleGenerator extends DefaultSQLGenerator { + + public OracleGenerator() { + + } + + public OracleGenerator(Class<? extends StatementHelper> statementHelperClazz) { + super(statementHelperClazz); + } + + /** + * Construct an OracleSQLGenerator with the specified identifiers for start + * and end of quoted strings. The identifiers may be different depending on + * the database engine and it's settings. + * + * @param quoteStart + * the identifier (character) denoting the start of a quoted + * string + * @param quoteEnd + * the identifier (character) denoting the end of a quoted string + */ + public OracleGenerator(String quoteStart, String quoteEnd) { + super(quoteStart, quoteEnd); + } + + public OracleGenerator(String quoteStart, String quoteEnd, + Class<? extends StatementHelper> statementHelperClazz) { + super(quoteStart, quoteEnd, statementHelperClazz); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.generator.DefaultSQLGenerator# + * generateSelectQuery(java.lang.String, java.util.List, + * com.vaadin.addon.sqlcontainer.query.FilteringMode, java.util.List, int, + * int, java.lang.String) + */ + @Override + public StatementHelper generateSelectQuery(String tableName, + List<Filter> filters, List<OrderBy> orderBys, int offset, + int pagelength, String toSelect) { + if (tableName == null || tableName.trim().equals("")) { + throw new IllegalArgumentException("Table name must be given."); + } + /* Adjust offset and page length parameters to match "row numbers" */ + offset = pagelength > 1 ? ++offset : offset; + pagelength = pagelength > 1 ? --pagelength : pagelength; + toSelect = toSelect == null ? "*" : toSelect; + StatementHelper sh = getStatementHelper(); + StringBuffer query = new StringBuffer(); + + /* Row count request is handled here */ + if ("COUNT(*)".equalsIgnoreCase(toSelect)) { + query.append(String.format( + "SELECT COUNT(*) AS %s FROM (SELECT * FROM %s", + QueryBuilder.quote("rowcount"), tableName)); + if (filters != null && !filters.isEmpty()) { + query.append(QueryBuilder.getWhereStringForFilters(filters, sh)); + } + query.append(")"); + sh.setQueryString(query.toString()); + return sh; + } + + /* SELECT without row number constraints */ + if (offset == 0 && pagelength == 0) { + query.append("SELECT ").append(toSelect).append(" FROM ") + .append(tableName); + if (filters != null) { + query.append(QueryBuilder.getWhereStringForFilters(filters, sh)); + } + if (orderBys != null) { + for (OrderBy o : orderBys) { + generateOrderBy(query, o, orderBys.indexOf(o) == 0); + } + } + sh.setQueryString(query.toString()); + return sh; + } + + /* Remaining SELECT cases are handled here */ + query.append(String + .format("SELECT * FROM (SELECT x.*, ROWNUM AS %s FROM (SELECT %s FROM %s", + QueryBuilder.quote("rownum"), toSelect, tableName)); + if (filters != null) { + query.append(QueryBuilder.getWhereStringForFilters(filters, sh)); + } + if (orderBys != null) { + for (OrderBy o : orderBys) { + generateOrderBy(query, o, orderBys.indexOf(o) == 0); + } + } + query.append(String.format(") x) WHERE %s BETWEEN %d AND %d", + QueryBuilder.quote("rownum"), offset, offset + pagelength)); + sh.setQueryString(query.toString()); + return sh; + } + +}
\ No newline at end of file diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/SQLGenerator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/SQLGenerator.java new file mode 100644 index 0000000000..dde7077eee --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/SQLGenerator.java @@ -0,0 +1,88 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator; + +import java.io.Serializable; +import java.util.List; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.sqlcontainer.RowItem; +import com.vaadin.data.util.sqlcontainer.query.OrderBy; + +/** + * The SQLGenerator interface is meant to be implemented for each different SQL + * syntax that is to be supported. By default there are implementations for + * HSQLDB, MySQL, PostgreSQL, MSSQL and Oracle syntaxes. + * + * @author Jonatan Kronqvist / Vaadin Ltd + */ +public interface SQLGenerator extends Serializable { + /** + * Generates a SELECT query with the provided parameters. Uses default + * filtering mode (INCLUSIVE). + * + * @param tableName + * Name of the table queried + * @param filters + * The filters, converted into a WHERE clause + * @param orderBys + * The the ordering conditions, converted into an ORDER BY clause + * @param offset + * The offset of the first row to be included + * @param pagelength + * The number of rows to be returned when the query executes + * @param toSelect + * String containing what to select, e.g. "*", "COUNT(*)" + * @return StatementHelper instance containing the query string for a + * PreparedStatement and the values required for the parameters + */ + public StatementHelper generateSelectQuery(String tableName, + List<Filter> filters, List<OrderBy> orderBys, int offset, + int pagelength, String toSelect); + + /** + * Generates an UPDATE query with the provided parameters. + * + * @param tableName + * Name of the table queried + * @param item + * RowItem containing the updated values update. + * @return StatementHelper instance containing the query string for a + * PreparedStatement and the values required for the parameters + */ + public StatementHelper generateUpdateQuery(String tableName, RowItem item); + + /** + * Generates an INSERT query for inserting a new row with the provided + * values. + * + * @param tableName + * Name of the table queried + * @param item + * New RowItem to be inserted into the database. + * @return StatementHelper instance containing the query string for a + * PreparedStatement and the values required for the parameters + */ + public StatementHelper generateInsertQuery(String tableName, RowItem item); + + /** + * Generates a DELETE query for deleting data related to the given RowItem + * from the database. + * + * @param tableName + * Name of the table queried + * @param primaryKeyColumns + * the names of the columns holding the primary key. Usually just + * one column, but might be several. + * @param versionColumn + * the column containing the version number of the row, null if + * versioning (optimistic locking) not enabled. + * @param item + * Item to be deleted from the database + * @return StatementHelper instance containing the query string for a + * PreparedStatement and the values required for the parameters + */ + public StatementHelper generateDeleteQuery(String tableName, + List<String> primaryKeyColumns, String versionColumn, RowItem item); +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/StatementHelper.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/StatementHelper.java new file mode 100644 index 0000000000..b012ce7685 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/StatementHelper.java @@ -0,0 +1,163 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Time; +import java.sql.Timestamp; +import java.sql.Types; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * StatementHelper is a simple helper class that assists TableQuery and the + * query generators in filling a PreparedStatement. The actual statement is + * generated by the query generator methods, but the resulting statement and all + * the parameter values are stored in an instance of StatementHelper. + * + * This class will also fill the values with correct setters into the + * PreparedStatement on request. + */ +public class StatementHelper implements Serializable { + + private String queryString; + + private List<Object> parameters = new ArrayList<Object>(); + private Map<Integer, Class<?>> dataTypes = new HashMap<Integer, Class<?>>(); + + public StatementHelper() { + } + + public void setQueryString(String queryString) { + this.queryString = queryString; + } + + public String getQueryString() { + return queryString; + } + + public void addParameterValue(Object parameter) { + if (parameter != null) { + parameters.add(parameter); + dataTypes.put(parameters.size() - 1, parameter.getClass()); + } else { + throw new IllegalArgumentException( + "You cannot add null parameters using addParamaters(Object). " + + "Use addParameters(Object,Class) instead"); + } + } + + public void addParameterValue(Object parameter, Class<?> type) { + parameters.add(parameter); + dataTypes.put(parameters.size() - 1, type); + } + + public void setParameterValuesToStatement(PreparedStatement pstmt) + throws SQLException { + for (int i = 0; i < parameters.size(); i++) { + if (parameters.get(i) == null) { + handleNullValue(i, pstmt); + } else { + pstmt.setObject(i + 1, parameters.get(i)); + } + } + + /* + * The following list contains the data types supported by + * PreparedStatement but not supported by SQLContainer: + * + * [The list is provided as PreparedStatement method signatures] + * + * setNCharacterStream(int parameterIndex, Reader value) + * + * setNClob(int parameterIndex, NClob value) + * + * setNString(int parameterIndex, String value) + * + * setRef(int parameterIndex, Ref x) + * + * setRowId(int parameterIndex, RowId x) + * + * setSQLXML(int parameterIndex, SQLXML xmlObject) + * + * setBytes(int parameterIndex, byte[] x) + * + * setCharacterStream(int parameterIndex, Reader reader) + * + * setClob(int parameterIndex, Clob x) + * + * setURL(int parameterIndex, URL x) + * + * setArray(int parameterIndex, Array x) + * + * setAsciiStream(int parameterIndex, InputStream x) + * + * setBinaryStream(int parameterIndex, InputStream x) + * + * setBlob(int parameterIndex, Blob x) + */ + } + + private void handleNullValue(int i, PreparedStatement pstmt) + throws SQLException { + if (BigDecimal.class.equals(dataTypes.get(i))) { + pstmt.setBigDecimal(i + 1, null); + } else if (Boolean.class.equals(dataTypes.get(i))) { + pstmt.setNull(i + 1, Types.BOOLEAN); + } else if (Byte.class.equals(dataTypes.get(i))) { + pstmt.setNull(i + 1, Types.SMALLINT); + } else if (Date.class.equals(dataTypes.get(i))) { + pstmt.setDate(i + 1, null); + } else if (Double.class.equals(dataTypes.get(i))) { + pstmt.setNull(i + 1, Types.DOUBLE); + } else if (Float.class.equals(dataTypes.get(i))) { + pstmt.setNull(i + 1, Types.FLOAT); + } else if (Integer.class.equals(dataTypes.get(i))) { + pstmt.setNull(i + 1, Types.INTEGER); + } else if (Long.class.equals(dataTypes.get(i))) { + pstmt.setNull(i + 1, Types.BIGINT); + } else if (Short.class.equals(dataTypes.get(i))) { + pstmt.setNull(i + 1, Types.SMALLINT); + } else if (String.class.equals(dataTypes.get(i))) { + pstmt.setString(i + 1, null); + } else if (Time.class.equals(dataTypes.get(i))) { + pstmt.setTime(i + 1, null); + } else if (Timestamp.class.equals(dataTypes.get(i))) { + pstmt.setTimestamp(i + 1, null); + } else { + + if (handleUnrecognizedTypeNullValue(i, pstmt, dataTypes)) { + return; + } + + throw new SQLException("Data type not supported by SQLContainer: " + + parameters.get(i).getClass().toString()); + } + } + + /** + * Handle unrecognized null values. Override this to handle null values for + * platform specific data types that are not handled by the default + * implementation of the {@link StatementHelper}. + * + * @param i + * @param pstmt + * @param dataTypes2 + * + * @return true if handled, false otherwise + * + * @see {@link http://dev.vaadin.com/ticket/9148} + */ + protected boolean handleUnrecognizedTypeNullValue(int i, + PreparedStatement pstmt, Map<Integer, Class<?>> dataTypes) + throws SQLException { + return false; + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/AndTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/AndTranslator.java new file mode 100644 index 0000000000..251a543a8a --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/AndTranslator.java @@ -0,0 +1,23 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.And; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public class AndTranslator implements FilterTranslator { + + @Override + public boolean translatesFilter(Filter filter) { + return filter instanceof And; + } + + @Override + public String getWhereStringForFilter(Filter filter, StatementHelper sh) { + return QueryBuilder.group(QueryBuilder.getJoinedFilterString( + ((And) filter).getFilters(), "AND", sh)); + } + +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/BetweenTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/BetweenTranslator.java new file mode 100644 index 0000000000..4fcaf759ea --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/BetweenTranslator.java @@ -0,0 +1,25 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.Between; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public class BetweenTranslator implements FilterTranslator { + + @Override + public boolean translatesFilter(Filter filter) { + return filter instanceof Between; + } + + @Override + public String getWhereStringForFilter(Filter filter, StatementHelper sh) { + Between between = (Between) filter; + sh.addParameterValue(between.getStartValue()); + sh.addParameterValue(between.getEndValue()); + return QueryBuilder.quote(between.getPropertyId()) + " BETWEEN ? AND ?"; + } + +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/CompareTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/CompareTranslator.java new file mode 100644 index 0000000000..4293e1d630 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/CompareTranslator.java @@ -0,0 +1,38 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.Compare; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public class CompareTranslator implements FilterTranslator { + + @Override + public boolean translatesFilter(Filter filter) { + return filter instanceof Compare; + } + + @Override + public String getWhereStringForFilter(Filter filter, StatementHelper sh) { + Compare compare = (Compare) filter; + sh.addParameterValue(compare.getValue()); + String prop = QueryBuilder.quote(compare.getPropertyId()); + switch (compare.getOperation()) { + case EQUAL: + return prop + " = ?"; + case GREATER: + return prop + " > ?"; + case GREATER_OR_EQUAL: + return prop + " >= ?"; + case LESS: + return prop + " < ?"; + case LESS_OR_EQUAL: + return prop + " <= ?"; + default: + return ""; + } + } + +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/FilterTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/FilterTranslator.java new file mode 100644 index 0000000000..84af9d5c97 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/FilterTranslator.java @@ -0,0 +1,16 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator.filter; + +import java.io.Serializable; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public interface FilterTranslator extends Serializable { + public boolean translatesFilter(Filter filter); + + public String getWhereStringForFilter(Filter filter, StatementHelper sh); + +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/IsNullTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/IsNullTranslator.java new file mode 100644 index 0000000000..a2a6cd2c09 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/IsNullTranslator.java @@ -0,0 +1,22 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.IsNull; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public class IsNullTranslator implements FilterTranslator { + + @Override + public boolean translatesFilter(Filter filter) { + return filter instanceof IsNull; + } + + @Override + public String getWhereStringForFilter(Filter filter, StatementHelper sh) { + IsNull in = (IsNull) filter; + return QueryBuilder.quote(in.getPropertyId()) + " IS NULL"; + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/LikeTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/LikeTranslator.java new file mode 100644 index 0000000000..25a85caec0 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/LikeTranslator.java @@ -0,0 +1,30 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.Like; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public class LikeTranslator implements FilterTranslator { + + @Override + public boolean translatesFilter(Filter filter) { + return filter instanceof Like; + } + + @Override + public String getWhereStringForFilter(Filter filter, StatementHelper sh) { + Like like = (Like) filter; + if (like.isCaseSensitive()) { + sh.addParameterValue(like.getValue()); + return QueryBuilder.quote(like.getPropertyId()) + " LIKE ?"; + } else { + sh.addParameterValue(like.getValue().toUpperCase()); + return "UPPER(" + QueryBuilder.quote(like.getPropertyId()) + + ") LIKE ?"; + } + } + +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/NotTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/NotTranslator.java new file mode 100644 index 0000000000..5dfbe240e7 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/NotTranslator.java @@ -0,0 +1,29 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.IsNull; +import com.vaadin.data.util.filter.Not; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public class NotTranslator implements FilterTranslator { + + @Override + public boolean translatesFilter(Filter filter) { + return filter instanceof Not; + } + + @Override + public String getWhereStringForFilter(Filter filter, StatementHelper sh) { + Not not = (Not) filter; + if (not.getFilter() instanceof IsNull) { + IsNull in = (IsNull) not.getFilter(); + return QueryBuilder.quote(in.getPropertyId()) + " IS NOT NULL"; + } + return "NOT " + + QueryBuilder.getWhereStringForFilter(not.getFilter(), sh); + } + +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/OrTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/OrTranslator.java new file mode 100644 index 0000000000..2f0ed814e0 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/OrTranslator.java @@ -0,0 +1,23 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.Or; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public class OrTranslator implements FilterTranslator { + + @Override + public boolean translatesFilter(Filter filter) { + return filter instanceof Or; + } + + @Override + public String getWhereStringForFilter(Filter filter, StatementHelper sh) { + return QueryBuilder.group(QueryBuilder.getJoinedFilterString( + ((Or) filter).getFilters(), "OR", sh)); + } + +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/QueryBuilder.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/QueryBuilder.java new file mode 100644 index 0000000000..24be8963e0 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/QueryBuilder.java @@ -0,0 +1,98 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator.filter; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public class QueryBuilder implements Serializable { + + private static ArrayList<FilterTranslator> filterTranslators = new ArrayList<FilterTranslator>(); + private static StringDecorator stringDecorator = new StringDecorator("\"", + "\""); + + static { + /* Register all default filter translators */ + addFilterTranslator(new AndTranslator()); + addFilterTranslator(new OrTranslator()); + addFilterTranslator(new LikeTranslator()); + addFilterTranslator(new BetweenTranslator()); + addFilterTranslator(new CompareTranslator()); + addFilterTranslator(new NotTranslator()); + addFilterTranslator(new IsNullTranslator()); + addFilterTranslator(new SimpleStringTranslator()); + } + + public synchronized static void addFilterTranslator( + FilterTranslator translator) { + filterTranslators.add(translator); + } + + /** + * Allows specification of a custom ColumnQuoter instance that handles + * quoting of column names for the current DB dialect. + * + * @param decorator + * the ColumnQuoter instance to use. + */ + public static void setStringDecorator(StringDecorator decorator) { + stringDecorator = decorator; + } + + public static String quote(Object str) { + return stringDecorator.quote(str); + } + + public static String group(String str) { + return stringDecorator.group(str); + } + + /** + * Constructs and returns a string representing the filter that can be used + * in a WHERE clause. + * + * @param filter + * the filter to translate + * @param sh + * the statement helper to update with the value(s) of the filter + * @return a string representing the filter. + */ + public synchronized static String getWhereStringForFilter(Filter filter, + StatementHelper sh) { + for (FilterTranslator ft : filterTranslators) { + if (ft.translatesFilter(filter)) { + return ft.getWhereStringForFilter(filter, sh); + } + } + return ""; + } + + public static String getJoinedFilterString(Collection<Filter> filters, + String joinString, StatementHelper sh) { + StringBuilder result = new StringBuilder(); + for (Filter f : filters) { + result.append(getWhereStringForFilter(f, sh)); + result.append(" ").append(joinString).append(" "); + } + // Remove the last instance of joinString + result.delete(result.length() - joinString.length() - 2, + result.length()); + return result.toString(); + } + + public static String getWhereStringForFilters(List<Filter> filters, + StatementHelper sh) { + if (filters == null || filters.isEmpty()) { + return ""; + } + StringBuilder where = new StringBuilder(" WHERE "); + where.append(getJoinedFilterString(filters, "AND", sh)); + return where.toString(); + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/SimpleStringTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/SimpleStringTranslator.java new file mode 100644 index 0000000000..f108003535 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/SimpleStringTranslator.java @@ -0,0 +1,30 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.Like; +import com.vaadin.data.util.filter.SimpleStringFilter; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public class SimpleStringTranslator implements FilterTranslator { + + @Override + public boolean translatesFilter(Filter filter) { + return filter instanceof SimpleStringFilter; + } + + @Override + public String getWhereStringForFilter(Filter filter, StatementHelper sh) { + SimpleStringFilter ssf = (SimpleStringFilter) filter; + // Create a Like filter based on the SimpleStringFilter and execute the + // LikeTranslator + String likeStr = ssf.isOnlyMatchPrefix() ? ssf.getFilterString() + "%" + : "%" + ssf.getFilterString() + "%"; + Like like = new Like(ssf.getPropertyId().toString(), likeStr); + like.setCaseSensitive(!ssf.isIgnoreCase()); + return new LikeTranslator().getWhereStringForFilter(like, sh); + } + +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/StringDecorator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/StringDecorator.java new file mode 100644 index 0000000000..8d2eabb5bc --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/StringDecorator.java @@ -0,0 +1,58 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.query.generator.filter; + +import java.io.Serializable; + +/** + * The StringDecorator knows how to produce a quoted string using the specified + * quote start and quote end characters. It also handles grouping of a string + * (surrounding it in parenthesis). + * + * Extend this class if you need to support special characters for grouping + * (parenthesis). + * + * @author Vaadin Ltd + */ +public class StringDecorator implements Serializable { + + private final String quoteStart; + private final String quoteEnd; + + /** + * Constructs a StringDecorator that uses the quoteStart and quoteEnd + * characters to create quoted strings. + * + * @param quoteStart + * the character denoting the start of a quote. + * @param quoteEnd + * the character denoting the end of a quote. + */ + public StringDecorator(String quoteStart, String quoteEnd) { + this.quoteStart = quoteStart; + this.quoteEnd = quoteEnd; + } + + /** + * Surround a string with quote characters. + * + * @param str + * the string to quote + * @return the quoted string + */ + public String quote(Object str) { + return quoteStart + str + quoteEnd; + } + + /** + * Groups a string by surrounding it in parenthesis + * + * @param str + * the string to group + * @return the grouped string + */ + public String group(String str) { + return "(" + str + ")"; + } +} |