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 | |
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')
36 files changed, 5652 insertions, 0 deletions
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/CacheFlushNotifier.java b/server/src/com/vaadin/data/util/sqlcontainer/CacheFlushNotifier.java new file mode 100644 index 0000000000..788966048d --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/CacheFlushNotifier.java @@ -0,0 +1,92 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer; + +import java.io.Serializable; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +import com.vaadin.data.util.sqlcontainer.query.FreeformQuery; +import com.vaadin.data.util.sqlcontainer.query.QueryDelegate; +import com.vaadin.data.util.sqlcontainer.query.TableQuery; + +/** + * CacheFlushNotifier is a simple static notification mechanism to inform other + * SQLContainers that the contents of their caches may have become stale. + */ +class CacheFlushNotifier implements Serializable { + /* + * SQLContainer instance reference list and dead reference queue. Used for + * the cache flush notification feature. + */ + private static List<WeakReference<SQLContainer>> allInstances = new ArrayList<WeakReference<SQLContainer>>(); + private static ReferenceQueue<SQLContainer> deadInstances = new ReferenceQueue<SQLContainer>(); + + /** + * Adds the given SQLContainer to the cache flush notification receiver list + * + * @param c + * Container to add + */ + public static void addInstance(SQLContainer c) { + removeDeadReferences(); + if (c != null) { + allInstances.add(new WeakReference<SQLContainer>(c, deadInstances)); + } + } + + /** + * Removes dead references from instance list + */ + private static void removeDeadReferences() { + java.lang.ref.Reference<? extends SQLContainer> dead = deadInstances + .poll(); + while (dead != null) { + allInstances.remove(dead); + dead = deadInstances.poll(); + } + } + + /** + * Iterates through the instances and notifies containers which are + * connected to the same table or are using the same query string. + * + * @param c + * SQLContainer that issued the cache flush notification + */ + public static void notifyOfCacheFlush(SQLContainer c) { + removeDeadReferences(); + for (WeakReference<SQLContainer> wr : allInstances) { + if (wr.get() != null) { + SQLContainer wrc = wr.get(); + if (wrc == null) { + continue; + } + /* + * If the reference points to the container sending the + * notification, do nothing. + */ + if (wrc.equals(c)) { + continue; + } + /* Compare QueryDelegate types and tableName/queryString */ + QueryDelegate wrQd = wrc.getQueryDelegate(); + QueryDelegate qd = c.getQueryDelegate(); + if (wrQd instanceof TableQuery + && qd instanceof TableQuery + && ((TableQuery) wrQd).getTableName().equals( + ((TableQuery) qd).getTableName())) { + wrc.refresh(); + } else if (wrQd instanceof FreeformQuery + && qd instanceof FreeformQuery + && ((FreeformQuery) wrQd).getQueryString().equals( + ((FreeformQuery) qd).getQueryString())) { + wrc.refresh(); + } + } + } + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/CacheMap.java b/server/src/com/vaadin/data/util/sqlcontainer/CacheMap.java new file mode 100644 index 0000000000..839fceb3c2 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/CacheMap.java @@ -0,0 +1,31 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * CacheMap extends LinkedHashMap, adding the possibility to adjust maximum + * number of items. In SQLContainer this is used for RowItem -cache. Cache size + * will be two times the page length parameter of the container. + */ +class CacheMap<K, V> extends LinkedHashMap<K, V> { + private static final long serialVersionUID = 679999766473555231L; + private int cacheLimit = SQLContainer.CACHE_RATIO + * SQLContainer.DEFAULT_PAGE_LENGTH; + + @Override + protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { + return size() > cacheLimit; + } + + void setCacheLimit(int limit) { + cacheLimit = limit > 0 ? limit : SQLContainer.DEFAULT_PAGE_LENGTH; + } + + int getCacheLimit() { + return cacheLimit; + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/data/util/sqlcontainer/ColumnProperty.java b/server/src/com/vaadin/data/util/sqlcontainer/ColumnProperty.java new file mode 100644 index 0000000000..168bce1880 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/ColumnProperty.java @@ -0,0 +1,248 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer; + +import java.sql.Date; +import java.sql.Time; +import java.sql.Timestamp; + +import com.vaadin.data.Property; + +/** + * ColumnProperty represents the value of one column in a RowItem. In addition + * to the value, ColumnProperty also contains some basic column attributes such + * as nullability status, read-only status and data type. + * + * Note that depending on the QueryDelegate in use this does not necessarily map + * into an actual column in a database table. + */ +final public class ColumnProperty implements Property { + private static final long serialVersionUID = -3694463129581802457L; + + private RowItem owner; + + private String propertyId; + + private boolean readOnly; + private boolean allowReadOnlyChange = true; + private boolean nullable = true; + + private Object value; + private Object changedValue; + private Class<?> type; + + private boolean modified; + + private boolean versionColumn; + + /** + * Prevent instantiation without required parameters. + */ + @SuppressWarnings("unused") + private ColumnProperty() { + } + + public ColumnProperty(String propertyId, boolean readOnly, + boolean allowReadOnlyChange, boolean nullable, Object value, + Class<?> type) { + if (propertyId == null) { + throw new IllegalArgumentException("Properties must be named."); + } + if (type == null) { + throw new IllegalArgumentException("Property type must be set."); + } + this.propertyId = propertyId; + this.type = type; + this.value = value; + + this.allowReadOnlyChange = allowReadOnlyChange; + this.nullable = nullable; + this.readOnly = readOnly; + } + + @Override + public Object getValue() { + if (isModified()) { + return changedValue; + } + return value; + } + + @Override + public void setValue(Object newValue) throws ReadOnlyException { + if (newValue == null && !nullable) { + throw new NotNullableException( + "Null values are not allowed for this property."); + } + if (readOnly) { + throw new ReadOnlyException( + "Cannot set value for read-only property."); + } + + /* Check if this property is a date property. */ + boolean isDateProperty = Time.class.equals(getType()) + || Date.class.equals(getType()) + || Timestamp.class.equals(getType()); + + if (newValue != null) { + /* Handle SQL dates, times and Timestamps given as java.util.Date */ + if (isDateProperty) { + /* + * Try to get the millisecond value from the new value of this + * property. Possible type to convert from is java.util.Date. + */ + long millis = 0; + if (newValue instanceof java.util.Date) { + millis = ((java.util.Date) newValue).getTime(); + /* + * Create the new object based on the millisecond value, + * according to the type of this property. + */ + if (Time.class.equals(getType())) { + newValue = new Time(millis); + } else if (Date.class.equals(getType())) { + newValue = new Date(millis); + } else if (Timestamp.class.equals(getType())) { + newValue = new Timestamp(millis); + } + } + } + + if (!getType().isAssignableFrom(newValue.getClass())) { + throw new IllegalArgumentException( + "Illegal value type for ColumnProperty"); + } + + /* + * If the value to be set is the same that has already been set, do + * not set it again. + */ + if (isValueAlreadySet(newValue)) { + return; + } + } + + /* Set the new value and notify container of the change. */ + changedValue = newValue; + modified = true; + owner.getContainer().itemChangeNotification(owner); + } + + private boolean isValueAlreadySet(Object newValue) { + Object referenceValue = isModified() ? changedValue : value; + + return (isNullable() && newValue == null && referenceValue == null) + || newValue.equals(referenceValue); + } + + @Override + public Class<?> getType() { + return type; + } + + @Override + public boolean isReadOnly() { + return readOnly; + } + + public boolean isReadOnlyChangeAllowed() { + return allowReadOnlyChange; + } + + @Override + public void setReadOnly(boolean newStatus) { + if (allowReadOnlyChange) { + readOnly = newStatus; + } + } + + public String getPropertyId() { + return propertyId; + } + + /** + * Returns the value of the Property in human readable textual format. + * + * @see java.lang.Object#toString() + * @deprecated get the string representation from the value + */ + @Deprecated + @Override + public String toString() { + throw new UnsupportedOperationException( + "Use ColumnProperty.getValue() instead of ColumnProperty.toString()"); + } + + public void setOwner(RowItem owner) { + if (owner == null) { + throw new IllegalArgumentException("Owner can not be set to null."); + } + if (this.owner != null) { + throw new IllegalStateException( + "ColumnProperties can only be bound once."); + } + this.owner = owner; + } + + public boolean isModified() { + return modified; + } + + public boolean isVersionColumn() { + return versionColumn; + } + + public void setVersionColumn(boolean versionColumn) { + this.versionColumn = versionColumn; + } + + public boolean isNullable() { + return nullable; + } + + /** + * An exception that signals that a <code>null</code> value was passed to + * the <code>setValue</code> method, but the value of this property can not + * be set to <code>null</code>. + */ + @SuppressWarnings("serial") + public class NotNullableException extends RuntimeException { + + /** + * Constructs a new <code>NotNullableException</code> without a detail + * message. + */ + public NotNullableException() { + } + + /** + * Constructs a new <code>NotNullableException</code> with the specified + * detail message. + * + * @param msg + * the detail message + */ + public NotNullableException(String msg) { + super(msg); + } + + /** + * Constructs a new <code>NotNullableException</code> from another + * exception. + * + * @param cause + * The cause of the failure + */ + public NotNullableException(Throwable cause) { + super(cause); + } + } + + public void commit() { + if (isModified()) { + modified = false; + value = changedValue; + } + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/OptimisticLockException.java b/server/src/com/vaadin/data/util/sqlcontainer/OptimisticLockException.java new file mode 100644 index 0000000000..adfd439ac8 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/OptimisticLockException.java @@ -0,0 +1,38 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer; + +import com.vaadin.data.util.sqlcontainer.query.TableQuery; + +/** + * An OptimisticLockException is thrown when trying to update or delete a row + * that has been changed since last read from the database. + * + * OptimisticLockException is a runtime exception because optimistic locking is + * turned off by default, and as such will never be thrown in a default + * configuration. In order to turn on optimistic locking, you need to specify + * the version column in your TableQuery instance. + * + * @see TableQuery#setVersionColumn(String) + * + * @author Jonatan Kronqvist / Vaadin Ltd + */ +public class OptimisticLockException extends RuntimeException { + + private final RowId rowId; + + public OptimisticLockException(RowId rowId) { + super(); + this.rowId = rowId; + } + + public OptimisticLockException(String msg, RowId rowId) { + super(msg); + this.rowId = rowId; + } + + public RowId getRowId() { + return rowId; + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/ReadOnlyRowId.java b/server/src/com/vaadin/data/util/sqlcontainer/ReadOnlyRowId.java new file mode 100644 index 0000000000..c73ffce63a --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/ReadOnlyRowId.java @@ -0,0 +1,31 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer; + +public class ReadOnlyRowId extends RowId { + private static final long serialVersionUID = -2626764781642012467L; + private final Integer rowNum; + + public ReadOnlyRowId(int rowNum) { + super(); + this.rowNum = rowNum; + } + + @Override + public int hashCode() { + return rowNum.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || !(obj instanceof ReadOnlyRowId)) { + return false; + } + return rowNum.equals(((ReadOnlyRowId) obj).rowNum); + } + + public int getRowNum() { + return rowNum; + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/Reference.java b/server/src/com/vaadin/data/util/sqlcontainer/Reference.java new file mode 100644 index 0000000000..dea1aa87c0 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/Reference.java @@ -0,0 +1,56 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer; + +import java.io.Serializable; + +/** + * The reference class represents a simple [usually foreign key] reference to + * another SQLContainer. Actual foreign key reference in the database is not + * required, but it is recommended to make sure that certain constraints are + * followed. + */ +@SuppressWarnings("serial") +class Reference implements Serializable { + + /** + * The SQLContainer that this reference points to. + */ + private SQLContainer referencedContainer; + + /** + * The column ID/name in the referencing SQLContainer that contains the key + * used for the reference. + */ + private String referencingColumn; + + /** + * The column ID/name in the referenced SQLContainer that contains the key + * used for the reference. + */ + private String referencedColumn; + + /** + * Constructs a new reference to be used within the SQLContainer to + * reference another SQLContainer. + */ + Reference(SQLContainer referencedContainer, String referencingColumn, + String referencedColumn) { + this.referencedContainer = referencedContainer; + this.referencingColumn = referencingColumn; + this.referencedColumn = referencedColumn; + } + + SQLContainer getReferencedContainer() { + return referencedContainer; + } + + String getReferencingColumn() { + return referencingColumn; + } + + String getReferencedColumn() { + return referencedColumn; + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/RowId.java b/server/src/com/vaadin/data/util/sqlcontainer/RowId.java new file mode 100644 index 0000000000..925325134a --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/RowId.java @@ -0,0 +1,81 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer; + +import java.io.Serializable; + +/** + * RowId represents identifiers of a single database result set row. + * + * The data structure of a RowId is an Object array which contains the values of + * the primary key columns of the identified row. This allows easy equals() + * -comparison of RowItems. + */ +public class RowId implements Serializable { + private static final long serialVersionUID = -3161778404698901258L; + protected Object[] id; + + /** + * Prevent instantiation without required parameters. + */ + protected RowId() { + } + + public RowId(Object[] id) { + if (id == null) { + throw new IllegalArgumentException("id parameter must not be null!"); + } + this.id = id; + } + + public Object[] getId() { + return id; + } + + @Override + public int hashCode() { + int result = 31; + if (id != null) { + for (Object o : id) { + if (o != null) { + result += o.hashCode(); + } + } + } + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null || !(obj instanceof RowId)) { + return false; + } + Object[] compId = ((RowId) obj).getId(); + if (id == null && compId == null) { + return true; + } + if (id.length != compId.length) { + return false; + } + for (int i = 0; i < id.length; i++) { + if ((id[i] == null && compId[i] != null) + || (id[i] != null && !id[i].equals(compId[i]))) { + return false; + } + } + return true; + } + + @Override + public String toString() { + StringBuffer s = new StringBuffer(); + for (int i = 0; i < id.length; i++) { + s.append(id[i]); + if (i < id.length - 1) { + s.append("/"); + } + } + return s.toString(); + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/RowItem.java b/server/src/com/vaadin/data/util/sqlcontainer/RowItem.java new file mode 100644 index 0000000000..d613a06b63 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/RowItem.java @@ -0,0 +1,133 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +import com.vaadin.data.Item; +import com.vaadin.data.Property; + +/** + * RowItem represents one row of a result set obtained from a QueryDelegate. + * + * Note that depending on the QueryDelegate in use this does not necessarily map + * into an actual row in a database table. + */ +public final class RowItem implements Item { + private static final long serialVersionUID = -6228966439127951408L; + private SQLContainer container; + private RowId id; + private Collection<ColumnProperty> properties; + + /** + * Prevent instantiation without required parameters. + */ + @SuppressWarnings("unused") + private RowItem() { + } + + public RowItem(SQLContainer container, RowId id, + Collection<ColumnProperty> properties) { + if (container == null) { + throw new IllegalArgumentException("Container cannot be null."); + } + if (id == null) { + throw new IllegalArgumentException("Row ID cannot be null."); + } + this.container = container; + this.properties = properties; + /* Set this RowItem as owner to the properties */ + if (properties != null) { + for (ColumnProperty p : properties) { + p.setOwner(this); + } + } + this.id = id; + } + + @Override + public Property<?> getItemProperty(Object id) { + if (id instanceof String && id != null) { + for (ColumnProperty cp : properties) { + if (id.equals(cp.getPropertyId())) { + return cp; + } + } + } + return null; + } + + @Override + public Collection<?> getItemPropertyIds() { + Collection<String> ids = new ArrayList<String>(properties.size()); + for (ColumnProperty cp : properties) { + ids.add(cp.getPropertyId()); + } + return Collections.unmodifiableCollection(ids); + } + + /** + * Adding properties is not supported. Properties are generated by + * SQLContainer. + */ + @Override + public boolean addItemProperty(Object id, Property property) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /** + * Removing properties is not supported. Properties are generated by + * SQLContainer. + */ + @Override + public boolean removeItemProperty(Object id) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + public RowId getId() { + return id; + } + + public SQLContainer getContainer() { + return container; + } + + public boolean isModified() { + if (properties != null) { + for (ColumnProperty p : properties) { + if (p.isModified()) { + return true; + } + } + } + return false; + } + + @Override + public String toString() { + StringBuffer s = new StringBuffer(); + s.append("ID:"); + s.append(getId().toString()); + for (Object propId : getItemPropertyIds()) { + s.append("|"); + s.append(propId.toString()); + s.append(":"); + Object value = getItemProperty(propId).getValue(); + s.append((null != value) ? value.toString() : null); + } + return s.toString(); + } + + public void commit() { + if (properties != null) { + for (ColumnProperty p : properties) { + p.commit(); + } + } + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/SQLContainer.java b/server/src/com/vaadin/data/util/sqlcontainer/SQLContainer.java new file mode 100644 index 0000000000..5827390723 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/SQLContainer.java @@ -0,0 +1,1716 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer; + +import java.io.IOException; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.ConcurrentModificationException; +import java.util.Date; +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; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.util.filter.Compare.Equal; +import com.vaadin.data.util.filter.Like; +import com.vaadin.data.util.filter.UnsupportedFilterException; +import com.vaadin.data.util.sqlcontainer.query.OrderBy; +import com.vaadin.data.util.sqlcontainer.query.QueryDelegate; +import com.vaadin.data.util.sqlcontainer.query.QueryDelegate.RowIdChangeListener; +import com.vaadin.data.util.sqlcontainer.query.TableQuery; +import com.vaadin.data.util.sqlcontainer.query.generator.MSSQLGenerator; +import com.vaadin.data.util.sqlcontainer.query.generator.OracleGenerator; + +public class SQLContainer implements Container, Container.Filterable, + Container.Indexed, Container.Sortable, Container.ItemSetChangeNotifier { + + /** Query delegate */ + private QueryDelegate delegate; + /** Auto commit mode, default = false */ + private boolean autoCommit = false; + + /** Page length = number of items contained in one page */ + private int pageLength = DEFAULT_PAGE_LENGTH; + public static final int DEFAULT_PAGE_LENGTH = 100; + + /** Number of items to cache = CACHE_RATIO x pageLength */ + public static final int CACHE_RATIO = 2; + + /** Item and index caches */ + private final Map<Integer, RowId> itemIndexes = new HashMap<Integer, RowId>(); + private final CacheMap<RowId, RowItem> cachedItems = new CacheMap<RowId, RowItem>(); + + /** Container properties = column names, data types and statuses */ + private final List<String> propertyIds = new ArrayList<String>(); + private final Map<String, Class<?>> propertyTypes = new HashMap<String, Class<?>>(); + private final Map<String, Boolean> propertyReadOnly = new HashMap<String, Boolean>(); + private final Map<String, Boolean> propertyNullable = new HashMap<String, Boolean>(); + + /** Filters (WHERE) and sorters (ORDER BY) */ + private final List<Filter> filters = new ArrayList<Filter>(); + private final List<OrderBy> sorters = new ArrayList<OrderBy>(); + + /** + * Total number of items available in the data source using the current + * query, filters and sorters. + */ + private int size; + + /** + * Size updating logic. Do not update size from data source if it has been + * updated in the last sizeValidMilliSeconds milliseconds. + */ + private final int sizeValidMilliSeconds = 10000; + private boolean sizeDirty = true; + private Date sizeUpdated = new Date(); + + /** Starting row number of the currently fetched page */ + private int currentOffset; + + /** ItemSetChangeListeners */ + private LinkedList<Container.ItemSetChangeListener> itemSetChangeListeners; + + /** Temporary storage for modified items and items to be removed and added */ + private final Map<RowId, RowItem> removedItems = new HashMap<RowId, RowItem>(); + private final List<RowItem> addedItems = new ArrayList<RowItem>(); + private final List<RowItem> modifiedItems = new ArrayList<RowItem>(); + + /** List of references to other SQLContainers */ + private final Map<SQLContainer, Reference> references = new HashMap<SQLContainer, Reference>(); + + /** Cache flush notification system enabled. Disabled by default. */ + private boolean notificationsEnabled; + + /** + * Prevent instantiation without a QueryDelegate. + */ + @SuppressWarnings("unused") + private SQLContainer() { + } + + /** + * Creates and initializes SQLContainer using the given QueryDelegate + * + * @param delegate + * QueryDelegate implementation + * @throws SQLException + */ + public SQLContainer(QueryDelegate delegate) throws SQLException { + if (delegate == null) { + throw new IllegalArgumentException( + "QueryDelegate must not be null."); + } + this.delegate = delegate; + getPropertyIds(); + cachedItems.setCacheLimit(CACHE_RATIO * getPageLength()); + } + + /**************************************/ + /** Methods from interface Container **/ + /**************************************/ + + /** + * Note! If auto commit mode is enabled, this method will still return the + * temporary row ID assigned for the item. Implement + * QueryDelegate.RowIdChangeListener to receive the actual Row ID value + * after the addition has been committed. + * + * {@inheritDoc} + */ + + @Override + public Object addItem() throws UnsupportedOperationException { + Object emptyKey[] = new Object[delegate.getPrimaryKeyColumns().size()]; + RowId itemId = new TemporaryRowId(emptyKey); + // Create new empty column properties for the row item. + List<ColumnProperty> itemProperties = new ArrayList<ColumnProperty>(); + for (String propertyId : propertyIds) { + /* Default settings for new item properties. */ + itemProperties + .add(new ColumnProperty(propertyId, propertyReadOnly + .get(propertyId), + !propertyReadOnly.get(propertyId), propertyNullable + .get(propertyId), null, getType(propertyId))); + } + RowItem newRowItem = new RowItem(this, itemId, itemProperties); + + if (autoCommit) { + /* Add and commit instantly */ + try { + if (delegate instanceof TableQuery) { + itemId = ((TableQuery) delegate) + .storeRowImmediately(newRowItem); + } else { + delegate.beginTransaction(); + delegate.storeRow(newRowItem); + delegate.commit(); + } + refresh(); + if (notificationsEnabled) { + CacheFlushNotifier.notifyOfCacheFlush(this); + } + getLogger().log(Level.FINER, "Row added to DB..."); + return itemId; + } catch (SQLException e) { + getLogger().log(Level.WARNING, + "Failed to add row to DB. Rolling back.", e); + try { + delegate.rollback(); + } catch (SQLException ee) { + getLogger().log(Level.SEVERE, + "Failed to roll back row addition", e); + } + return null; + } + } else { + addedItems.add(newRowItem); + fireContentsChange(); + return itemId; + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#containsId(java.lang.Object) + */ + + @Override + public boolean containsId(Object itemId) { + if (itemId == null) { + return false; + } + + if (cachedItems.containsKey(itemId)) { + return true; + } else { + for (RowItem item : addedItems) { + if (item.getId().equals(itemId)) { + return itemPassesFilters(item); + } + } + } + if (removedItems.containsKey(itemId)) { + return false; + } + + if (itemId instanceof ReadOnlyRowId) { + int rowNum = ((ReadOnlyRowId) itemId).getRowNum(); + return rowNum >= 0 && rowNum < size; + } + + if (itemId instanceof RowId && !(itemId instanceof TemporaryRowId)) { + try { + return delegate.containsRowWithKey(((RowId) itemId).getId()); + } catch (Exception e) { + /* Query failed, just return false. */ + getLogger().log(Level.WARNING, "containsId query failed", e); + } + } + return false; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getContainerProperty(java.lang.Object, + * java.lang.Object) + */ + + @Override + public Property<?> getContainerProperty(Object itemId, Object propertyId) { + Item item = getItem(itemId); + if (item == null) { + return null; + } + return item.getItemProperty(propertyId); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getContainerPropertyIds() + */ + + @Override + public Collection<?> getContainerPropertyIds() { + return Collections.unmodifiableCollection(propertyIds); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getItem(java.lang.Object) + */ + + @Override + public Item getItem(Object itemId) { + if (!cachedItems.containsKey(itemId)) { + int index = indexOfId(itemId); + if (index >= size) { + // The index is in the added items + int offset = index - size; + RowItem item = addedItems.get(offset); + if (itemPassesFilters(item)) { + return item; + } else { + return null; + } + } else { + // load the item into cache + updateOffsetAndCache(index); + } + } + return cachedItems.get(itemId); + } + + /** + * Bypasses in-memory filtering to return items that are cached in memory. + * <em>NOTE</em>: This does not bypass database-level filtering. + * + * @param itemId + * the id of the item to retrieve. + * @return the item represented by itemId. + */ + public Item getItemUnfiltered(Object itemId) { + if (!cachedItems.containsKey(itemId)) { + for (RowItem item : addedItems) { + if (item.getId().equals(itemId)) { + return item; + } + } + } + return cachedItems.get(itemId); + } + + /** + * NOTE! Do not use this method if in any way avoidable. This method doesn't + * (and cannot) use lazy loading, which means that all rows in the database + * will be loaded into memory. + * + * {@inheritDoc} + */ + + @Override + public Collection<?> getItemIds() { + updateCount(); + ArrayList<RowId> ids = new ArrayList<RowId>(); + ResultSet rs = null; + try { + // Load ALL rows :( + delegate.beginTransaction(); + rs = delegate.getResults(0, 0); + List<String> pKeys = delegate.getPrimaryKeyColumns(); + while (rs.next()) { + RowId id = null; + if (pKeys.isEmpty()) { + /* Create a read only itemId */ + id = new ReadOnlyRowId(rs.getRow()); + } else { + /* Generate itemId for the row based on primary key(s) */ + Object[] itemId = new Object[pKeys.size()]; + for (int i = 0; i < pKeys.size(); i++) { + itemId[i] = rs.getObject(pKeys.get(i)); + } + id = new RowId(itemId); + } + if (id != null && !removedItems.containsKey(id)) { + ids.add(id); + } + } + rs.getStatement().close(); + rs.close(); + delegate.commit(); + } catch (SQLException e) { + getLogger().log(Level.WARNING, + "getItemIds() failed, rolling back.", e); + try { + delegate.rollback(); + } catch (SQLException e1) { + getLogger().log(Level.SEVERE, "Failed to roll back state", e1); + } + try { + rs.getStatement().close(); + rs.close(); + } catch (SQLException e1) { + getLogger().log(Level.WARNING, "Closing session failed", e1); + } + throw new RuntimeException("Failed to fetch item indexes.", e); + } + for (RowItem item : getFilteredAddedItems()) { + ids.add(item.getId()); + } + return Collections.unmodifiableCollection(ids); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getType(java.lang.Object) + */ + + @Override + public Class<?> getType(Object propertyId) { + if (!propertyIds.contains(propertyId)) { + return null; + } + return propertyTypes.get(propertyId); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#size() + */ + + @Override + public int size() { + updateCount(); + return size + sizeOfAddedItems() - removedItems.size(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeItem(java.lang.Object) + */ + + @Override + public boolean removeItem(Object itemId) + throws UnsupportedOperationException { + if (!containsId(itemId)) { + return false; + } + for (RowItem item : addedItems) { + if (item.getId().equals(itemId)) { + addedItems.remove(item); + fireContentsChange(); + return true; + } + } + + if (autoCommit) { + /* Remove and commit instantly. */ + Item i = getItem(itemId); + if (i == null) { + return false; + } + try { + delegate.beginTransaction(); + boolean success = delegate.removeRow((RowItem) i); + delegate.commit(); + refresh(); + if (notificationsEnabled) { + CacheFlushNotifier.notifyOfCacheFlush(this); + } + if (success) { + getLogger().log(Level.FINER, "Row removed from DB..."); + } + return success; + } catch (SQLException e) { + getLogger().log(Level.WARNING, + "Failed to remove row, rolling back", e); + try { + delegate.rollback(); + } catch (SQLException ee) { + /* Nothing can be done here */ + getLogger().log(Level.SEVERE, + "Failed to rollback row removal", ee); + } + return false; + } catch (OptimisticLockException e) { + getLogger().log(Level.WARNING, + "Failed to remove row, rolling back", e); + try { + delegate.rollback(); + } catch (SQLException ee) { + /* Nothing can be done here */ + getLogger().log(Level.SEVERE, + "Failed to rollback row removal", ee); + } + throw e; + } + } else { + removedItems.put((RowId) itemId, (RowItem) getItem(itemId)); + cachedItems.remove(itemId); + refresh(); + return true; + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeAllItems() + */ + + @Override + public boolean removeAllItems() throws UnsupportedOperationException { + if (autoCommit) { + /* Remove and commit instantly. */ + try { + delegate.beginTransaction(); + boolean success = true; + for (Object id : getItemIds()) { + if (!delegate.removeRow((RowItem) getItem(id))) { + success = false; + } + } + if (success) { + delegate.commit(); + getLogger().log(Level.FINER, "All rows removed from DB..."); + refresh(); + if (notificationsEnabled) { + CacheFlushNotifier.notifyOfCacheFlush(this); + } + } else { + delegate.rollback(); + } + return success; + } catch (SQLException e) { + getLogger().log(Level.WARNING, + "removeAllItems() failed, rolling back", e); + try { + delegate.rollback(); + } catch (SQLException ee) { + /* Nothing can be done here */ + getLogger().log(Level.SEVERE, "Failed to roll back", ee); + } + return false; + } catch (OptimisticLockException e) { + getLogger().log(Level.WARNING, + "removeAllItems() failed, rolling back", e); + try { + delegate.rollback(); + } catch (SQLException ee) { + /* Nothing can be done here */ + getLogger().log(Level.SEVERE, "Failed to roll back", ee); + } + throw e; + } + } else { + for (Object id : getItemIds()) { + removedItems.put((RowId) id, (RowItem) getItem(id)); + cachedItems.remove(id); + } + refresh(); + return true; + } + } + + /*************************************************/ + /** Methods from interface Container.Filterable **/ + /*************************************************/ + + /** + * {@inheritDoc} + */ + + @Override + public void addContainerFilter(Filter filter) + throws UnsupportedFilterException { + // filter.setCaseSensitive(!ignoreCase); + + filters.add(filter); + refresh(); + } + + /** + * {@inheritDoc} + */ + + @Override + public void removeContainerFilter(Filter filter) { + filters.remove(filter); + refresh(); + } + + /** + * {@inheritDoc} + */ + public void addContainerFilter(Object propertyId, String filterString, + boolean ignoreCase, boolean onlyMatchPrefix) { + if (propertyId == null || !propertyIds.contains(propertyId)) { + return; + } + + /* Generate Filter -object */ + String likeStr = onlyMatchPrefix ? filterString + "%" : "%" + + filterString + "%"; + Like like = new Like(propertyId.toString(), likeStr); + like.setCaseSensitive(!ignoreCase); + filters.add(like); + refresh(); + } + + /** + * {@inheritDoc} + */ + public void removeContainerFilters(Object propertyId) { + ArrayList<Filter> toRemove = new ArrayList<Filter>(); + for (Filter f : filters) { + if (f.appliesToProperty(propertyId)) { + toRemove.add(f); + } + } + filters.removeAll(toRemove); + refresh(); + } + + /** + * {@inheritDoc} + */ + + @Override + public void removeAllContainerFilters() { + filters.clear(); + refresh(); + } + + /**********************************************/ + /** Methods from interface Container.Indexed **/ + /**********************************************/ + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Indexed#indexOfId(java.lang.Object) + */ + + @Override + public int indexOfId(Object itemId) { + // First check if the id is in the added items + for (int ix = 0; ix < addedItems.size(); ix++) { + RowItem item = addedItems.get(ix); + if (item.getId().equals(itemId)) { + if (itemPassesFilters(item)) { + updateCount(); + return size + ix; + } else { + return -1; + } + } + } + + if (!containsId(itemId)) { + return -1; + } + if (cachedItems.isEmpty()) { + getPage(); + } + int size = size(); + boolean wrappedAround = false; + while (!wrappedAround) { + for (Integer i : itemIndexes.keySet()) { + if (itemIndexes.get(i).equals(itemId)) { + return i; + } + } + // load in the next page. + int nextIndex = (currentOffset / (pageLength * CACHE_RATIO) + 1) + * (pageLength * CACHE_RATIO); + if (nextIndex >= size) { + // Container wrapped around, start from index 0. + wrappedAround = true; + nextIndex = 0; + } + updateOffsetAndCache(nextIndex); + } + return -1; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Indexed#getIdByIndex(int) + */ + + @Override + public Object getIdByIndex(int index) { + if (index < 0 || index > size() - 1) { + return null; + } + if (index < size) { + if (itemIndexes.keySet().contains(index)) { + return itemIndexes.get(index); + } + updateOffsetAndCache(index); + return itemIndexes.get(index); + } else { + // The index is in the added items + int offset = index - size; + return addedItems.get(offset).getId(); + } + } + + /**********************************************/ + /** Methods from interface Container.Ordered **/ + /**********************************************/ + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#nextItemId(java.lang.Object) + */ + + @Override + public Object nextItemId(Object itemId) { + return getIdByIndex(indexOfId(itemId) + 1); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#prevItemId(java.lang.Object) + */ + + @Override + public Object prevItemId(Object itemId) { + return getIdByIndex(indexOfId(itemId) - 1); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#firstItemId() + */ + + @Override + public Object firstItemId() { + updateCount(); + if (size == 0) { + if (addedItems.isEmpty()) { + return null; + } else { + int ix = -1; + do { + ix++; + } while (!itemPassesFilters(addedItems.get(ix)) + && ix < addedItems.size()); + if (ix < addedItems.size()) { + return addedItems.get(ix).getId(); + } + } + } + if (!itemIndexes.containsKey(0)) { + updateOffsetAndCache(0); + } + return itemIndexes.get(0); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#lastItemId() + */ + + @Override + public Object lastItemId() { + if (addedItems.isEmpty()) { + int lastIx = size() - 1; + if (!itemIndexes.containsKey(lastIx)) { + updateOffsetAndCache(size - 1); + } + return itemIndexes.get(lastIx); + } else { + int ix = addedItems.size(); + do { + ix--; + } while (!itemPassesFilters(addedItems.get(ix)) && ix >= 0); + if (ix >= 0) { + return addedItems.get(ix).getId(); + } else { + return null; + } + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#isFirstId(java.lang.Object) + */ + + @Override + public boolean isFirstId(Object itemId) { + return firstItemId().equals(itemId); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#isLastId(java.lang.Object) + */ + + @Override + public boolean isLastId(Object itemId) { + return lastItemId().equals(itemId); + } + + /***********************************************/ + /** Methods from interface Container.Sortable **/ + /***********************************************/ + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Sortable#sort(java.lang.Object[], + * boolean[]) + */ + + @Override + public void sort(Object[] propertyId, boolean[] ascending) { + sorters.clear(); + if (propertyId == null || propertyId.length == 0) { + refresh(); + return; + } + /* Generate OrderBy -objects */ + boolean asc = true; + for (int i = 0; i < propertyId.length; i++) { + /* Check that the property id is valid */ + if (propertyId[i] instanceof String + && propertyIds.contains(propertyId[i])) { + try { + asc = ascending[i]; + } catch (Exception e) { + getLogger().log(Level.WARNING, "", e); + } + sorters.add(new OrderBy((String) propertyId[i], asc)); + } + } + refresh(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Sortable#getSortableContainerPropertyIds() + */ + + @Override + public Collection<?> getSortableContainerPropertyIds() { + return getContainerPropertyIds(); + } + + /**************************************/ + /** Methods specific to SQLContainer **/ + /**************************************/ + + /** + * Refreshes the container - clears all caches and resets size and offset. + * Does NOT remove sorting or filtering rules! + */ + public void refresh() { + sizeDirty = true; + currentOffset = 0; + cachedItems.clear(); + itemIndexes.clear(); + fireContentsChange(); + } + + /** + * Returns modify state of the container. + * + * @return true if contents of this container have been modified + */ + public boolean isModified() { + return !removedItems.isEmpty() || !addedItems.isEmpty() + || !modifiedItems.isEmpty(); + } + + /** + * Set auto commit mode enabled or disabled. Auto commit mode means that all + * changes made to items of this container will be immediately written to + * the underlying data source. + * + * @param autoCommitEnabled + * true to enable auto commit mode + */ + public void setAutoCommit(boolean autoCommitEnabled) { + autoCommit = autoCommitEnabled; + } + + /** + * Returns status of the auto commit mode. + * + * @return true if auto commit mode is enabled + */ + public boolean isAutoCommit() { + return autoCommit; + } + + /** + * Returns the currently set page length. + * + * @return current page length + */ + public int getPageLength() { + return pageLength; + } + + /** + * Sets the page length used in lazy fetching of items from the data source. + * Also resets the cache size to match the new page length. + * + * As a side effect the container will be refreshed. + * + * @param pageLength + * new page length + */ + public void setPageLength(int pageLength) { + setPageLengthInternal(pageLength); + refresh(); + } + + /** + * Sets the page length internally, without refreshing the container. + * + * @param pageLength + * the new page length + */ + private void setPageLengthInternal(int pageLength) { + this.pageLength = pageLength > 0 ? pageLength : DEFAULT_PAGE_LENGTH; + cachedItems.setCacheLimit(CACHE_RATIO * getPageLength()); + } + + /** + * Adds the given OrderBy to this container and refreshes the container + * contents with the new sorting rules. + * + * Note that orderBy.getColumn() must return a column name that exists in + * this container. + * + * @param orderBy + * OrderBy to be added to the container sorting rules + */ + public void addOrderBy(OrderBy orderBy) { + if (orderBy == null) { + return; + } + if (!propertyIds.contains(orderBy.getColumn())) { + throw new IllegalArgumentException( + "The column given for sorting does not exist in this container."); + } + sorters.add(orderBy); + refresh(); + } + + /** + * Commits all the changes, additions and removals made to the items of this + * container. + * + * @throws UnsupportedOperationException + * @throws SQLException + */ + public void commit() throws UnsupportedOperationException, SQLException { + try { + getLogger().log(Level.FINER, + "Commiting changes through delegate..."); + delegate.beginTransaction(); + /* Perform buffered deletions */ + for (RowItem item : removedItems.values()) { + if (!delegate.removeRow(item)) { + throw new SQLException("Removal failed for row with ID: " + + item.getId()); + } + } + /* Perform buffered modifications */ + for (RowItem item : modifiedItems) { + if (delegate.storeRow(item) > 0) { + /* + * Also reset the modified state in the item in case it is + * reused e.g. in a form. + */ + item.commit(); + } else { + delegate.rollback(); + refresh(); + throw new ConcurrentModificationException( + "Item with the ID '" + item.getId() + + "' has been externally modified."); + } + } + /* Perform buffered additions */ + for (RowItem item : addedItems) { + delegate.storeRow(item); + } + delegate.commit(); + removedItems.clear(); + addedItems.clear(); + modifiedItems.clear(); + refresh(); + if (notificationsEnabled) { + CacheFlushNotifier.notifyOfCacheFlush(this); + } + } catch (SQLException e) { + delegate.rollback(); + throw e; + } catch (OptimisticLockException e) { + delegate.rollback(); + throw e; + } + } + + /** + * Rolls back all the changes, additions and removals made to the items of + * this container. + * + * @throws UnsupportedOperationException + * @throws SQLException + */ + public void rollback() throws UnsupportedOperationException, SQLException { + getLogger().log(Level.FINE, "Rolling back changes..."); + removedItems.clear(); + addedItems.clear(); + modifiedItems.clear(); + refresh(); + } + + /** + * Notifies this container that a property in the given item has been + * modified. The change will be buffered or made instantaneously depending + * on auto commit mode. + * + * @param changedItem + * item that has a modified property + */ + void itemChangeNotification(RowItem changedItem) { + if (autoCommit) { + try { + delegate.beginTransaction(); + if (delegate.storeRow(changedItem) == 0) { + delegate.rollback(); + refresh(); + throw new ConcurrentModificationException( + "Item with the ID '" + changedItem.getId() + + "' has been externally modified."); + } + delegate.commit(); + if (notificationsEnabled) { + CacheFlushNotifier.notifyOfCacheFlush(this); + } + getLogger().log(Level.FINER, "Row updated to DB..."); + } catch (SQLException e) { + getLogger().log(Level.WARNING, + "itemChangeNotification failed, rolling back...", e); + try { + delegate.rollback(); + } catch (SQLException ee) { + /* Nothing can be done here */ + getLogger().log(Level.SEVERE, "Rollback failed", e); + } + throw new RuntimeException(e); + } + } else { + if (!(changedItem.getId() instanceof TemporaryRowId) + && !modifiedItems.contains(changedItem)) { + modifiedItems.add(changedItem); + } + } + } + + /** + * Determines a new offset for updating the row cache. The offset is + * calculated from the given index, and will be fixed to match the start of + * a page, based on the value of pageLength. + * + * @param index + * Index of the item that was requested, but not found in cache + */ + private void updateOffsetAndCache(int index) { + if (itemIndexes.containsKey(index)) { + return; + } + currentOffset = (index / (pageLength * CACHE_RATIO)) + * (pageLength * CACHE_RATIO); + if (currentOffset < 0) { + currentOffset = 0; + } + getPage(); + } + + /** + * Fetches new count of rows from the data source, if needed. + */ + private void updateCount() { + if (!sizeDirty + && new Date().getTime() < sizeUpdated.getTime() + + sizeValidMilliSeconds) { + return; + } + try { + try { + delegate.setFilters(filters); + } catch (UnsupportedOperationException e) { + getLogger().log(Level.FINE, + "The query delegate doesn't support filtering", e); + } + try { + delegate.setOrderBy(sorters); + } catch (UnsupportedOperationException e) { + getLogger().log(Level.FINE, + "The query delegate doesn't support filtering", e); + } + int newSize = delegate.getCount(); + if (newSize != size) { + size = newSize; + refresh(); + } + sizeUpdated = new Date(); + sizeDirty = false; + getLogger().log(Level.FINER, + "Updated row count. New count is: " + size); + } catch (SQLException e) { + throw new RuntimeException("Failed to update item set size.", e); + } + } + + /** + * Fetches property id's (column names and their types) from the data + * source. + * + * @throws SQLException + */ + private void getPropertyIds() throws SQLException { + propertyIds.clear(); + propertyTypes.clear(); + delegate.setFilters(null); + delegate.setOrderBy(null); + ResultSet rs = null; + ResultSetMetaData rsmd = null; + try { + delegate.beginTransaction(); + rs = delegate.getResults(0, 1); + boolean resultExists = rs.next(); + rsmd = rs.getMetaData(); + Class<?> type = null; + for (int i = 1; i <= rsmd.getColumnCount(); i++) { + if (!isColumnIdentifierValid(rsmd.getColumnLabel(i))) { + continue; + } + String colName = rsmd.getColumnLabel(i); + /* + * Make sure not to add the same colName twice. This can easily + * happen if the SQL query joins many tables with an ID column. + */ + if (!propertyIds.contains(colName)) { + propertyIds.add(colName); + } + /* Try to determine the column's JDBC class by all means. */ + if (resultExists && rs.getObject(i) != null) { + type = rs.getObject(i).getClass(); + } else { + try { + type = Class.forName(rsmd.getColumnClassName(i)); + } catch (Exception e) { + getLogger().log(Level.WARNING, "Class not found", e); + /* On failure revert to Object and hope for the best. */ + type = Object.class; + } + } + /* + * Determine read only and nullability status of the column. A + * column is read only if it is reported as either read only or + * auto increment by the database, and also it is set as the + * version column in a TableQuery delegate. + */ + boolean readOnly = rsmd.isAutoIncrement(i) + || rsmd.isReadOnly(i); + if (delegate instanceof TableQuery + && rsmd.getColumnLabel(i).equals( + ((TableQuery) delegate).getVersionColumn())) { + readOnly = true; + } + propertyReadOnly.put(colName, readOnly); + propertyNullable.put(colName, + rsmd.isNullable(i) == ResultSetMetaData.columnNullable); + propertyTypes.put(colName, type); + } + rs.getStatement().close(); + rs.close(); + delegate.commit(); + getLogger().log(Level.FINER, "Property IDs fetched."); + } catch (SQLException e) { + getLogger().log(Level.WARNING, + "Failed to fetch property ids, rolling back", e); + try { + delegate.rollback(); + } catch (SQLException e1) { + getLogger().log(Level.SEVERE, "Failed to roll back", e1); + } + try { + if (rs != null) { + if (rs.getStatement() != null) { + rs.getStatement().close(); + } + rs.close(); + } + } catch (SQLException e1) { + getLogger().log(Level.WARNING, "Failed to close session", e1); + } + throw e; + } + } + + /** + * Fetches a page from the data source based on the values of pageLenght and + * currentOffset. Also updates the set of primary keys, used in + * identification of RowItems. + */ + private void getPage() { + updateCount(); + ResultSet rs = null; + ResultSetMetaData rsmd = null; + cachedItems.clear(); + itemIndexes.clear(); + try { + try { + delegate.setOrderBy(sorters); + } catch (UnsupportedOperationException e) { + /* The query delegate doesn't support sorting. */ + /* No need to do anything. */ + getLogger().log(Level.FINE, + "The query delegate doesn't support sorting", e); + } + delegate.beginTransaction(); + rs = delegate.getResults(currentOffset, pageLength * CACHE_RATIO); + rsmd = rs.getMetaData(); + List<String> pKeys = delegate.getPrimaryKeyColumns(); + // } + /* Create new items and column properties */ + ColumnProperty cp = null; + int rowCount = currentOffset; + if (!delegate.implementationRespectsPagingLimits()) { + rowCount = currentOffset = 0; + setPageLengthInternal(size); + } + while (rs.next()) { + List<ColumnProperty> itemProperties = new ArrayList<ColumnProperty>(); + /* Generate row itemId based on primary key(s) */ + Object[] itemId = new Object[pKeys.size()]; + for (int i = 0; i < pKeys.size(); i++) { + itemId[i] = rs.getObject(pKeys.get(i)); + } + RowId id = null; + if (pKeys.isEmpty()) { + id = new ReadOnlyRowId(rs.getRow()); + } else { + id = new RowId(itemId); + } + List<String> propertiesToAdd = new ArrayList<String>( + propertyIds); + if (!removedItems.containsKey(id)) { + for (int i = 1; i <= rsmd.getColumnCount(); i++) { + if (!isColumnIdentifierValid(rsmd.getColumnLabel(i))) { + continue; + } + String colName = rsmd.getColumnLabel(i); + Object value = rs.getObject(i); + Class<?> type = value != null ? value.getClass() + : Object.class; + if (value == null) { + for (String propName : propertyTypes.keySet()) { + if (propName.equals(rsmd.getColumnLabel(i))) { + type = propertyTypes.get(propName); + break; + } + } + } + /* + * In case there are more than one column with the same + * name, add only the first one. This can easily happen + * if you join many tables where each table has an ID + * column. + */ + if (propertiesToAdd.contains(colName)) { + cp = new ColumnProperty(colName, + propertyReadOnly.get(colName), + !propertyReadOnly.get(colName), + propertyNullable.get(colName), value, type); + itemProperties.add(cp); + propertiesToAdd.remove(colName); + } + } + /* Cache item */ + itemIndexes.put(rowCount, id); + + // if an item with the id is contained in the modified + // cache, then use this record and add it to the cached + // items. Otherwise create a new item + int modifiedIndex = indexInModifiedCache(id); + if (modifiedIndex != -1) { + cachedItems.put(id, modifiedItems.get(modifiedIndex)); + } else { + cachedItems.put(id, new RowItem(this, id, + itemProperties)); + } + + rowCount++; + } + } + rs.getStatement().close(); + rs.close(); + delegate.commit(); + getLogger().log( + Level.FINER, + "Fetched " + pageLength * CACHE_RATIO + + " rows starting from " + currentOffset); + } catch (SQLException e) { + getLogger().log(Level.WARNING, + "Failed to fetch rows, rolling back", e); + try { + delegate.rollback(); + } catch (SQLException e1) { + getLogger().log(Level.SEVERE, "Failed to roll back", e1); + } + try { + if (rs != null) { + if (rs.getStatement() != null) { + rs.getStatement().close(); + rs.close(); + } + } + } catch (SQLException e1) { + getLogger().log(Level.WARNING, "Failed to close session", e1); + } + throw new RuntimeException("Failed to fetch page.", e); + } + } + + /** + * Returns the index of the item with the given itemId for the modified + * cache. + * + * @param itemId + * @return the index of the item with the itemId in the modified cache. Or + * -1 if not found. + */ + private int indexInModifiedCache(Object itemId) { + for (int ix = 0; ix < modifiedItems.size(); ix++) { + RowItem item = modifiedItems.get(ix); + if (item.getId().equals(itemId)) { + return ix; + } + } + return -1; + } + + private int sizeOfAddedItems() { + return getFilteredAddedItems().size(); + } + + private List<RowItem> getFilteredAddedItems() { + ArrayList<RowItem> filtered = new ArrayList<RowItem>(addedItems); + if (filters != null && !filters.isEmpty()) { + for (RowItem item : addedItems) { + if (!itemPassesFilters(item)) { + filtered.remove(item); + } + } + } + return filtered; + } + + private boolean itemPassesFilters(RowItem item) { + for (Filter filter : filters) { + if (!filter.passesFilter(item.getId(), item)) { + return false; + } + } + return true; + } + + /** + * Checks is the given column identifier valid to be used with SQLContainer. + * Currently the only non-valid identifier is "rownum" when MSSQL or Oracle + * is used. This is due to the way the SELECT queries are constructed in + * order to implement paging in these databases. + * + * @param identifier + * Column identifier + * @return true if the identifier is valid + */ + private boolean isColumnIdentifierValid(String identifier) { + if (identifier.equalsIgnoreCase("rownum") + && delegate instanceof TableQuery) { + TableQuery tq = (TableQuery) delegate; + if (tq.getSqlGenerator() instanceof MSSQLGenerator + || tq.getSqlGenerator() instanceof OracleGenerator) { + return false; + } + } + return true; + } + + /** + * Returns the QueryDelegate set for this SQLContainer. + * + * @return current querydelegate + */ + protected QueryDelegate getQueryDelegate() { + return delegate; + } + + /************************************/ + /** UNSUPPORTED CONTAINER FEATURES **/ + /************************************/ + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#addContainerProperty(java.lang.Object, + * java.lang.Class, java.lang.Object) + */ + + @Override + public boolean addContainerProperty(Object propertyId, Class<?> type, + Object defaultValue) throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeContainerProperty(java.lang.Object) + */ + + @Override + public boolean removeContainerProperty(Object propertyId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#addItem(java.lang.Object) + */ + + @Override + public Item addItem(Object itemId) throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object, + * java.lang.Object) + */ + + @Override + public Item addItemAfter(Object previousItemId, Object newItemId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Indexed#addItemAt(int, java.lang.Object) + */ + + @Override + public Item addItemAt(int index, Object newItemId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Indexed#addItemAt(int) + */ + + @Override + public Object addItemAt(int index) throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object) + */ + + @Override + public Object addItemAfter(Object previousItemId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /******************************************/ + /** ITEMSETCHANGENOTIFIER IMPLEMENTATION **/ + /******************************************/ + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.Container.ItemSetChangeNotifier#addListener(com.vaadin + * .data.Container.ItemSetChangeListener) + */ + + @Override + public void addListener(Container.ItemSetChangeListener listener) { + if (itemSetChangeListeners == null) { + itemSetChangeListeners = new LinkedList<Container.ItemSetChangeListener>(); + } + itemSetChangeListeners.add(listener); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.Container.ItemSetChangeNotifier#removeListener(com.vaadin + * .data.Container.ItemSetChangeListener) + */ + + @Override + public void removeListener(Container.ItemSetChangeListener listener) { + if (itemSetChangeListeners != null) { + itemSetChangeListeners.remove(listener); + } + } + + protected void fireContentsChange() { + if (itemSetChangeListeners != null) { + final Object[] l = itemSetChangeListeners.toArray(); + final Container.ItemSetChangeEvent event = new SQLContainer.ItemSetChangeEvent( + this); + for (int i = 0; i < l.length; i++) { + ((Container.ItemSetChangeListener) l[i]) + .containerItemSetChange(event); + } + } + } + + /** + * Simple ItemSetChangeEvent implementation. + */ + @SuppressWarnings("serial") + public static class ItemSetChangeEvent extends EventObject implements + Container.ItemSetChangeEvent { + + private ItemSetChangeEvent(SQLContainer source) { + super(source); + } + + @Override + public Container getContainer() { + return (Container) getSource(); + } + } + + /**************************************************/ + /** ROWIDCHANGELISTENER PASSING TO QUERYDELEGATE **/ + /**************************************************/ + + /** + * Adds a RowIdChangeListener to the QueryDelegate + * + * @param listener + */ + public void addListener(RowIdChangeListener listener) { + if (delegate instanceof QueryDelegate.RowIdChangeNotifier) { + ((QueryDelegate.RowIdChangeNotifier) delegate) + .addListener(listener); + } + } + + /** + * Removes a RowIdChangeListener from the QueryDelegate + * + * @param listener + */ + public void removeListener(RowIdChangeListener listener) { + if (delegate instanceof QueryDelegate.RowIdChangeNotifier) { + ((QueryDelegate.RowIdChangeNotifier) delegate) + .removeListener(listener); + } + } + + /** + * Calling this will enable this SQLContainer to send and receive cache + * flush notifications for its lifetime. + */ + public void enableCacheFlushNotifications() { + if (!notificationsEnabled) { + notificationsEnabled = true; + CacheFlushNotifier.addInstance(this); + } + } + + /******************************************/ + /** Referencing mechanism implementation **/ + /******************************************/ + + /** + * Adds a new reference to the given SQLContainer. In addition to the + * container you must provide the column (property) names used for the + * reference in both this and the referenced SQLContainer. + * + * Note that multiple references pointing to the same SQLContainer are not + * supported. + * + * @param refdCont + * Target SQLContainer of the new reference + * @param refingCol + * Column (property) name in this container storing the (foreign + * key) reference + * @param refdCol + * Column (property) name in the referenced container storing the + * referenced key + */ + public void addReference(SQLContainer refdCont, String refingCol, + String refdCol) { + if (refdCont == null) { + throw new IllegalArgumentException( + "Referenced SQLContainer can not be null."); + } + if (!getContainerPropertyIds().contains(refingCol)) { + throw new IllegalArgumentException( + "Given referencing column name is invalid." + + " Please ensure that this container" + + " contains a property ID named: " + refingCol); + } + if (!refdCont.getContainerPropertyIds().contains(refdCol)) { + throw new IllegalArgumentException( + "Given referenced column name is invalid." + + " Please ensure that the referenced container" + + " contains a property ID named: " + refdCol); + } + if (references.keySet().contains(refdCont)) { + throw new IllegalArgumentException( + "An SQLContainer instance can only be referenced once."); + } + references.put(refdCont, new Reference(refdCont, refingCol, refdCol)); + } + + /** + * Removes the reference pointing to the given SQLContainer. + * + * @param refdCont + * Target SQLContainer of the reference + * @return true if successful, false if the reference did not exist + */ + public boolean removeReference(SQLContainer refdCont) { + if (refdCont == null) { + throw new IllegalArgumentException( + "Referenced SQLContainer can not be null."); + } + return references.remove(refdCont) == null ? false : true; + } + + /** + * Sets the referenced item. The referencing column of the item in this + * container is updated accordingly. + * + * @param itemId + * Item Id of the reference source (from this container) + * @param refdItemId + * Item Id of the reference target (from referenced container) + * @param refdCont + * Target SQLContainer of the reference + * @return true if the referenced item was successfully set, false on + * failure + */ + public boolean setReferencedItem(Object itemId, Object refdItemId, + SQLContainer refdCont) { + if (refdCont == null) { + throw new IllegalArgumentException( + "Referenced SQLContainer can not be null."); + } + Reference r = references.get(refdCont); + if (r == null) { + throw new IllegalArgumentException( + "Reference to the given SQLContainer not defined."); + } + try { + getContainerProperty(itemId, r.getReferencingColumn()).setValue( + refdCont.getContainerProperty(refdItemId, + r.getReferencedColumn())); + return true; + } catch (Exception e) { + getLogger() + .log(Level.WARNING, "Setting referenced item failed.", e); + return false; + } + } + + /** + * Fetches the Item Id of the referenced item from the target SQLContainer. + * + * @param itemId + * Item Id of the reference source (from this container) + * @param refdCont + * Target SQLContainer of the reference + * @return Item Id of the referenced item, or null if not found + */ + public Object getReferencedItemId(Object itemId, SQLContainer refdCont) { + if (refdCont == null) { + throw new IllegalArgumentException( + "Referenced SQLContainer can not be null."); + } + Reference r = references.get(refdCont); + if (r == null) { + throw new IllegalArgumentException( + "Reference to the given SQLContainer not defined."); + } + Object refKey = getContainerProperty(itemId, r.getReferencingColumn()) + .getValue(); + + refdCont.removeAllContainerFilters(); + refdCont.addContainerFilter(new Equal(r.getReferencedColumn(), refKey)); + Object toReturn = refdCont.firstItemId(); + refdCont.removeAllContainerFilters(); + return toReturn; + } + + /** + * Fetches the referenced item from the target SQLContainer. + * + * @param itemId + * Item Id of the reference source (from this container) + * @param refdCont + * Target SQLContainer of the reference + * @return The referenced item, or null if not found + */ + public Item getReferencedItem(Object itemId, SQLContainer refdCont) { + return refdCont.getItem(getReferencedItemId(itemId, refdCont)); + } + + private void writeObject(java.io.ObjectOutputStream out) throws IOException { + out.defaultWriteObject(); + } + + private void readObject(java.io.ObjectInputStream in) throws IOException, + ClassNotFoundException { + in.defaultReadObject(); + if (notificationsEnabled) { + /* + * Register instance with CacheFlushNotifier after de-serialization + * if notifications are enabled + */ + CacheFlushNotifier.addInstance(this); + } + } + + private static final Logger getLogger() { + return Logger.getLogger(SQLContainer.class.getName()); + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/SQLUtil.java b/server/src/com/vaadin/data/util/sqlcontainer/SQLUtil.java new file mode 100644 index 0000000000..4a48dbf499 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/SQLUtil.java @@ -0,0 +1,36 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer; + +import java.io.Serializable; + +public class SQLUtil implements Serializable { + /** + * Escapes different special characters in strings that are passed to SQL. + * Replaces the following: + * + * <list> <li>' is replaced with ''</li> <li>\x00 is removed</li> <li>\ is + * replaced with \\</li> <li>" is replaced with \"</li> <li> + * \x1a is removed</li> </list> + * + * Also note! The escaping done here may or may not be enough to prevent any + * and all SQL injections so it is recommended to check user input before + * giving it to the SQLContainer/TableQuery. + * + * @param constant + * @return \\\'\' + */ + public static String escapeSQL(String constant) { + if (constant == null) { + return null; + } + String fixedConstant = constant; + fixedConstant = fixedConstant.replaceAll("\\\\x00", ""); + fixedConstant = fixedConstant.replaceAll("\\\\x1a", ""); + fixedConstant = fixedConstant.replaceAll("'", "''"); + fixedConstant = fixedConstant.replaceAll("\\\\", "\\\\\\\\"); + fixedConstant = fixedConstant.replaceAll("\\\"", "\\\\\""); + return fixedConstant; + } +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/TemporaryRowId.java b/server/src/com/vaadin/data/util/sqlcontainer/TemporaryRowId.java new file mode 100644 index 0000000000..b4bca75a2a --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/TemporaryRowId.java @@ -0,0 +1,32 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer; + +public class TemporaryRowId extends RowId { + private static final long serialVersionUID = -641983830469018329L; + + public TemporaryRowId(Object[] id) { + super(id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || !(obj instanceof TemporaryRowId)) { + return false; + } + Object[] compId = ((TemporaryRowId) obj).getId(); + return id.equals(compId); + } + + @Override + public String toString() { + return "Temporary row id"; + } + +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/connection/J2EEConnectionPool.java b/server/src/com/vaadin/data/util/sqlcontainer/connection/J2EEConnectionPool.java new file mode 100644 index 0000000000..9aa4f7c4be --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/connection/J2EEConnectionPool.java @@ -0,0 +1,72 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.connection; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.sql.DataSource; + +public class J2EEConnectionPool implements JDBCConnectionPool { + + private String dataSourceJndiName; + + private DataSource dataSource = null; + + public J2EEConnectionPool(DataSource dataSource) { + this.dataSource = dataSource; + } + + public J2EEConnectionPool(String dataSourceJndiName) { + this.dataSourceJndiName = dataSourceJndiName; + } + + @Override + public Connection reserveConnection() throws SQLException { + Connection conn = getDataSource().getConnection(); + conn.setAutoCommit(false); + + return conn; + } + + private DataSource getDataSource() throws SQLException { + if (dataSource == null) { + dataSource = lookupDataSource(); + } + return dataSource; + } + + private DataSource lookupDataSource() throws SQLException { + try { + InitialContext ic = new InitialContext(); + return (DataSource) ic.lookup(dataSourceJndiName); + } catch (NamingException e) { + throw new SQLException( + "NamingException - Cannot connect to the database. Cause: " + + e.getMessage()); + } + } + + @Override + public void releaseConnection(Connection conn) { + if (conn != null) { + try { + conn.close(); + } catch (SQLException e) { + Logger.getLogger(J2EEConnectionPool.class.getName()).log( + Level.FINE, "Could not release SQL connection", e); + } + } + } + + @Override + public void destroy() { + dataSource = null; + } + +}
\ No newline at end of file diff --git a/server/src/com/vaadin/data/util/sqlcontainer/connection/JDBCConnectionPool.java b/server/src/com/vaadin/data/util/sqlcontainer/connection/JDBCConnectionPool.java new file mode 100644 index 0000000000..cf12461588 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/connection/JDBCConnectionPool.java @@ -0,0 +1,41 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.connection; + +import java.io.Serializable; +import java.sql.Connection; +import java.sql.SQLException; + +/** + * Interface for implementing connection pools to be used with SQLContainer. + */ +public interface JDBCConnectionPool extends Serializable { + /** + * Retrieves a connection. + * + * @return a usable connection to the database + * @throws SQLException + */ + public Connection reserveConnection() throws SQLException; + + /** + * Releases a connection that was retrieved earlier. + * + * Note that depending on implementation, the transaction possibly open in + * the connection may or may not be rolled back. + * + * @param conn + * Connection to be released + */ + public void releaseConnection(Connection conn); + + /** + * Destroys the connection pool: close() is called an all the connections in + * the pool, whether available or reserved. + * + * This method was added to fix PostgreSQL -related issues with connections + * that were left hanging 'idle'. + */ + public void destroy(); +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/connection/SimpleJDBCConnectionPool.java b/server/src/com/vaadin/data/util/sqlcontainer/connection/SimpleJDBCConnectionPool.java new file mode 100644 index 0000000000..21760014b9 --- /dev/null +++ b/server/src/com/vaadin/data/util/sqlcontainer/connection/SimpleJDBCConnectionPool.java @@ -0,0 +1,168 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.data.util.sqlcontainer.connection; + +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.HashSet; +import java.util.Set; + +/** + * Simple implementation of the JDBCConnectionPool interface. Handles loading + * the JDBC driver, setting up the connections and ensuring they are still + * usable upon release. + */ +@SuppressWarnings("serial") +public class SimpleJDBCConnectionPool implements JDBCConnectionPool { + + private int initialConnections = 5; + private int maxConnections = 20; + + private String driverName; + private String connectionUri; + private String userName; + private String password; + + private transient Set<Connection> availableConnections; + private transient Set<Connection> reservedConnections; + + private boolean initialized; + + public SimpleJDBCConnectionPool(String driverName, String connectionUri, + String userName, String password) throws SQLException { + if (driverName == null) { + throw new IllegalArgumentException( + "JDBC driver class name must be given."); + } + if (connectionUri == null) { + throw new IllegalArgumentException( + "Database connection URI must be given."); + } + if (userName == null) { + throw new IllegalArgumentException( + "Database username must be given."); + } + if (password == null) { + throw new IllegalArgumentException( + "Database password must be given."); + } + this.driverName = driverName; + this.connectionUri = connectionUri; + this.userName = userName; + this.password = password; + + /* Initialize JDBC driver */ + try { + Class.forName(driverName).newInstance(); + } catch (Exception ex) { + throw new RuntimeException("Specified JDBC Driver: " + driverName + + " - initialization failed.", ex); + } + } + + public SimpleJDBCConnectionPool(String driverName, String connectionUri, + String userName, String password, int initialConnections, + int maxConnections) throws SQLException { + this(driverName, connectionUri, userName, password); + this.initialConnections = initialConnections; + this.maxConnections = maxConnections; + } + + private void initializeConnections() throws SQLException { + availableConnections = new HashSet<Connection>(initialConnections); + reservedConnections = new HashSet<Connection>(initialConnections); + for (int i = 0; i < initialConnections; i++) { + availableConnections.add(createConnection()); + } + initialized = true; + } + + @Override + public synchronized Connection reserveConnection() throws SQLException { + if (!initialized) { + initializeConnections(); + } + if (availableConnections.isEmpty()) { + if (reservedConnections.size() < maxConnections) { + availableConnections.add(createConnection()); + } else { + throw new SQLException("Connection limit has been reached."); + } + } + + Connection c = availableConnections.iterator().next(); + availableConnections.remove(c); + reservedConnections.add(c); + + return c; + } + + @Override + public synchronized void releaseConnection(Connection conn) { + if (conn == null || !initialized) { + return; + } + /* Try to roll back if necessary */ + try { + if (!conn.getAutoCommit()) { + conn.rollback(); + } + } catch (SQLException e) { + /* Roll back failed, close and discard connection */ + try { + conn.close(); + } catch (SQLException e1) { + /* Nothing needs to be done */ + } + reservedConnections.remove(conn); + return; + } + reservedConnections.remove(conn); + availableConnections.add(conn); + } + + private Connection createConnection() throws SQLException { + Connection c = DriverManager.getConnection(connectionUri, userName, + password); + c.setAutoCommit(false); + if (driverName.toLowerCase().contains("mysql")) { + try { + Statement s = c.createStatement(); + s.execute("SET SESSION sql_mode = 'ANSI'"); + s.close(); + } catch (Exception e) { + // Failed to set ansi mode; continue + } + } + return c; + } + + @Override + public void destroy() { + for (Connection c : availableConnections) { + try { + c.close(); + } catch (SQLException e) { + // No need to do anything + } + } + for (Connection c : reservedConnections) { + try { + c.close(); + } catch (SQLException e) { + // No need to do anything + } + } + + } + + private void writeObject(java.io.ObjectOutputStream out) throws IOException { + initialized = false; + out.defaultWriteObject(); + } + +} 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 + ")"; + } +} |